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:
+33
-14
@@ -71,9 +71,9 @@ manual runs too.
|
||||
| `internal/provider/keyedlock` | `keyedlock_test.go` | the per-name lock kvm2 and proxmox share: the entry is removed once uncontended, waiters share one lock, a waiter gives up with its ctx | pure |
|
||||
| `internal/provider/proxmox` | `id_test.go`, `mark_test.go`, `state_test.go`, `seed_test.go`, `errors_test.go`, `api_test.go` | pure pieces: providerID round-trip and malformed forms, the VMID derived from the UID (deterministic, in range, probing forward and wrapping), the description mark (keys, parsed from the captured clone, admin lines kept), state mapping and the IPv4 filter from captures, seed names and building (empty name Permanent without its payload, staging failure Transient); the `classify` table and the 500-message matchers against every captured error; the API types decoded from the captures | pure, over the responses Phase 0 captured from PVE 9.2.20 (`testdata/`) |
|
||||
| | `client_test.go` | the thin HTTP client: the token read from its file on every request (missing Transient, unparsable Permanent), `apiError` from status, reason phrase and body, the data envelope, method, path and parameters of every seam call, the clone and destroy query shapes, the volid's slash escaped, HTTP/1.1 only, request timeout and cancel Transient and the client unaffected, a child span under a parent and none without, the token never in errors or logs, the CA file trusted and loaded lazily, an x509 failure Permanent, insecure mode, the upload streamed as multipart | `httptest.NewTLSServer` replaying the captures byte for byte |
|
||||
| | `create_test.go`, `get_test.go`, `delete_test.go`, `list_test.go`, `task_test.go`, `proxmox_test.go`, `provider_test.go`, `fake_test.go` | Create: resume after every partial step and after a crash mid-clone, the VMID probe and ownership check, the template's shape refused before any clone, resize as a task, the seed overwritten without a delete, the net0 bridge and VLAN rewrite keeping the MAC, returns only when running, cloud-init sentinel never logged; Get: lock checked before name, the agent's messages, paused from `status/current`, hibernated, a reused VMID, four calls when running; Delete: every already-gone and not-ours case, the seed deleted on the VM's node or on every node, hibernated Permanent, the per-name lock; ListByTag: candidates by tag or name prefix, the mark decides, an unreadable VM skipped unless ctx ended; task and lock waits; `New` makes no request; the contract sweep: every method returns within a second of a cancel (blocked on its first seam call, and mid-operation), every error wrapped with the configured name, op and id, logging tiers V0/V1/V2 with the no-op lines | in-memory `fakeAPI` behind the `api` seam, its error texts checked against the captures |
|
||||
| | `create_test.go`, `get_test.go`, `delete_test.go`, `list_test.go`, `task_test.go`, `proxmox_test.go`, `provider_test.go`, `fake_test.go` | Create: resume after every partial step and after a crash mid-clone, the VMID probe and ownership check, the template's shape refused before any clone, resize as a task, the seed overwritten without a delete, the net0 bridge and VLAN rewrite keeping the MAC, returns only when running, cloud-init sentinel never logged; Get: lock checked before name, the agent's messages, paused from `status/current`, hibernated, a reused VMID, four calls when running; Delete: every already-gone and not-ours case, the seed deleted before the destroy on the VM's node or on every node, a hibernated VM destroyed, the per-name lock; ListByTag: candidates by tag or name prefix, the mark decides, an unreadable VM skipped unless ctx ended; task and lock waits; `New` makes no request; the contract sweep: every method returns within a second of a cancel (blocked on its first seam call, and mid-operation), every error wrapped with the configured name, op and id, logging tiers V0/V1/V2 with the no-op lines | in-memory `fakeAPI` behind the `api` seam, its error texts checked against the captures |
|
||||
| | `observe_test.go`, `check_test.go`, `provider_test.go` | the decorator: every seam method logs exactly one line (guarded by reflection), starts no span, identifiers at V1, detail at V2, upload body never logged, error fields; a Get through `provider.WithTracing` over the real client makes four `HTTP GET` spans, all children of `provider.get` and none carrying the token; health check: one entry per node in config order, nodes checked concurrently (a barrier, not a timer), expired ctx returns promptly, the reasons table at V2 with a sentinel secret, the transport reasons from real `net/http` and `crypto/tls` errors, detail keys | `fakeAPI` + `funcr` capture + span recorder; the real client over `httptest` for the tracing and transport-reason rows |
|
||||
| | `proxmox_integration_test.go` (`-tags=integration`) | one real VM: `New` without a request, `Check` Ready with details, Create, idempotent re-Create with an unchanged VM count, Running with an IPv4 and the mark read back, ListByTag, Delete, NotFound, no VM and no seed left, HTTP spans under `provider.create`; `TestProxmoxCheck_realHost` runs the health check against the host and creates nothing. **Compiles, not yet run against a host** | a **real Proxmox VE host** — see below |
|
||||
| | `proxmox_integration_test.go` (`-tags=integration`) | one real VM: `New` without a request, `Check` Ready with details, Create, idempotent re-Create with an unchanged VM count, Running with an IPv4 and the mark read back, ListByTag, Delete, NotFound, no VM and no seed left, HTTP spans under `provider.create`; `TestProxmoxCheck_realHost` runs the health check against the host and creates nothing. **Passed on pv1-1 (PVE 9.2.20), 2026-09-24**: Create 6.2 s, address 50 s later, whole run 61 s | a **real Proxmox VE host** — see below |
|
||||
| `internal/health` | `probe_test.go` | probes through a **real CONNECT-capable proxy stub** to a real TLS server: tunnel success, refused CONNECT, wrong status, dead proxy, plain-http forward | `httptest` + hijacked bidirectional tunnel |
|
||||
| | `engine_test.go` | thresholds, first-verdict, latency suppression matrix, dropped-event retry, stale-probe UID guard, tick scheduling/seeding/pruning, `Start` end-to-end | fake `client.Reader` + injected `probeFn` |
|
||||
| `internal/lease` | `store_test.go` | capacity, `MaxLeases=0`, all three selection tie-breaks, target-scoped vs global cooldowns, expiry via fake clock, report-on-retained-lease, idempotent release, **40 concurrent acquires never exceeding capacity** | injectable clock; the concurrency case is why `-race` matters |
|
||||
@@ -117,19 +117,21 @@ envtest suite the scaffold generated.
|
||||
`httptest` replaying responses captured from PVE 9.2.20. Whether a
|
||||
clone, config write, seed upload and start yield a guest that boots and
|
||||
reports an address is provable only on a real host (the proxmox
|
||||
integration test below). Two kinds of path have met only the fake:
|
||||
integration test below, which has passed on pv1-1). Two kinds of path
|
||||
have met only the fake:
|
||||
- **the cross-node paths**: a clone that targets another node, the seed
|
||||
deleted on every node when the VM's node is unknown, a Check round over
|
||||
several nodes. The test host is a single node, so the integration test
|
||||
cannot reach them either
|
||||
([proxmox-provider-spec.md](proxmox-provider-spec.md) entry 24);
|
||||
- **answers never captured**: what the storage status call returns for a
|
||||
missing or disabled storage (every 500 there is taken as
|
||||
`StorageNotFound`), what destroy says for a hibernated VM, and a stop
|
||||
on a paused VM. The provider does not depend on their wording: any
|
||||
500 from the storage status is `StorageNotFound`, a hibernated VM is
|
||||
refused before destroy is called, and a paused VM gets the same stop
|
||||
as a running one. What the fake answers there is its own guess.
|
||||
- **an answer never captured**: the storage status for a storage the
|
||||
cluster knows but cannot use on a node (disabled there, or offline).
|
||||
Every 500 there is taken as `StorageNotFound`, and what the fake
|
||||
answers is its own guess. The three answers Phase 0 missed were
|
||||
captured on pv1-1 on 2026-09-24: a missing storage is a 400 with
|
||||
`storage: No such storage.`, a hibernated VM is destroyed without
|
||||
complaint (its suspend state volume with it), and a stop on a paused
|
||||
VM ends `OK` with the VM stopped.
|
||||
|
||||
## The KVM integration test (real hypervisor)
|
||||
|
||||
@@ -303,6 +305,7 @@ export PROXMOX_TEST_CA_FILE=$HOME/.config/proxmox-ca.pem # the node's pve-roo
|
||||
export PROXMOX_TEST_NODE=pv1-1
|
||||
export PROXMOX_TEST_TEMPLATE_VMID=9001
|
||||
export PROXMOX_TEST_ISO_STORAGE=proxy-seeds # holds iso content
|
||||
export PROXMOX_TEST_VLAN_TAG=4 # template 9001's net0 is untagged; proxies live on VLAN 4
|
||||
make test-integration-proxmox
|
||||
```
|
||||
|
||||
@@ -381,10 +384,26 @@ variables; to run only this test:
|
||||
go test -tags=integration -count=1 -v -run TestProxmoxCheck ./internal/provider/proxmox/
|
||||
```
|
||||
|
||||
**Neither test has run against a host yet.** Both compile
|
||||
(`go vet -tags=integration ./...`); Phase 10 of the plan runs them on
|
||||
pv1-1. Like the kvm tests they are not part of `make test` and not run in
|
||||
CI.
|
||||
**Last run: 2026-09-24 on pv1-1** (Proxmox VE 9.2.20, kernel
|
||||
7.0.14-17-pve, template 9001, `proxy-seeds`, VLAN tag 4). Both tests
|
||||
passed; `go test` reported 60.9 s for the package.
|
||||
|
||||
| Step | Time |
|
||||
|---|---|
|
||||
| `Check` | 141 ms (125 ms in `TestProxmoxCheck_realHost`) |
|
||||
| clone task, submit to done | about 3 s (seen done at the third 1 s poll) |
|
||||
| `Create`, clone to started VM | 6.2 s |
|
||||
| second `Create` (adoption, nothing created) | 32 ms |
|
||||
| Create's return to the first address | 50.2 s (the agent install from the internet) |
|
||||
| `Delete` (stop, seed delete, destroy), `Get`, second `Delete` | 3.1 s |
|
||||
| `TestProxmox_lifecycle` | 59.7 s |
|
||||
|
||||
The first `Get`, 32 ms after the second Create, was Transient: the
|
||||
listing still showed the new VM with `status: unknown`. The next poll 5 s
|
||||
later saw it running. The Delete deleted the seed while the stopped VM's
|
||||
`ide2` still referenced it, then destroyed the VM, and step 10 found
|
||||
neither left. Like the kvm tests they are not part of `make test` and not
|
||||
run in CI.
|
||||
|
||||
## The scaffolded `make test-e2e` suite
|
||||
|
||||
|
||||
Reference in New Issue
Block a user