feat(reconcile): re-assert pool membership after restore-over-existing (campaign-2 R2, v0.74.0)

Pool membership is what lets the pool-scoped token reach a guest; pct restore
--pool sets it only at CREATE, so a restore over an existing VMID drops the guest
from the felhom pool and 403s the next restore-test/DR on VM.Audit. This empty-pool
state is the true root cause of the campaign's "R1" (bind-mount restore failing was
a symptom — restore-test's existing bind neutralization never ran without config-read).

Add Client.PoolAddVMID (PUT /pools, additive+idempotent, Pool.Allocate) and call it
in bring-up after liveness when spec.Pool!="" — warn-not-fail on a hiccup (liveness
wins). B3 scratch-teardown 403 diagnosed as a cascade (restoretest already passes
Pool). Role/ACL untouched. Tests + red-proof.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-07 18:42:19 +02:00
parent e04b75e1f8
commit ca0b169a4e
8 changed files with 278 additions and 0 deletions
+26
View File
@@ -1,3 +1,29 @@
## v0.74.0 — pool membership re-asserted after restore-over-existing (campaign-2 R2) (2026-07-07)
Closes campaign-2 finding **R2** (`felhom.eu/documentation/tests/CAMPAIGN-2-2026-07-07.md`). Pool
membership is what lets the pool-scoped `FelhomAgentGuest` token reach a guest (the grant applies only
to pool MEMBERS). `pct restore --pool` sets membership at CREATE, but a restore **over an existing
VMID** (the host-loss/finale path) never re-applies it — and no code re-added a guest to the pool — so
every destroy-restore silently dropped membership, and the NEXT `--selftest=restore-test`/DR 403'd on
`VM.Audit`/`VM.Allocate`. That empty-pool state is the true root cause of the campaign's "R1" (the
bind-mount restore failure was a symptom: with no config-read, restore-test's *existing, correct*
bind-mount neutralization never ran).
- **`Client.PoolAddVMID(ctx, pool, vmid)`** (`internal/proxmox/mutate.go`): `PUT /pools/{pool}`
`vms={vmid}` — PVE-additive (merge, not replace), idempotent (already-member swallowed), needs
`Pool.Allocate` (the token has it). Sync — no UPID.
- **bring-up re-asserts** (`internal/reconcile/bringup.go`): after liveness is proven, if `spec.Pool
!= ""`, call `PoolAddVMID`. A pool-add hiccup is surfaced as a LOUD warning + `res.StartWarnings`
but must NOT flip a healthy running guest's verdict (membership matters for the NEXT op).
- **B3 (scratch teardown 403) — diagnosed, no code:** `restoretest.go` already passes
`Pool: DefaultPool` for scratch restores, so the campaign's `VM.Allocate` teardown 403 was a
CASCADE of the bind-mount restore failing (half-built guest outside any pool), not an independent
gap. A re-run on the healed pool confirms.
- Tests: `PoolAddVMID` PUT shape + idempotency + real-error-surfaces + validation (`pool_test.go`);
bring-up re-asserts when `Pool!=""` (red-proof: pre-fix no-call → FAIL, demonstrated), no-pool→no-call,
pool-add failure warns-but-passes (liveness wins, guest kept). Role/ACL untouched — the fault was
membership, not privileges.
## v0.73.0 — F2 mount-role fallback: enrolled user-data drives are ejectable/decommissionable again (2026-07-06)
Closes campaign finding **F2** (`felhom.eu/documentation/audits/CAMPAIGN-nomercy-2026-07-06.md` +