Files
egress-proxies-operator/docs/plans-executions/2026-09-16-1333-kvm2-provider.md
T
kacerrandClaude Fable 5.1 95768281be kvm2: config block, constructor registration, IDs, state mapping and classify
The kvm2 block reuses KVMConfig; validateKVM gains a block-name variant so
messages say kvm2. The package skeleton has a non-dialing New, strict
providerID parsing, the state table and a 34-row classify test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:38:48 +00:00

4.5 KiB

Execution: kvm2, a clean-room libvirt/KVM provider built from the provider contract

Plan: docs/plans/2026-09-16-1333-kvm2-provider.md

  • Step 0 — Plan file and branch
  • Step 1 — The spec, docs/kvm2-provider-spec.md
  • Step 2 — Config wiring, IDs, state mapping, classify
  • Step 3 — Pure builders: domain and volume XML, metadata, seed
  • Step 4 — Transport, seam, decorator, fakes
  • Step 5 — Image cache and pool-space precheck
  • Step 6 — The four contract methods
  • Step 7 — Checker and the CRD comment
  • Step 8 — Deployment files, Makefile, integration test
  • Step 9 — Documentation
  • Step 10 — Milestone review
  • Step 11 — Real-host validation, changelog, MR

Step 0 — Plan file and branch

main was fast-forwarded to the merge of PR #7 (0dc25a1), which carries the three provider documents, and feat/kvm2-provider was cut from it. The plan was committed alone as 854856c. The pre-existing local modification of .claude/settings.json stays uncommitted throughout.

Step 1 — The spec, docs/kvm2-provider-spec.md

2026-09-16 13:37. Written by the orchestrator, not a subagent, since the spec is the "specifics" a user hands to the implementer. All 24 entries answered; every Evidence line cites the v1 user guide, the 2026-09-04 execution log, a go-libvirt symbol, or the contract. Nothing was taken from internal/provider/kvm/.

Design choices that go beyond the user's profile answers, all recorded in the spec: an adoption guard on Create (foreign UID is Permanent), a pool-space precheck mapped to ErrQuotaExceeded, a dedicated connection for image downloads so a cancelled poll cannot cut one off, the Checker on fresh connections rather than the pool, SSH authentication and host-key failures classified Permanent (v1 retried them forever), SSH material re-read on every dial so rotation needs no restart, kvm2's own metadata namespace (no adoption of v1 domains), and the shared validateKVM refactored to take the block name so messages say kvm2..

Checks run on the spec:

grep -c '^\*\*Answer:\*\*$' docs/kvm2-provider-spec.md      # 0 blank answers
grep -n 'internal/provider/kvm/' docs/kvm2-provider-spec.md   # empty

plus the axis-code check against contract §4 headings and the path-existence loop from the docs-split log. All clean.

Worth noting: the spec's entry 21 lists the concrete test name for every contract §11 item, so the later steps' briefs can quote it rather than re-derive the mapping.

Step 2 — Config wiring, IDs, state mapping, classify

2026-09-16 16:40. First subagent step. The brief named the allowed files, the spec entries, the test names and the prohibition on internal/provider/kvm/; the agent reported nothing opened there, with one honest caveat: a golangci-lint run printed seven single source lines from v1 files among its findings, which it did not act on. The review greps (grep -rn 'provider/kvm"' internal/provider/kvm2/, the v1 identifier smell list) are clean.

What landed: ProviderConfig.KVM2 reusing KVMConfig, setBlocks, the allow-list and dispatch in Config.validate, validateKVM refactored into validateKVMBlock(prefix, block, c) with validateKVM kept as a wrapper so v1 messages and tests are untouched, "kvm2": kvm2.New in cmd/main.go (both type lists in one commit), and the package skeleton: kvm2.go (New never dials, warns on unreadable files, withDefaults, host, wrapErr, stubbed methods), id.go, state.go, errors.go with 34 classify rows.

Decisions the agent made that the spec left open, all accepted: New checks file existence only (parsing belongs to the dialer); withDefaults copies hosts and images; firstIPv4 also drops an address whose type tag and literal disagree; there is no explicit fs.ErrNotExist branch in classify because it falls to the Transient default anyway.

Commands as run by the agent and re-run by the orchestrator:

go vet ./... && go build ./... && go test -race -count=1 ./internal/provider/... ./cmd/...
go test -short ./...

All packages ok.

Worth noting: bin/golangci-lint panics inside staticcheck's IR builder on this machine (unexpected expr: *ast.KeyValueExpr on stdlib internal/poll) because the local toolchain is Go 1.27.0 while go.mod says 1.26 and the bundled staticcheck predates it; it reproduces on untouched packages, so lint is deferred to Step 10 with a toolchain note. The .claude/settings.json modification is still local and uncommitted.