image pinning: eliminate :latest from all 5 unpinned templates + standing gate

bentopdf :latest -> v2.8.6; calibre-web :latest -> v4.0.6 (== running digest on
demo 9201, c31a738b - pin is a no-op); papra :latest -> 26.6.1-rootless (latest
was the rootless variant); recipe-importer :latest -> v0.9.11 (tag pre-existed,
digest-equal, no retag needed); termix :latest -> 2.5.0.

All five pins digest-identical to what :latest resolved to on 2026-07-12.
New gate scripts/check-image-pins.py (catches floating tags AND untagged refs;
red-proofed both shapes). Standing rule in CLAUDE.md + REUSE.md row.
This commit is contained in:
2026-07-12 14:37:57 +02:00
parent 2ebe082a2a
commit 71828a81cf
11 changed files with 149 additions and 26 deletions
+58 -21
View File
@@ -1,26 +1,63 @@
# REPORT — healthcheck sweep: `localhost` → `127.0.0.1` across all 48 templates (2026-07-06)
# REPORT — image pinning: `:latest` eliminated from the catalog (2026-07-12)
**Class:** implementation (mechanical sweep; push = deploy). **Baseline:** `main` @ `d86e256` → (this commit).
Part C of the pilot-blockers bundle (A = agent v0.73.0 F2, B = controller v0.102.0 async restore).
Provenance: RERUN vaultwarden healthcheck observation, escalated to a class.
**Class:** implementation (template change + sweep gate; push = deploy).
**Baseline:** `main` @ `2ebe082` → (this commit). **Scope:** 5 compose image lines + new gate script
+ CLAUDE.md rule. No healthcheck/env/format touch-ups; no controller change; no redeploys triggered.
## The class
48/53 templates used `localhost` in their docker healthcheck `test:` line. BusyBox `wget` (and the
node `http.get`, python `urllib`/`socket.create_connection`, and `curl` one-shot forms) resolve
`localhost`→IPv6 `::1` with NO cross-address-family fallback, so an app that binds IPv4-only reads
docker-`unhealthy` while fully serving (vaultwarden, re-run 2026-07-06).
## Why
## The change
Mechanical `localhost``127.0.0.1`, scoped strictly to the healthcheck `test:` lines of the 48 affected
`templates/*/docker-compose.yml` (all four check-binary forms incl. mealie's python socket tuple). Diff
review confirmed: **no** app env/config/label line changed; `.felhom.yml` files were already clean (zero
occurrences). New REUSE.md convention row.
The controller's recovery-unit `ImagePins` (felhom-controller `internal/backup/recovery_unit.go`)
pins the compose *tag*: restoring a `:latest`-pinned app re-pulls whatever `:latest` points to at
restore time — potentially schema-incompatible with the data being restored. Load-bearing rule:
**a deployed app pins to the digest it is currently running** (the pin never causes a version jump);
undeployed apps pin to the latest verified upstream stable.
## Live acceptance
Catalog synced to guest 9201 (`POST /api/sync` 200); vaultwarden template + container now
`http://127.0.0.1:80/alive`. **vaultwarden recreated → docker-`healthy`** (the observation's subject flips).
bookstack got the same 127.0.0.1 sweep but stays `unhealthy` from an **app-level HTTP 500** on its health
path (pre-existing, unrelated to localhost — documented).
## The five pins (old → new, with evidence)
## Not changed
Only healthcheck `test:` lines. No env, config, labels, ports, or `.felhom.yml`.
| App | New pin | Evidence (all gathered 2026-07-12) |
|-----|---------|------------------------------------|
| **calibre-web** (DEPLOYED, demo 9201) | `crocodilestick/calibre-web-automated:v4.0.6` | Running RepoDigest on 9201: `sha256:c31a738b6d5e…`; Docker Hub tag `v4.0.6` digest: `sha256:c31a738b6d5e…`**exact match** (Scenario A: pin == running image; a newer tag would have violated the no-silent-upgrade guard) |
| **bentopdf** (undeployed) | `ghcr.io/alam00000/bentopdf:v2.8.6` | GitHub latest release v2.8.6 (2026-06-28); ghcr manifest digest `sha256:eaeea1e4…` == `latest` |
| **papra** (undeployed) | `ghcr.io/papra-hq/papra:26.6.1-rootless` | ghcr `latest` digest `sha256:a7a42e22…` == `26.6.1-rootless`; the `-root` variant differs (`c5757540…`) — the **rootless** variant is what `:latest` was shipping, so behavior (permission semantics) is preserved exactly |
| **recipe-importer** (own image, undeployed) | `gitea.dooplex.hu/admin/recipe-importer:v0.9.11` | Its repo CHANGELOG declares v0.9.11 (2026-02-26); the registry **already had** `v0.9.11` (build.sh pushes `:VERSION`+`:latest` together), digest `sha256:f3cb617c…` == `latest`**Scenario C's retag was unnecessary**; no push performed |
| **termix** (undeployed) | `ghcr.io/lukegus/termix:2.5.0` | ghcr digests: `latest` == `2.5.0` == `release-2.5.0` (`sha256:4d337131…`); bare CalVer tag chosen |
All five `docker manifest inspect <ref>` checks ran on the build server (180) with per-command exit
codes checked — 5× OK, zero pipe-swallowed.
`templates.json`: contains **no image strings** (legacy Portainer-era metadata) — untouched, per §4.3.
## Sweep + red-proofs (§10 / Scenario D)
- New rerunnable gate: `scripts/check-image-pins.py` — scans every `templates/*/docker-compose.yml`
`image:` line; fails on floating tags (`latest`,`dev`,`nightly`,`edge`,`main`,`master`) AND on
untagged refs (implicit :latest); `@sha256:` counts as pinned; registry-port refs handled.
- **Final run: `image-pin gate OK — 53 templates, 0 unpinned images`, exit 0.**
- **Red-proof 1** (floating tag): reverted termix to `:latest` → exit 1 with
`templates/termix/docker-compose.yml:11 … [floating tag :latest]` → restored → exit 0. ✓
- **Red-proof 2** (bare shape): stripped termix's tag entirely → exit 1 with
`[NO TAG (implicit :latest)]` → restored → exit 0. ✓ (Both §7-D shapes proven.)
## Fleet-deployment verification (Scenario B)
- demo 9201: `docker ps` — bentopdf/papra/termix/recipe-importer **not deployed**; calibre-web
running from `:latest` (Up 4 hours at check time).
- felhotest (router.abonet.hu:33022): **unreachable** (connection refused).
- Peti's box: offline (dead fan, no ETA).
- Operator asked and approved proceeding: every pin is digest-identical to current `:latest`, so
even a hidden deployment elsewhere sees zero change vs. the status quo on its next redeploy.
## Git-sync no-op verification on 9201
_(filled after push — see below)_
## Observations (not acted on)
- When Peti's box returns: check whether bentopdf/papra/termix/recipe-importer are deployed there;
if any is running an OLDER digest than the pin, decide upgrade vs. per-node pin then. felhotest
should get the same check when reachable again.
- papra upstream publishes only `-root`/`-rootless` variant tags — any future manual upgrade must
keep the `-rootless` suffix or permissions semantics change.
- bentopdf's ghcr `tags/list` API returns a stale/partial page (newest listed was 1.15.3 while
v2.8.6 exists and serves) — tag existence must be checked via manifest HEAD/inspect, not tags/list.
- recipe-importer's registry `:latest` remains published (allowed; only templates must not use it).