Skip to content

feat(settings): apply releases and manage versions from the console - #564

Open
dviejokfs wants to merge 2 commits into
mainfrom
feat/console-self-update
Open

feat(settings): apply releases and manage versions from the console#564
dviejokfs wants to merge 2 commits into
mainfrom
feat/console-self-update

Conversation

@dviejokfs

Copy link
Copy Markdown
Contributor

What

The console already told operators a newer release existed, then left them to SSH in. This adds the missing action to that banner — an Update now button — plus a Settings → Version page for what's running, which channel it tracks, and an explicit update check.

How it works

The download / checksum-verify / atomic-swap machinery is reused from the existing temps upgrade. The genuinely new problems were deciding whether the process will come back, and reporting the result of an operation that kills the process that started it.

Install and restart are separate

Supervisor restart_mode What a click does
systemd / launchd automatic Installs, exits; the supervisor brings temps back on the new binary
none / container manual Installs; temps keeps serving the old binary until you restart it

A missing supervisor is deliberately not a blocker. Installing was always possible there — only the restart wasn't — so refusing helped nobody. Containers behave the same way, with a caveat that recreating the container reverts to the image's binary.

Reporting an outcome across a restart

The attempt is journalled to <data_dir>/self-update.json before the exit and resolved on the next boot by comparing the running version against the target. So the console says "updated to X" or "came back on the old version", never an indefinite spinner. installed_pending_restart stays put rather than degrading to failed when the old binary boots again — a restart you haven't done yet is not a failed update.

Safety

  • New binary must answer --version before it's moved into place (catches wrong libc, corrupt extraction).
  • Replaced binary kept as a .bak sibling for rollback, path surfaced in the UI.
  • A missing published checksum fails closed here, unlike the interactive CLI where a human can judge.

Turning it off

Two independent switches:

  • temps serve --disable-self-update — bootstrap flag, cannot be cleared over the API.
  • self_update.enabled — runtime toggle in Settings → Version.

The flag wins, and the UI says so rather than showing a toggle that does nothing.

Permission

Gated on a new platform:update permission, not settings:write. Replacing the running binary and dropping in-flight requests is a different class of action from editing a config value, so custom roles scoped to settings must not inherit it. Granted to Admin and PlatformAdmin by default. Every attempt is audited as PLATFORM_UPDATE_STARTED before the process exits.

Release channels

self_update.channel pins stable / beta / nightly; unset keeps the existing behaviour of inferring from the running version tag. The notifier re-reads it each pass, so switching takes effect without a restart. A check that finds nothing newer now clears the shared notice — otherwise moving a nightly box onto stable would leave the stale nightly banner up forever, since stable is older and could never overwrite it.

API

Endpoint Purpose
GET /settings/update Capability: can it apply, why not, restart mode, version, channel, last attempt
POST /settings/update Install a release (optional version pin)
POST /settings/update/check Query the release API now

CLI parity: temps platform update status | apply | check | channel [name\|auto].

Evidence

Run against a live server on a dev slot. All four capability states:

$ curl -s .../api/settings/update            # no supervisor, enabled
can_apply: True   blocker: None   supervisor: none   restart_mode: manual

$ # after --disable-self-update, setting left ENABLED
can_apply: False  blocker: disabled_by_flag
"The server was started with --disable-self-update. Restart it without that flag
 to allow updates from the console; this cannot be re-enabled from the UI."
$ curl -X POST .../api/settings/update  ->  HTTP 409

$ # after self_update.enabled = false
blocker: disabled_by_setting

Permission gating — a custom key holding only settings:read:

GET  /settings/update  ->  allowed: false   (server-side can_apply reported separately)
POST /settings/update  ->  HTTP 403  "This operation requires the platform:update permission"

Real end-to-end install, automatic mode (simulated systemd) — downloaded a real 93.5 MB release:

INFO Downloading release asset to=v0.1.0-beta.55 size_mb="93.5"
INFO Previous binary kept at .../target/fast/temps.bak — restore it with `mv ...`
WARN Binary replaced — exiting so the supervisor restarts temps on the new version

Then restarting deliberately on the old binary produced an honest failure rather than a false success:

{ "status": "failed",
  "error": "Restarted on v0.1.0-beta.1 instead of v0.1.0-beta.55. The binary swap did
            not take effect ... The replaced binary was kept at .../temps.bak." }

Real end-to-end install, manual mode (no supervisor) — the case this PR unblocks:

POST -> 202 {"restart_mode":"manual","estimated_restart_secs":0,
             "message":"...temps keeps running the current version until you restart it."}
phase: verifying -> pending_restart
readyz 200                                     # server never went down
binary on disk:      v0.1.0-beta.55            # swapped
running process:     v0.1.0-nightly.20260806…  # unchanged, as promised
journal: "status": "installed_pending_restart"
UI: "Installed — restart temps to apply"

Channel switching via CLI:

$ temps platform update channel stable
✓ Now tracking the stable channel.
$ temps platform update check
✗ Could not check the stable channel: No stable releases found.   # honest — none published

$ temps platform update channel beta && temps platform update check
Channel: beta   Current: v0.1.0-nightly.20260806…   Latest: v0.1.0-beta.55
ℹ Nothing newer on this channel.        # correct: beta.55 is OLDER than the nightly

UI verified in-browser: banner + button, confirm dialog (enabled and blocked variants), the Version page, and the pending-restart result card.

Checks

  • 20 new updater tests + 4 contract tests; temps-core 303 passing, temps-auth 330 passing
  • cargo clippy --all-targets -- -D warnings clean on temps-core, temps-auth, temps-config, temps-cli
  • tsc --noEmit clean for web/ and no new errors in apps/temps-cli (5 pre-existing errors on main in providers/notifications/openapi-ts.config.ts are untouched)

Notes for review

  • Multi-node: this updates the control plane only. Worker nodes running temps agent are unaffected and still upgrade separately.
  • Split topology (ADR-017): updating the console leaves the sibling temps proxy on its old binary; the capability surfaces that as a caveat before the click.

The console already told operators a newer release existed and then left
them to SSH in. This adds the missing action to that banner: an "Update
now" button that downloads the release, verifies its published SHA-256,
swaps the binary and exits so the supervisor restarts temps on it.

The download/verify/swap machinery is reused from `temps upgrade`. What
is new is deciding honestly whether the process will actually come back,
and reporting the result of an operation that kills the process that
started it:

- Supervision is detected up front (systemd `INVOCATION_ID`, launchd
  `XPC_SERVICE_NAME`, container markers). Containers and unsupervised
  processes are refused with the reason and the command to run instead,
  rather than exiting into permanent downtime.
- The attempt is journalled to `<data_dir>/self-update.json` before the
  exit and resolved on the next boot by comparing the running version to
  the target, so the console can report "updated to X" or "came back on
  the old version" instead of a spinner.
- The new binary must answer `--version` before it is moved into place,
  and the replaced one is kept as a `.bak` sibling for rollback.
- A missing checksum fails closed here, unlike the interactive CLI where
  a human can judge.

Two independent off switches, since an operator may want to forbid this
entirely:

- `temps serve --disable-self-update` — bootstrap flag, cannot be
  cleared over the API.
- `self_update.enabled` in settings — runtime toggle in Settings →
  Platform. The flag wins.

Gated on a new `platform:update` permission rather than `settings:write`:
replacing the running binary and dropping in-flight requests is a
different class of action from editing a config value, so custom roles
scoped to settings must not inherit it. Every attempt is audited as
`PLATFORM_UPDATE_STARTED` before the process exits.

Adds `GET/POST /settings/update` with CLI parity as
`temps platform update status|apply`.
… restart

Three changes on top of the console update action.

**Installing no longer requires a supervisor.** A missing systemd/launchd was
treated as a blocker, so the one case where an operator most wants help — a
temps they start themselves — got a refusal. Installing the binary was always
possible there; only the restart wasn't. The two are now separate: every
permitted install proceeds, and `restart_mode` says up front whether temps
restarts itself (`automatic`) or keeps serving the old binary until the
operator restarts it (`manual`). Containers move the same way, from refusal to
an install plus a caveat that recreating the container reverts to the image.

The journal gained `installed_pending_restart` for that state. It resolves to
`succeeded` when the new version finally boots, and — importantly — stays put
rather than failing when the old one boots again, because a restart the
operator hasn't done yet is not a failed update.

**A Version page** under Settings → Version: the running version, the channel
and where it came from, the supervisor and what a restart will do, the binary
path, an explicit "Check for updates" that queries the release API instead of
waiting for the periodic pass, and the outcome of the last attempt. The
platform-updates toggle moves here from the Platform page rather than being
duplicated.

**Release channels are selectable.** `self_update.channel` pins stable, beta or
nightly; unset keeps the previous behaviour of inferring from the running
version tag. The background notifier re-reads it every pass, so switching takes
effect without a restart. A check that finds nothing newer now clears the
shared notice — otherwise moving a nightly box onto stable would leave the old
nightly banner up forever, since stable is older and could never overwrite it.

Adds `POST /settings/update/check`, with CLI parity as
`temps platform update check` and `temps platform update channel [name|auto]`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant