diff --git a/cmd/quarry-client/http.go b/cmd/quarry-client/http.go index d2fa542..291e889 100644 --- a/cmd/quarry-client/http.go +++ b/cmd/quarry-client/http.go @@ -131,7 +131,7 @@ func serveSHA256SUMS(rw http.ResponseWriter, req *http.Request) (Err error) { } // Extension targets are consumed by quarry-sysupdate itself, not by // systemd-sysupdate. - if strings.HasPrefix(target.Path, xsysupdate.ExtensionTargetPrefix) { + if _, _, ok := xsysupdate.CutExtensionTargetPrefix(target.Path); ok { slog.Debug("Stripped quarry extension target from generated SHA256SUMS", "target", target.Path) continue diff --git a/cmd/quarry-sysupdate/update.go b/cmd/quarry-sysupdate/update.go index aaef769..983721b 100644 --- a/cmd/quarry-sysupdate/update.go +++ b/cmd/quarry-sysupdate/update.go @@ -8,10 +8,11 @@ import ( "errors" "fmt" "log/slog" - "strings" "github.com/urfave/cli/v3" + "go.amutable.dev/quarry/internal/third_party/assert" + "go.amutable.dev/quarry/internal/tufclient" "go.amutable.dev/quarry/internal/xsysupdate" ) @@ -38,20 +39,55 @@ var updateCommand = withExtensionDirFlag(withQuarryUserFlag(withQuarryProxyFlag( } slog.Info("Initialised extensions.", "extensions", exts.String()) - for target, err := range unprivIterTargetFiles(ctx) { - if err != nil { - return fmt.Errorf("error while scanning repos: %w", err) - } - if !strings.HasPrefix(target.Path, xsysupdate.ExtensionTargetPrefix) { - continue - } + // Legacy-prefixed targets are only applied after all other targets so + // we can allow them to be overridden by the non-legacy prefix. + // TODO: Drop this. + var ( + applied = map[string]struct{}{} // uses the stripped name + legacyTargets []*tufclient.TargetInfo + ) + applyTarget := func(target *tufclient.TargetInfo) error { slog.Info("Extension target found.", "target", target.Path, "repository", target.Repo.Name) + name, legacy, ok := xsysupdate.CutExtensionTargetPrefix(target.Path) + assert.Assertf(ok, "extension target path %q must have extension prefix", target.Path) + if _, ok := applied[name]; ok { + slog.Info("Skipping legacy extension target superseded by new prefix.", + "target", target.Path, "repository", target.Repo.Name) + assert.Assertf(!legacy, "extension target path %q must not be applied after legacy paths", target.Path) + return nil + } if err := exts.DoApplyTarget(ctx, target); err != nil { return fmt.Errorf("apply extension target %s: %w", target.Path, err) } + applied[name] = struct{}{} slog.Info("Extension target applied.", "target", target.Path, "repository", target.Repo.Name) + return nil + } + for target, err := range unprivIterTargetFiles(ctx) { + if err != nil { + return fmt.Errorf("error while scanning repos: %w", err) + } + if _, legacy, ok := xsysupdate.CutExtensionTargetPrefix(target.Path); !ok { + // Not an extension target file. + continue + } else if legacy { + // Defer application of legacy-prefixed extensions until we've + // done everything else so we can skip them if there was a + // non-legacy version of the same file. + legacyTargets = append(legacyTargets, target) + continue + } + // Otherwise, apply the extension target file. + if err := applyTarget(target); err != nil { + return err + } + } + for _, target := range legacyTargets { + if err := applyTarget(target); err != nil { + return err + } } if err := exts.DoBeforeUpdate(ctx); err != nil { return fmt.Errorf("before update extensions: %w", err) diff --git a/hack/quarry-bakery b/hack/quarry-bakery index 87b6f13..2b692ce 100755 --- a/hack/quarry-bakery +++ b/hack/quarry-bakery @@ -37,7 +37,7 @@ SHA256SUMS="$(mktemp "quarry-bakery-$sysext.SHA256SUMS.XXXXXX")" trap 'rm -f "$SHA256SUMS"' EXIT curl -sSL -o "$SHA256SUMS" "https://github.com/flatcar/sysext-bakery/releases/download/$sysext/SHA256SUMS" -# Pre-compute the special .zzz-quarry-special/sysupdate.FOO.d/* entries for the +# Pre-compute the special .quarry/sysupdate.FOO.d/* entries for the # repository to insert it into the targets.json. sysext_transfer_url="https://github.com/flatcar/sysext-bakery/releases/download/$sysext/$sysext.conf" sysext_transfer="$(curl -sSL -o- "$sysext_transfer_url")" @@ -51,7 +51,7 @@ sysext_transfer_entry="$(cat <<-EOF "version": 1, "spec_version": "1.0.31", "targets": { - ".zzz-quarry-special/sysupdate.$sysext.d/$sysext.transfer": { + ".quarry/sysupdate.$sysext.d/$sysext.transfer": { "hashes": { "sha256": "$(sha256sum <<<"$sysext_transfer" | awk '{ print $1 }')" }, diff --git a/internal/xsysupdate/extensions.go b/internal/xsysupdate/extensions.go index ef77028..617b0f0 100644 --- a/internal/xsysupdate/extensions.go +++ b/internal/xsysupdate/extensions.go @@ -12,13 +12,37 @@ import ( "errors" "fmt" "log/slog" + "strings" "go.amutable.dev/quarry/internal/tufclient" ) -// ExtensionTargetPrefix is the path prefix used by quarry extension target -// files to indicate they are a special kind of repository metadata. -const ExtensionTargetPrefix = ".zzz-quarry-special/" +const ( + // ExtensionTargetPrefix is the path prefix used by quarry extension target + // files to indicate they are a special kind of repository metadata. + ExtensionTargetPrefix = ".quarry/" + + // legacyExtensionTargetPrefix is the old name for [ExtensionTargetPrefix], + // still accepted for backward compatibility. + // TODO: Drop this. + legacyExtensionTargetPrefix = ".zzz-quarry-special/" +) + +// CutExtensionTargetPrefix returns path without its [ExtensionTargetPrefix] +// with similar semantics to [strings.CutPrefix]. legacy is true if the prefix +// is actually the legacy (to-be-removed) prefix and should be used when +// deciding which version of an extension path should have priority. +func CutExtensionTargetPrefix(path string) (name string, legacy, ok bool) { + for prefix, isLegacy := range map[string]bool{ + ExtensionTargetPrefix: false, + legacyExtensionTargetPrefix: true, + } { + if name, ok := strings.CutPrefix(path, prefix); ok { + return name, isLegacy, ok + } + } + return path, false, false +} // Extension represents a Quarry extension that deal with special files in the // repository. diff --git a/internal/xsysupdate/tags.go b/internal/xsysupdate/tags.go index 370c973..5ffa4c7 100644 --- a/internal/xsysupdate/tags.go +++ b/internal/xsysupdate/tags.go @@ -26,7 +26,10 @@ const ( // The filename suffix is the tag name, and the target file contents are // the tag value. If the file is zero-length then the tag is a "sentinel" // and thus is just of the form "foo.bar" rather than "foo.bar=baz". - TagsPrefix = ExtensionTargetPrefix + "machine-tags/" + TagsPrefix = ExtensionTargetPrefix + tagsName + + // tagsName is [TagsPrefix] without the [ExtensionTargetPrefix]. + tagsName = "machine-tags/" // maxTagFileSize is the maximum tag target file size we are willing to // read. hostnamed has a restriction of 255 characters but lets use @@ -132,10 +135,11 @@ func (ext *TagsExtension) Init(ctx context.Context) (context.Context, error) { // ApplyTarget collects the machine tag described by the target file, to be // applied in BeforeUpdate. func (ext *TagsExtension) ApplyTarget(ctx context.Context, info *tufclient.TargetInfo) (_ bool, Err error) { - if !strings.HasPrefix(info.Path, TagsPrefix) { + name, _, ok := CutExtensionTargetPrefix(info.Path) + if !ok || !strings.HasPrefix(name, tagsName) { return false, nil // not for this extension } - tagName := strings.TrimPrefix(info.Path, TagsPrefix) + tagName := strings.TrimPrefix(name, tagsName) slog.Info("[xsysupdate tags target] Collecting machine tag file.", "target", info.Path, "repository", info.Repo.Name, "tagName", tagName) diff --git a/internal/xsysupdate/tags_test.go b/internal/xsysupdate/tags_test.go index 0d371a1..447a4dc 100644 --- a/internal/xsysupdate/tags_test.go +++ b/internal/xsysupdate/tags_test.go @@ -162,8 +162,8 @@ func TestTagsApplyTarget_NotForUs(t *testing.T) { for _, targetPath := range []string{ "regular/target.bin", - ".zzz-quarry-special/sysupdate.d/foo.transfer", - ".zzz-quarry-special/machine-tags", // no trailing slash: not a tag file + ".quarry/sysupdate.d/foo.transfer", + ".quarry/machine-tags", // no trailing slash: not a tag file } { info := &tufclient.TargetInfo{ TargetFiles: &tufmetadata.TargetFiles{Path: targetPath}, diff --git a/internal/xsysupdate/transfer.go b/internal/xsysupdate/transfer.go index ee427a2..12f7725 100644 --- a/internal/xsysupdate/transfer.go +++ b/internal/xsysupdate/transfer.go @@ -31,7 +31,11 @@ const ( // extension to sysupdate. Any target file in a repository with this prefix // will be installed as /etc/sysupdate.* (with the [Source] Path= adjusted to // point to the local quarry-http-client server). - TransferFilePrefix = ExtensionTargetPrefix + "sysupdate." + TransferFilePrefix = ExtensionTargetPrefix + transferFileName + + // transferFileName is [TransferFilePrefix] without the + // [ExtensionTargetPrefix]. + transferFileName = "sysupdate." liveLink = "live" lastLink = "last" @@ -227,14 +231,10 @@ func (ext *TransferFileExtension) patchTransferFile(wtr io.Writer, rdr io.Reader // ApplyTarget saves the transfer file from the repository to the per-update // transfer directory. func (ext *TransferFileExtension) ApplyTarget(ctx context.Context, info *tufclient.TargetInfo) (_ bool, Err error) { - if !strings.HasPrefix(info.Path, TransferFilePrefix) { + transferPath, _, ok := CutExtensionTargetPrefix(info.Path) + if !ok || !strings.HasPrefix(transferPath, transferFileName) { return false, nil // not for this extension } - transferPath, ok := strings.CutPrefix(info.Path, ExtensionTargetPrefix) - if !ok { - // Should never happen. - return false, fmt.Errorf("extension transfer file %s does not have %s prefix", info.Path, ExtensionTargetPrefix) - } slog.Info("[xsysupdate transfers target] Applying transfer file.", "target", info.Path, "repository", info.Repo.Name) diff --git a/internal/xsysupdate/transfer_test.go b/internal/xsysupdate/transfer_test.go index d68f856..c781d9f 100644 --- a/internal/xsysupdate/transfer_test.go +++ b/internal/xsysupdate/transfer_test.go @@ -591,7 +591,7 @@ Type=partition MatchPattern=foo_@v `) + "\n") - const targetPath = ".zzz-quarry-special/sysupdate.d/foo.transfer" + const targetPath = ".quarry/sysupdate.d/foo.transfer" srv := testrepo.New(t) target := srv.WriteTarget(t, targetPath, bytes.NewReader(body)) @@ -626,7 +626,7 @@ func TestApplyTarget_NestedPath(t *testing.T) { body := []byte("dummy=value\n") - const targetPath = ".zzz-quarry-special/sysupdate.d/foo.transfer.d/x.conf" + const targetPath = ".quarry/sysupdate.d/foo.transfer.d/x.conf" srv := testrepo.New(t) target := srv.WriteTarget(t, targetPath, bytes.NewReader(body))