proxmox: review fixes, real-host validation record, changelog

Phase 10. Two review passes (go-operator-reviewer, /code-review high)
over the branch, recorded in docs/reviews/2026-09-23-proxmox-provider-review/:
9 findings fixed, 3 accepted as documented limits, 7 rejected with a
reason.

- Delete removes the seed between the stop and the destroy, so a failed
  seed delete leaves the VM for the next Delete instead of leaking the
  ISO; a Permanent refusal on the every-node path fails the call.
- Create skips a probe VMID whose listed name is someone else's without
  waiting on its lock.
- The three answers Phase 0 never captured are fixtures now: a missing
  storage is a 400 (the Checker reports StorageNotFound), a hibernated
  VM is destroyable (Delete destroys ours), a paused VM stops.
- TestProxmox_lifecycle and TestProxmoxCheck_realHost passed on pv1-1
  (PVE 9.2.20); the docs record the run and drop "not yet run".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-24 10:28:28 +00:00
co-authored by Claude Opus 5.5
parent 7f2073f94b
commit 3d72507894
28 changed files with 882 additions and 201 deletions
@@ -14,7 +14,7 @@ Pairs with [docs/plans/2026-09-18-1502-proxmox-provider.md](../plans/2026-09-18-
- [x] Phase 7 — Delete, Checker, CRD comment
- [x] Phase 8 — Contract and observability sweep, integration test, testing.md
- [x] Phase 9 — Deployment and documentation
- [ ] Phase 10 — Review, real-host validation, changelog, MR
- [x] Phase 10 — Review, real-host validation, changelog, MR
## Phase 0 — Plan file, branch, host recon, fixtures
@@ -1298,3 +1298,144 @@ Worth noting:
guide's "Not yet proven end to end" gap and the README's matching
clause come out once they pass. Then the CHANGELOG entry after the user
confirms, and the MR.
## Phase 10 — Review, real-host validation, changelog, MR
Where the plan's Phase 10 text and the spec disagreed, the spec won, as in
every phase since Phase 1. The host is pv1-1 at `https://192.168.0.8:8006`,
not 192.168.2.8, the template is 9001 and the ISO storage `proxy-seeds`.
**Review.** Two read-only passes over `git diff main...HEAD`: the
`go-operator-reviewer` agent, briefed with the spec and the contract
(P1 to P6, S1 to S3), then `/code-review high` on the branch (CR1 to
CR10). Of the 19 findings (two found by both passes), 9 were fixed, 3
accepted as documented limits and 7 rejected with a reason, all recorded in
[docs/reviews/2026-09-23-proxmox-provider-review/REPORT.md](../reviews/2026-09-23-proxmox-provider-review/REPORT.md).
The one that mattered was CR1/P1. Delete destroyed the VM and then deleted
the seed, so a failed seed delete left a seed nothing would ever remove:
the next `Get` is NotFound, the finalizer goes, and GC lists only VMs.
Delete now removes the seed between the stop and the destroy, so the VM
`Get` looks at goes last. A 403 on the every-node path now fails instead
of being skipped. P2 (CR3) was the other code change: a probe VMID whose
listed name is someone else's is skipped at once, instead of waiting up to
the clone budget on its lock on every retry.
**Lint.** `make lint` still panics inside staticcheck on Go 1.27, so, as
in Phases 8 and 9:
```bash
bin/golangci-lint run --disable staticcheck --max-same-issues 0 --max-issues-per-linter 0 ./internal/provider/proxmox/... ./internal/provider/
bin/golangci-lint run --build-tags integration --disable staticcheck --max-same-issues 0 --max-issues-per-linter 0 ./internal/provider/proxmox/... ./internal/provider/
```
Each run was diffed against the pre-fix baseline by file and message,
with line numbers and occurrence counts stripped. Three new goconst hits
came in along the way, and all three were fixed rather than accepted. The
P2 test's lock value made `"backup"`, then `"migrate"`, a repeated
literal; it now uses `"snapshot-delete"`. The fake's missing-storage error
repeated `"Parameter verification failed."`; it is now built from the raw
captured body. Final counts: goconst 224 (231 with the tag), logcheck 12,
modernize 2, unparam 3, gocyclo 1.
**Real host.** The user ran the host checklist on pv1-1 and then:
```bash
export PROXMOX_TEST_URL=https://192.168.0.8:8006
export PROXMOX_TEST_TOKEN_FILE=$HOME/.config/proxmox-token
export PROXMOX_TEST_CA_FILE=$HOME/.config/proxmox-ca.pem
export PROXMOX_TEST_NODE=pv1-1
export PROXMOX_TEST_TEMPLATE_VMID=9001
export PROXMOX_TEST_ISO_STORAGE=proxy-seeds
export PROXMOX_TEST_VLAN_TAG=4
make test-integration-proxmox
# --- PASS: TestProxmox_lifecycle (59.68s)
# --- PASS: TestProxmoxCheck_realHost (0.13s)
```
It ran the review fixes, uncommitted, on the first try. Clone task about
3 s, Create 6.2 s, address 50.2 s after Create returned, Delete plus Get
plus a second Delete 3.1 s. The Delete deleted the seed while the stopped
VM's `ide2` still referenced it (imgdel `OK`), then destroyed the VM, and
step 10 found neither left. That settles the one assumption behind the
reorder that the fake could not prove. The run confirmed P6 as well: the
first `Get`, about 6 s after the clone, met `status: unknown` in the
listing and answered Transient, blaming the node, which was up. The
message now names both causes.
**The three answers Phase 0 never captured.** Captured with a scratch
helper that passes the token to curl through a process substitution and
writes the Phase 0 fixture shape. Its first offline test caught a bug:
Python's text mode folds `\r\n`, so the header split found nothing, and
the file is now opened with `newline=""`. The calls were:
```bash
pvecap storage_status_missing GET /nodes/pv1-1/storage/proxy-missing/status
pve POST /nodes/pv1-1/qemu/9001/clone -d newid=199999 -d name=proxy-reconaaaaaaaaaaa -d full=1
pve POST /nodes/pv1-1/qemu/199999/status/start
pve POST /nodes/pv1-1/qemu/199999/status/suspend # pause
pvecap stop_paused_response POST /nodes/pv1-1/qemu/199999/status/stop
pvecap task_status_stop_paused GET /nodes/pv1-1/tasks/<upid>/status
pvecap status_current_after_stop_paused GET /nodes/pv1-1/qemu/199999/status/current
pve POST /nodes/pv1-1/qemu/199999/status/start
pve POST /nodes/pv1-1/qemu/199999/status/suspend -d todisk=1 # hibernate: stopped, lock suspended
pvecap err_destroy_hibernated DELETE '/nodes/pv1-1/qemu/199999?purge=1&destroy-unreferenced-disks=1'
pvecap task_status_destroy_hibernated GET /nodes/pv1-1/tasks/<upid>/status
```
Two of the three contradicted the code:
- A missing storage is a **400** with the field error
`storage: No such storage.`, not a 500, so the Checker reported
`APIError`. `isNoSuchStorage` matches that exact field error, and a 500
is still taken as missing (not captured). The matcher matrix caught a
first version that fired on any `storage` field error, which a clone
onto a storage without image support also has.
- The destroy **accepts a hibernated VM**. Its task ended `OK`, removing
both disks and the `vm-199999-state-suspend-…` volume. Delete refused
such a VM of ours as Permanent, believing the destroy would, so it now
destroys it like a stopped one. Create's adoption still refuses one it
finds before it was ever recorded. The capture was renamed
`destroy_hibernated_response.json`, because the client test globs
`err_*.json` as errors.
- A stop on a paused VM ends `OK` with the VM stopped, as assumed.
The destroy also cleaned up the scratch VM, and a listing of local-lvm
content for VMID 199999 came back empty.
**Kind pass.** The user built and loaded the image, deployed, applied a
scratch providers config with only `kubernetes` and `proxmox-home`, ran
the sample Proxy to Ready, ran a `curl -x` through it, deleted it and
checked pv1-1 for leftovers. Everything passed. The first attempt at the
override went to a ConfigMap named `providers-config`. `config/default`'s
`namePrefix` makes the mounted one `egress-proxies-operator-providers-config`,
so the pod kept the shipped config: no `proxmox-home` ProviderHealth, and
`kvm-home` and `kvm2-home` failing on the missing SSH key. The corrected
command replaced the prefixed ConfigMap and deleted the stray one. The
Secret keeps its plain name (`proxmox-token`), since kustomize renames
only resources it builds. `make deploy`'s image edit to
`config/manager/kustomization.yaml` was reverted, not committed.
Worth noting:
- **Behaviour changes from Phase 9.** Delete's order is now stop, seed,
destroy. A hibernated VM of ours is destroyed rather than latched. The
Checker reports a missing ISO storage as `StorageNotFound`. A
foreign-named VM on a probe VMID is skipped without a config read.
- **Accepted, not fixed** (guide Known gaps, spec entry 24):
- P3: Create adopts a leftover stopped VM whatever the current
`placement` says.
- P4: a retry can clone a second VM in a narrow crash window.
- **Still unproven:**
- A storage that exists but cannot be used on a node.
- Every cross-node path. The host has one node.
- **Flakes.** kvm's `TestKeepalive_keepsHealthyClientOpen` failed once
under `-race` ("unexpected packet in response to channel open") and
passed 5 of 5 on rerun. This branch does not touch `internal/provider/kvm`.
The parked kvm2 items (S5.1, the 200 ms `TestCheck_hostsCheckedConcurrently`)
did not bite, though the reviewer agent saw the latter fail once.
- **Docs.** The "not yet run" wording is gone from the README, the
guide, `testing.md`, spec entry 21 and `providers-existing.md`.
`testing.md` gained a timings table and the `PROXMOX_TEST_VLAN_TAG`
export line. The link sweep script from the Phase 9 entry found 0
broken links over the seven edited docs, and the guide has no `—`.
- `.claude/settings.json` stays out of the commit.