controllerswap: F1 verify hardening — reject crash-looping no-healthcheck image v0.47.0

controllerHealthy reads RestartCount (running&&rc>0 -> not ok) + signals needsDwell for no-healthcheck;
verify requires verifyDwell(=3) consecutive ok polls for a no-healthcheck image (real healthcheck
trusted immediately). Closes the F1 hole (alpine crash-loop passed the point-in-time check). Red-proof
+ dwell + real-image tests. No sudoers/orchestration change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pg8ANF97SEeKYSN5Jxw3qJ
This commit is contained in:
2026-06-29 22:45:41 +02:00
parent bb548e3c5a
commit 3844df7c23
8 changed files with 218 additions and 57 deletions
+20
View File
@@ -1,3 +1,23 @@
## v0.47.0 — controller-swap verify hardening: reject a crash-looping no-healthcheck image (F1) (2026-06-29)
Closes F1 from the no-mercy testrun: a controller image with **no HEALTHCHECK that crash-loops** could
land a single "Running" inspect poll → the swap marked it healthy → **no rollback** (alpine tagged as
the controller passed in ~4 s, then `Restarting (0)`). The real controller image has a healthcheck so
the live severity is low, but the rollback safety net had a hole.
- **`internal/localapi/controllerswap.go`:** `controllerHealthy` now also reads `{{.RestartCount}}` (a
4th `docker inspect -f` field) — `running && RestartCount>0` → not-ok (a process that has already
crash-restarted isn't stably up, regardless of healthcheck). It also signals `needsDwell` for the
no-healthcheck (`none`) case. `verify` adds a **stability dwell**: a no-healthcheck image must report
ok on `verifyDwell` (=3) **consecutive** polls before it's accepted; a real `healthy` result is
trusted immediately (Docker already gated it). Any not-ok resets the dwell. Timeout → existing
rollback path runs. No change to writeImage, the sudoers grants (the `*` in `docker inspect -f *`
spans the extended template — confirmed live), or the state-file/rollback orchestration.
- Tests: F1 **red-proof** (`RestartCount>0` → verify false; companion: rc=0+dwell=1 verifies → the rc
check is what blocks it); the **dwell** (single ok then crash → verify false; companion dwell=1
accepts it); a real `healthy` image verifies promptly (no false rollback). Existing
`RollbackOnUnhealthy` / `HealthyWithNoHealthcheck` stay green. Version `0.46.0 → 0.47.0`.
## v0.46.0 — leaf lifecycle: signal + loud-log a regenerated leaf (prevention, Part B.1) (2026-06-29)
Makes an accidental local-API leaf **regeneration** (the 2026-06-28 root→non-root migration class —