feat(updates): add host updater engine - #836
Conversation
🔐 Codex Security Review
Review SummaryOverall Risk: MEDIUM Findings[MEDIUM] Required checksum assets are not produced by the release pipeline
[MEDIUM] Command deadlines can hang on inherited output pipes
NotesThe diff contains no mining-pool, wallet, worker-credential, protobuf, frontend, or database changes. Test execution was unavailable because the workspace is read-only. Generated by Codex Security Review | |
18ff63c to
23c53c3
Compare
23c53c3 to
08c30dc
Compare
08c30dc to
19dccd7
Compare
6dc6cf0 to
ec26881
Compare
ec26881 to
c6796c3
Compare
c6796c3 to
bcdf08a
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new (currently dormant) host updater daemon to the server codebase, including a Unix-socket HTTP API contract, socket security/ownership enforcement, and a crash-safe self-update handoff mechanism. This forms the foundation for later one-click upgrade wiring and packaging work in the stacked PRs.
Changes:
- Introduces
updaterapitransport-neutral operation/status types for the local updater protocol. - Adds a Unix-socket HTTP server with strict socket-directory trust validation and explicit socket mode/ownership.
- Adds self-update handoff marker logic and a new
fleet-updatercommand entrypoint with restart/rollback handling.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| server/internal/updaterapi/types.go | Defines the JSON operation model (phases, status, trigger, error response) shared across the local updater boundary. |
| server/internal/updater/server.go | Implements Unix-socket HTTP endpoints and socket-path trust/permission enforcement. |
| server/internal/updater/server_test.go | Tests socket binding/ownership readiness and stale/live socket handling behavior. |
| server/internal/updater/self_update_handoff.go | Implements durable self-update handoff markers and idempotent rollback/commit semantics. |
| server/internal/updater/self_update_handoff_test.go | Exercises crash windows and mismatch/symlink hardening for the self-update handoff. |
| server/internal/updater/manager.go | Core updater manager logic (state, locking, activation boundaries, validation, etc.). |
| server/internal/updater/manager_test.go | Extensive coverage for staging/activation, recovery, cleanup, and durability invariants. |
| server/cmd/fleet-updater/main.go | Adds the updater daemon binary entrypoint, signal handling, socket readiness commit, and exec-based self-restart. |
| server/cmd/fleet-updater/main_test.go | Tests argv rewriting for self-update handoff and startup-failure rollback behavior. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c5c8287b47
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d52cfb6426
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4b144a8260
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a7736eec63
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6a8b988c4e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Reviewable diff: +3633/-0 across 5 files (excludes generated, test, and story files).
Summary
Adds the dormant host updater engine that will power one-click upgrades: a single-flight daemon, durable operation state, and a narrow Unix-socket HTTP interface. Its trigger contract accepts optional caller-generated operation IDs, making same-ID retries idempotent while retaining target-only compatibility for older callers. It stages and preflights a release while Fleet remains online, then crosses a marker-protected activation boundary with bounded commands, crash recovery, and coordinated updater self-restart. The binary is not packaged, installed, or connected to Fleet API in this PR, so merging it exposes no operator-facing behavior.
Stack: The update-notification foundation #841 → #842 → #843 → #844 → #845 is merged. The one-click phase is #835 (merged) → #836 → #837 → #838 → #839 → #840. This is 2/6 and its diff is relative to
main; #835 supplies release metadata plus the preflight/activation contract. Fleet API bridging, packaging, installation, and client exposure remain in #837–#840.How it works
At startup, the daemon validates and canonicalizes every component of its state, install, executable, and socket trust boundaries before loading or binding anything. The install chain may contain root plus one consistent deployment-admin UID—the owner #839 has already verified controls the same rootful Docker daemon—while the updater executable remains root/daemon-controlled. It then takes a lifetime advisory lock, reconciles only activation swaps proven by a durable marker, and removes crash-abandoned artifacts whose exact names are derived from the operation ID. Operation logs are confined and retained within fixed count/byte bounds; the local API refuses live or ambiguous socket owners and explicitly sets the bound socket identity and mode. A trigger accepts only a stable or RC tag, binds an optional canonical operation ID to that target, deduplicates concurrent same-ID admission, and constructs artifact URLs from the fixed Proto Fleet GitHub Releases origin.
The manager downloads the archive and SHA-256 sidecar, verifies transfer integrity, safely extracts bounded regular-file content, retains the updater payload through a private file descriptor, and preserves deployment configuration including the operator-owned HA node identity. Commands have phase deadlines, whole-process-group cancellation, and bounded output. After preflight, the staged tree and its parent are synchronized under a cancellable activation-length preparation deadline while the operation is still in
Preflight; only then can the marker-backed, non-cancelable swap begin. Shutdown cancels pre-activation work and waits for terminal state before releasing the daemon lock.After #835's preflight succeeds with Fleet still online and the staged tree is durable, the manager atomically persists an activation marker and rotates
deploymenttodeployment.previousbefore installing the new tree. Pre-command failures are reconciled before terminal state is written; a restart restores the previous tree only when the marker proves the two-rename gap, never merely because an old backup exists. Success and failure become terminal only after that exact transition is durable; if the state write fails, memory retains the last nonterminal recovery context and blocks retriggers until startup reconciliation. Once Fleet is healthy, the protected updater candidate must execute--versionsuccessfully on the host and report the exact target tag. Before replacement, the manager hard-links the working executable into a retained rollback slot and durably writes a protected sibling handoff marker. The matching argv identifies the first candidate attempt; returned startup errors restore.previous, while a signal, kill, or power loss leaves the marker for the next supervisor start to reconcile before manager initialization. Secured-socket readiness durably removes the marker. Failedexec, missing destination entries, and restore/marker-clear crash windows are idempotently recoverable without consuming the backup; ordinary starts and post-ready failures cannot downgrade the updater.sequenceDiagram participant C as Local caller participant U as Host updater participant G as GitHub Releases participant R as run-fleet.sh U->>U: Acquire daemon lock U->>U: Reconcile marker-proven swap, then clean stale artifacts C->>U: POST /v1/upgrade with target tag U->>G: Download archive and SHA-256 sidecar U->>U: Verify, safely extract, and preserve configuration U->>R: --preflight-only with deadline alt Preflight fails or shutdown begins U->>U: Bounded cleanup and persisted failure else Preflight passes U->>U: Sync staged tree under cancellable deadline U->>U: Persist swap marker and activating state U->>U: Fsync and rotate deployment directories alt Pre-command swap fails or process stops U->>U: Reconcile only the marker-proven layout else Swap is durable U->>U: Persist recovery command and consume marker U->>R: --skip-build with activation deadline alt Fleet fails to become healthy U->>U: Keep forward deployment and recovery details else Fleet is healthy U->>U: Smoke-test updater and retain rollback binary U->>U: Persist handoff marker, replace updater, and persist success U->>U: Drain API, release lock, and exec with one-shot handoff alt Exec fails, startup returns, or restart finds pending marker U->>U: Restore previous updater and exit nonzero else Manager initializes and secured socket binds U->>U: Consume rollback eligibility end end end end C->>U: GET /v1/status U-->>C: Persisted operation statusAreas of the code involved
server/internal/updaterapiserver/internal/updater/manager.goserver/internal/updater/self_update_handoff.goserver/internal/updater/server.goserver/cmd/fleet-updaterserver/internal/updater/*_test.goKey technical decisions & trade-offs
deployment.previousis insufficient because forward migrations may already exist.run-fleet.shmay have applied forward-only migrations. A 45-minute deadline is the liveness bound, and failed starts keep the new deployment plus an actionable recovery command.ECONNREFUSEDon an existing socket.0660.deployment/updater/proto-fleet-updaterand publishes a.sha256sidecar for every architecture archive; feat(updates): bootstrap host updater installs #839 installs that payload and service. The production-bundle contract test belongs with that packaging workflow rather than this dormant engine.fsyncremains inherently non-preemptible. Command output is capped at 64 MiB, while confined retention keeps the current plus newest prior logs within eight files and 256 MiB.TimeoutStopSecabove the 45-minute activation bound (with margin or infinity) so service shutdown preserves this PR's graceful-wait contract.Testing & validation
DB_PASSWORD=fleet GOWORK=off go test ./...GOWORK=off go test ./cmd/fleet-updater ./internal/updaterGOWORK=off go test -race ./cmd/fleet-updater ./internal/updaterGOWORK=off go vet ./cmd/fleet-updater ./internal/updatergolangci-lint run -c .golangci.yamlamd64static build andgit diff --check