Skip to content

Take the update target from the seed, not a systemd credential #407

Description

@onel

Size

S

Area

backend

Depends on

none (#404 shipped the reader this moves; this changes where it reads from)

Summary

#404 made the update target and window settable per box, delivered as two systemd credentials (ImportCredential=malmo.update_target_url / malmo.update_window on host-agent.service). On a real cloud VM they never arrive. A hosted box receives its per-box facts as user-data from the metadata service, not as a systemd credential, which is exactly why malmo-seed-materialize.sh exists (ENVIRONMENT.md # Provisioning & first-boot, the real-cloud seed channel). ImportCredential= reads systemd's credential store, and user-data does not feed it. So the #404 mechanism works in the QEMU lane and is inert on a provisioned box.

This issue was re-scoped after #404 was reviewed. Two things changed from the first plan:

  • Only the target URL moves into the seed, not the window. The seed arrives as user-data, and user-data is written once when the VM is created and can never be changed after that. So the seed is the right home for a fact that is fixed for the life of the box, and the wrong home for anything an operator needs to change later. "Which control plane does this box belong to" is fixed — a box never moves between control planes. "Which release, and in which window" is not.
  • The window and the per-box release move to the control plane's answer instead, which is what UPDATES.md # 8.1 specified all along ("the cloud holds a target version per box_id"). That is a separate issue, and this one does not wait for it.

Until that lands, the window keeps working as it does today, minus the credential: MALMO_UPDATE_WINDOW, then the built-in default.

Spec / source of truth

docs/specs/ENVIRONMENT.md # Provisioning & first-boot (how a box actually receives per-box facts, and why there are two channels), docs/specs/UPDATES.md # 8.4 (the per-box bullets #404 added, which describe the credential mechanism this replaces)

Do

  1. Add one optional field to profile.Seed (internal/profile/seed.go): update_target_url (string), omitempty so an appliance or un-steered seed round-trips unchanged. It is configuration, not identity — unlike box_id it is re-read on every boot, never frozen at first ingestion.
  2. Give host-agent its own seed read. It needs update_target_url and nothing else, so it must not go through profile.ReadSeed: that reader is written for the brain and hard-errors on a seed with no box_id or assertion_verification_key, which is correct for the brain and wrong here. Honour MALMO_SEED_PATH the way the brain does, so tests and the boot proof can point it somewhere.
  3. Decide the three seed states explicitly, and log each one:
    • absent — the appliance case, and a hosted box provisioned without a seed. Not an error. Fall through to the environment variable, then the default, silently.
    • present and readable — use update_target_url when it is there; fall through when it is not.
    • present and malformed — refuse and do not start the update loop, on hosted. A seed that will not parse might have carried a target, and we cannot tell. This is the same rule A provisioned box can be pointed at its own update target #404 already applies to an unusable URL, for the same reason: a box that was meant to be pinned must not quietly join stable.
  4. Change cmd/host-agent-real/updateconfig.go to read the URL from the seed. Precedence becomes seed > environment variable > compiled default, the same shape as today. The from value credential becomes seed (CLAUDE.md # Standard structured fields lists the allowed values — update it).
  5. Remove the credential path, do not keep both. Drop both ImportCredential= lines from dist/systemd/host-agent.service, the readCredential helper, credUpdateTargetURL, credUpdateWindow, credentialsDirEnv and fromCredential. Two mechanisms for one setting, one of which silently does nothing in production, is worse than either alone. windowSetting loses its credential branch and keeps env → default.
  6. Keep everything else from A provisioned box can be pointed at its own update target #404 as-is: an unusable target URL is refused and the update loop does not start (no fall back to the fleet endpoint), an unusable window warns and falls back, and the winning source is logged once at startup, loudly when it is not the default.
  7. Prove the seed channel in CI. The update boot in the cloud lane already delivers a seed over SMBIOS, but takes its target from an environment drop-in (dev/cloud/cloud-assertions.sh, the 30-update-target.conf block). Move update_target_url out of that drop-in and into that boot's seed JSON (dev/cloud/run-cloud-tests.sh, seed_cred_keyed), leaving the repository and window overrides on the environment. Without this, nothing anywhere tests the channel this issue exists to use.
  8. The cloud side must send the field for this to do anything on a real box. Coordinated separately; this side ships inert and safe.
  9. Documentation, in the same PR: ENVIRONMENT.md # Provisioning & first-boot (the seed's shape, replacing the "Per-box update credentials (realized, A provisioned box can be pointed at its own update target #404)" bullet and the credential sentence further down the file), UPDATES.md # 8.4 (replacing the A provisioned box can be pointed at its own update target #404 credential bullet), CLAUDE.md (the from values), plus a progress entry and the indexes. The A provisioned box can be pointed at its own update target #404 progress entry is a frozen snapshot — the new entry references it, it is not edited.
  10. Tests: the existing updateconfig_test.go cases carry over with the source swapped (field present / absent / unparseable, precedence against the environment variable, an absent field leaving the fleet default untouched). Add the seed-absent case, which is the appliance path and must not error, and the malformed-seed case, which must refuse.

Touch

internal/profile/seed.go, cmd/host-agent-real/updateconfig.go (+ its test), cmd/host-agent-real/updatetarget_hosted.go, dist/systemd/host-agent.service, dev/cloud/run-cloud-tests.sh, dev/cloud/cloud-assertions.sh, docs/specs/ENVIRONMENT.md, docs/specs/UPDATES.md, CLAUDE.md, docs/progress/

Done when

  • A box whose seed.json carries update_target_url reads that URL instead of the compiled default, observable in the journal at startup, and the cloud boot proof asserts it through the seed rather than an environment drop-in.
  • A box whose seed carries no such field, or no seed at all, behaves exactly as it does today.
  • An unparseable target URL, and a malformed seed, are both refused loudly, with the box not falling back to the fleet default.
  • No ImportCredential= for these two settings remains in the shipped unit, and no credential-reading code remains in cmd/host-agent-real/.
  • make check green; docs and a progress entry land in the same PR.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Next upacceptedTriaged and ready for implementationarea:backendbrain / Gosize:S<= half day

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions