v0.42.0: agentic controller update — in-guest image swap + rollback (Phase 1)

New local-API POST /controller/swap (+ GET /controller/swap/status), withGuest-
scoped: the agent records the previous image, confirms the target is present,
rewrites /etc/felhom-controller-image, restarts felhom-controller-bootstrap.service,
verifies the new controller is healthy (docker inspect, <=90s), and ROLLS BACK to
the previous image if not. Single-flight per guest; strict image-ref validation;
crash-safety state file. GuestBinder.GuestExec is the pct-exec seam.
--selftest=controller-swap exercises it directly.

Tests: happy/rollback-on-unhealthy(+red-proof)/image-absent/no-healthcheck/
bad-image-400/single-flight-409.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtXesNa2LGbMmE4DNL6SE7
This commit is contained in:
2026-06-26 21:26:12 +02:00
parent 4725396c81
commit 6f14b66191
6 changed files with 672 additions and 6 deletions
+26
View File
@@ -3,6 +3,32 @@
All notable changes to **felhom-agent** are recorded here. Update on every code
change that gets pushed.
## v0.42.0 — agentic controller update: in-guest image swap + rollback (Phase 1) (2026-06-26)
The host agent now owns the in-guest controller image **swap** — the new-architecture replacement for
the controller's dead in-container `docker compose` self-update. The controller pre-pulls the target
image (shared docker socket, its own registry token) then asks the agent to swap; the agent — external
to the controller container, so it survives the controller being killed mid-swap — does the rest and
**rolls back** if the new controller doesn't come up healthy.
- **New local-API routes** (`internal/localapi/controllerswap.go`, token-scoped via `withGuest`):
- `POST /controller/swap {image}`**202** `{status:"swapping", previous_image, target_image}`, then
async: record previous (crash-safety state file `/var/lib/felhom-agent/controller-swap-<vmid>.json`)
→ confirm the target image is present in the guest (else abort, **no swap**) → write
`/etc/felhom-controller-image``systemctl restart felhom-controller-bootstrap.service` → poll the
new controller to **healthy** (`docker inspect`, ≤90s) → **roll back** to the previous image + restart
if it doesn't (the guest is never left without a controller). Single-flight per guest (409 if busy).
Image ref is strict-validated (`gitea.dooplex.hu/admin/felhom-controller:<semver>`) before any action.
- `GET /controller/swap/status``{state: swapping|done|failed, current, previous, target, error}`.
- **`GuestBinder.GuestExec`** (`internal/localapi/guestbind.go`): the one `pct exec` seam the swap
composes over (cat/inspect/write/restart), reusing the fenced root runner.
- **`--selftest=controller-swap -vmid -image <ref>`**: exercise the primitive directly (the target image
must already be pulled in the guest).
- Wired `ControllerSwap: guestBinder` into the local-API server (`cmd/felhom-agent/main.go`).
- Tests (`controllerswap_test.go`): happy swap, **rollback-on-unhealthy** (+ companion red-proof:
dropping the rollback leaves the guest on the bad image and fails the test), image-absent no-swap,
no-healthcheck-running, bad-image 400, single-flight 409.
## v0.41.0 — provisioned customer guests auto-start after a host reboot (`onboot:1`) (2026-06-24)
**F3 fix.** The provision back-half now sets **`onboot:1`** on the customer guest, so after a host