You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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.
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.
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.
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).
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.
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.
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.
The cloud side must send the field for this to do anything on a real box. Coordinated separately; this side ships inert and safe.
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.
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.
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_windowonhost-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 whymalmo-seed-materialize.shexists (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:
UPDATES.md# 8.1 specified all along ("the cloud holds a target version perbox_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
profile.Seed(internal/profile/seed.go):update_target_url(string),omitemptyso an appliance or un-steered seed round-trips unchanged. It is configuration, not identity — unlikebox_idit is re-read on every boot, never frozen at first ingestion.update_target_urland nothing else, so it must not go throughprofile.ReadSeed: that reader is written for the brain and hard-errors on a seed with nobox_idorassertion_verification_key, which is correct for the brain and wrong here. HonourMALMO_SEED_PATHthe way the brain does, so tests and the boot proof can point it somewhere.update_target_urlwhen it is there; fall through when it is not.stable.cmd/host-agent-real/updateconfig.goto read the URL from the seed. Precedence becomes seed > environment variable > compiled default, the same shape as today. Thefromvaluecredentialbecomesseed(CLAUDE.md# Standard structured fields lists the allowed values — update it).ImportCredential=lines fromdist/systemd/host-agent.service, thereadCredentialhelper,credUpdateTargetURL,credUpdateWindow,credentialsDirEnvandfromCredential. Two mechanisms for one setting, one of which silently does nothing in production, is worse than either alone.windowSettingloses its credential branch and keeps env → default.updateboot in the cloud lane already delivers a seed over SMBIOS, but takes its target from an environment drop-in (dev/cloud/cloud-assertions.sh, the30-update-target.confblock). Moveupdate_target_urlout 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.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(thefromvalues), 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.updateconfig_test.gocases 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
seed.jsoncarriesupdate_target_urlreads 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.ImportCredential=for these two settings remains in the shipped unit, and no credential-reading code remains incmd/host-agent-real/.make checkgreen; docs and a progress entry land in the same PR.