From e068a573c6ba381ec9e00787dfe40397cbdc7dbb Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 15 Oct 2026 09:31:09 +0200 Subject: [PATCH 1/2] sysupdate: replace the remote machine-tags setting with local tags We used to set the machine tags based on a remote TUF tags file. In the most recent discussions we decided that we would instead make this more client focused. So this commit drops setting the tags from a remote resource and instead relies on something else setting them. In our case that is a confext using /etc/tags.d [1]. Without this commit the machine tags from the confext would be reset by quarry every run. This also enables `Wants=systemd-sysupdate-auto-enable.service` now so that we get the auto-enable of components/features based on machine-tags. It does not do the loop-until-things-settled, that is a bigger change and our current 2min timer will do okay but we really need this soon too. Signed-off-by: Michael Vogt [1] https://github.com/amutable-systems/amutablectl/pull/194 --- cmd/quarry-sysupdate/update.go | 8 -------- contrib/systemd/quarry-sysupdate.service | 6 ++++++ docs/getting-started.md | 4 ++-- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/cmd/quarry-sysupdate/update.go b/cmd/quarry-sysupdate/update.go index 983721b..b5d9a24 100644 --- a/cmd/quarry-sysupdate/update.go +++ b/cmd/quarry-sysupdate/update.go @@ -24,14 +24,6 @@ var updateCommand = withExtensionDirFlag(withQuarryUserFlag(withQuarryProxyFlag( &xsysupdate.TransferFileExtension{ OverrideSourcePathURL: ctxQuarryProxyURL(ctx), }, - &xsysupdate.TagsExtension{ - // TODO: There is no ownership model for machine tags yet, so - // as a temporary workaround we take full control of the acp.* - // and amutable.* tag namespaces. These tags always exactly - // match the set shipped by the configured repositories, and - // tags outside them are never touched. - AllowedNamespaces: []string{"acp.", "amutable."}, - }, } ctx, err := exts.DoInit(ctx) if err != nil { diff --git a/contrib/systemd/quarry-sysupdate.service b/contrib/systemd/quarry-sysupdate.service index c61f682..ea7feeb 100644 --- a/contrib/systemd/quarry-sysupdate.service +++ b/contrib/systemd/quarry-sysupdate.service @@ -7,6 +7,12 @@ Description=Quarry-Based Automatic System Update Wants=network-online.target After=network-online.target +# Features gated on machine tags (SuggestOnMachineTag=) become enabled only +# when `systemd-sysupdate enable-feature --feature-suggested` runs, which is +# what systemd-sysupdate-auto-enable.service does. Run it ahead of every +# update: a tag that arrives with this run's confext is picked up by the next. +Wants=systemd-sysupdate-auto-enable.service +After=systemd-sysupdate-auto-enable.service # For containers we assume that the manager will handle updates. And we likely # can't even access our backing block device anyway. diff --git a/docs/getting-started.md b/docs/getting-started.md index a6e658d..7987be5 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -392,8 +392,8 @@ directly, but for those who are interested: local HTTP server as a source in transfer files. * `quarry sysupdate update` wraps `systemd-sysupdate` and does some pre-update - and post-update steps (such as installing tags and instantiating transfer - files from the TUF repository). + and post-update steps (such as instantiating transfer files from the TUF + repository). In most cases, you should avoid interacting with these tools -- these are intended to be used by a `systemd-sysupdate`-managed operating system directly. From 27adea8497f8e5b2dd4b99cca0ac0c47ae12e470 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Fri, 23 Oct 2026 13:32:05 +0200 Subject: [PATCH 2/2] sysupdate: run a second update pass when the refresh changed the tags The machine tags from a confext only land in /etc/machine-info during the `systemd-confext refresh`, i.e. after the update already ran. So features gated on those tags (like docker) were only picked up by the next timer run. Now we compare the tags before and after the refresh and if they changed we auto-enable and update once more. Note that not a full loop, two passes are enough for our tags use-case. Later if we would want to use systext to ship new transfer files we still need the --loop= from systemd v263+ or a manual loop here. --- cmd/quarry-sysupdate/update.go | 51 +++++++++++++++---------- cmd/quarry-sysupdate/utils_sysupdate.go | 34 +++++++++++++++++ 2 files changed, 65 insertions(+), 20 deletions(-) diff --git a/cmd/quarry-sysupdate/update.go b/cmd/quarry-sysupdate/update.go index b5d9a24..994fb29 100644 --- a/cmd/quarry-sysupdate/update.go +++ b/cmd/quarry-sysupdate/update.go @@ -5,12 +5,13 @@ package sysupdate import ( "context" - "errors" "fmt" "log/slog" + "slices" "github.com/urfave/cli/v3" + "go.amutable.dev/quarry/internal/hostnamed" "go.amutable.dev/quarry/internal/third_party/assert" "go.amutable.dev/quarry/internal/tufclient" "go.amutable.dev/quarry/internal/xsysupdate" @@ -98,37 +99,47 @@ var updateCommand = withExtensionDirFlag(withQuarryUserFlag(withQuarryProxyFlag( // the extension application, clean up the successfully installed // components, and force-downgrade to the old versions. - components, err := listComponents(ctx) - if err != nil { - return fmt.Errorf("get list of sysupdate components: %w", err) - } - slog.Info("Spawning 'systemd-sysupdate update'...", - "components", components) - var errs []error - for _, component := range components { - if err := sysupdate(ctx, component); err != nil { - slog.Error("sysupdate for component failed.", - "err", err.Error(), "component", component) - // FIXME: If we hit an error, continue on because we don't have - // a decent revert story at the moment. - errs = append(errs, err) - } - } - if err := errors.Join(errs...); err != nil { - return fmt.Errorf("sysupdate failed: %w", err) + if err := sysupdateAll(ctx); err != nil { + return err } - slog.Info("'systemd-sysupdate update' completed.") if err := exts.Close(); err != nil { return fmt.Errorf("post-update extension cleanup: %w", err) } + // A merged confext may ship tags.d(5) files, which systemd applies as + // part of the refresh. Features gated on those tags were evaluated + // with the old tags above, so they need another update pass. + tagsBefore, err := hostnamed.CurrentTags(ctx, "") + if err != nil { + return fmt.Errorf("fetch machine tags before refresh: %w", err) + } + // Do online-reload of sysexts and confexts. slog.Info("Trigger online-reload of new components.") if err := sysupdateRefresh(ctx); err != nil { return fmt.Errorf("online-reload components: %w", err) } + tagsAfter, err := hostnamed.CurrentTags(ctx, "") + if err != nil { + return fmt.Errorf("fetch machine tags after refresh: %w", err) + } + if slices.Equal(tagsBefore, tagsAfter) { + return nil + } + slog.Info("Machine tags changed by refresh, running a second update pass.", + "before", tagsBefore, "after", tagsAfter) + if err := enableSuggested(ctx); err != nil { + return err + } + if err := sysupdateAll(ctx); err != nil { + return err + } + if err := sysupdateRefresh(ctx); err != nil { + return fmt.Errorf("online-reload components after second pass: %w", err) + } + return nil }, }))) diff --git a/cmd/quarry-sysupdate/utils_sysupdate.go b/cmd/quarry-sysupdate/utils_sysupdate.go index c8a93e3..6522cd1 100644 --- a/cmd/quarry-sysupdate/utils_sysupdate.go +++ b/cmd/quarry-sysupdate/utils_sysupdate.go @@ -53,6 +53,40 @@ func sysupdate(ctx context.Context, component string) error { return systemdcmd.Call(ctx, "sysupdate", args...) } +// sysupdateAll runs "systemd-sysupdate update" for every component. +func sysupdateAll(ctx context.Context) error { + components, err := listComponents(ctx) + if err != nil { + return fmt.Errorf("get list of sysupdate components: %w", err) + } + slog.Info("Spawning 'systemd-sysupdate update'...", + "components", components) + var errs []error + for _, component := range components { + if err := sysupdate(ctx, component); err != nil { + slog.Error("sysupdate for component failed.", + "err", err.Error(), "component", component) + // FIXME: If we hit an error, continue on because we don't have + // a decent revert story at the moment. + errs = append(errs, err) + } + } + if err := errors.Join(errs...); err != nil { + return fmt.Errorf("sysupdate failed: %w", err) + } + slog.Info("'systemd-sysupdate update' completed.") + return nil +} + +// enableSuggested enables the components and features whose SuggestOn* +// conditions now hold, e.g. after the machine tags changed. +func enableSuggested(ctx context.Context) error { + if err := systemdcmd.Call(ctx, "systemctl", "start", "systemd-sysupdate-auto-enable.service"); err != nil { + return fmt.Errorf("failed to trigger systemd-sysupdate-auto-enable service: %w", err) + } + return nil +} + func sysupdateRefresh(ctx context.Context) error { errs := []error{ // systemd-confext refresh