diff --git a/cmd/quarry-sysupdate/update.go b/cmd/quarry-sysupdate/update.go index 983721b..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" @@ -24,14 +25,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 { @@ -106,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 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.