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>
16 KiB
Changelog
2026-09-24 10:28 UTC — proxmox provider: reviewed, fixed, proven on a real Proxmox VE 9.2 host
The proxmox provider (VMs cloned from a template on Proxmox VE through its REST API)
finished its last phase: a two-pass review, a real-host run on pv1-1, and captures of the
three API answers the design had only guessed.
- Delete now removes the seed ISO before destroying the VM. Before, a seed delete that
failed after the destroy was never retried (the next Get is NotFound), so the ISO leaked.
A 403 for
Datastore.Allocatenow fails Delete on every path instead of being skipped. - Create skips a probe VMID held by someone else's VM at once, instead of waiting up to 10 minutes on that VM's lock on every retry.
- The captures corrected two assumptions: a missing ISO storage is a 400 (the health check
now says
StorageNotFound, notAPIError), and a hibernated VM can be destroyed (Delete destroys ours instead of latching the Proxy). TestProxmox_lifecycleandTestProxmoxCheck_realHostpassed on PVE 9.2.20: Create 6.2 s, an address 50 s later. A kind pass took a sample Proxy to Ready and back.- Review record:
docs/reviews/2026-09-23-proxmox-provider-review/REPORT.md. Timings:docs/testing.md. Code:internal/provider/proxmox/{delete,create,check,errors}.go.
2026-09-18 07:52 UTC — docs/operating.md, starting with the libvirt-RPC log query
New home for day-to-day helpers against a running operator, seeded with the jq query
that pulls timestamp + rpc out of the manager's JSON logs (both libvirt providers log the
same "libvirt call" msg, so one filter covers kvm and kvm2), plus the fromjson?
variant for streams carrying non-JSON lines. Linked from the README's doc index.
2026-09-18 07:44 UTC — a kvm2 crawl-pool sample
Until now the only kvm2 manifest was the one-VM config/samples/proxy_kvm2.yaml, which
left the multi-proxy shape — the one a crawler actually leases from — undocumented.
- Added
config/samples/proxy_kvm2_crawl_pool.yaml: five Managed Proxies on thekvm2-homeentry, four sharing one labelled cloud-init Secret and one inline (it runs squid on 8080, so it cannot share a Secret that pinshttp_port 3128). Covers thetierattribute the discovery API selects on, amedium/ubuntu-24.04member with a raisedmaxLeases, and a member with a tightenedhealthCheck. - Kept out of
config/samples/kustomization.yamlon purpose soapply -kdoesn't boot a whole pool; the exclusion is commented there next to the existingproviders-config.yamlone. Validated withkubectl apply --dry-run=server.
2026-09-17 20:36 UTC — kvm2: the four high findings from the code review, and its first run on a real hypervisor
A scoped review of the kvm2 provider (review/REPORT.md: 52 findings, 4 high, 20 medium,
28 low) produced four fixes, one commit each, then the provider was deployed in-cluster and
validated against beelink — the first time any of this code had run against real libvirt.
- H1 — a stalled write no longer delays a connection teardown.
sshSocket.Closeran the blocking close before the unblocking one, so a transfer stuck in a TCP write pinned the teardown for as long as the kernel retransmitted (~15 min).link.markDeadcompounded it by killing inside async.Oncebefore signalling dead, which made even cancelling the reconcile useless. WithMaxConcurrentReconciles: 3, one silent host could stall every Proxy on every provider.internal/provider/kvm2/{ssh,backend}.go. - H2 — a source error no longer reaches go-libvirt's stream sender. A failing HTTP body
deadlocked
StorageVolUploadoutright and leaked two goroutines plus 4 MiB per occurrence, against a 128Mi pod limit. The defect is upstream (requestStreamnever drains an unbufferedoutErr); the workaround wraps the reader so it can only ever end the stream cleanly, and moves the transfer deadline onto the HTTP source so a stalled mirror unwinds instead of leaking. A daemon-side failure mid-upload still leaks — documented in Known gaps with the two-character upstream patch.internal/provider/kvm2/{backend,image}.go. - H3 — an unreadable address is no longer reported as a missing one. Any error from both
address sources read as "no address yet", so a pooled connection killed by an unrelated
reconcile demoted a healthy proxy to Provisioning, cleared its IP and made the health engine
drop its counters. One source failing stays "no address yet"; every source failing, or a dead
connection, is now Transient.
internal/provider/kvm2/get.go. - H4 — the base image cache is no longer shared between provider entries. kvm2 built
byte-identical volume names to the
kvmprovider in the same pool, so two entries could share one file, a concurrent fetch could boot a guest from a half-written base, and asha256edit deleted the base under live overlays. Names now carry the entry (base-<entry>-<key>.<format>), and a digest mismatch is refused rather than resolved by deleting.internal/provider/kvm2/{volume,image}.go,internal/provider/config_kvm.go. - klog and
net/httperrors now join the structured log stream. client-go's leader election wroteI0917 ...lines straight to stderr because nothing routed klog into logr, and the discovery server'shttp.Serverhad noErrorLog. The deployment also gained--zap-encoder=jsonand--zap-time-encoding=rfc3339, sokubectl logs | jqworks.cmd/main.go,internal/discovery/server.go,config/manager/manager.yaml. - The
kvm2-homeprovider entry is enabled inconfig/manager/providers_config.yaml. Validated end to end: health check Ready, a 433 MB image cached in 8.3 s under the new name beside kvm's untouched one, a second Proxy reusing the cache, Squid answering a real CONNECT probe, and Delete leaving the base image while removing the domain and its volumes.
2026-09-16 11:46 UTC — Provider blueprint split into contract, spec template and appendix
docs/provider-contract.mdis the generic provider specification: MUST/SHOULD/MAY rules with the caller behind each one, 19 behaviour-profile axes with stable codes (where providers may differ, such as blocking vs submit-and-return Create, or a backend with no provisioning state), error taxonomy, credential hygiene, testing obligations, the adding-a-provider checklist and the contract's known limits. It names no existing provider.docs/provider-spec-template.mdis the fill-in questionnaire a new provider's design answers before implementation; entry numbers match the contract's profile table.docs/providers-existing.mdmaps kubernetes, gcp and kvm to every profile and keeps their per-provider reference notes, shared gaps and proposed features.docs/providers.mdis now an index; README, CLAUDE.md and architecture.md point at the new files. This supersedes the single-file blueprint written earlier on the same branch.- Two rules the callers enforce that were not written down before: orphan GC sweeps
providers sequentially with no deadline, so a hung
ListByTagstalls all collection; and the reconciler deletes an instance a backend reports as Stopped before start.
2026-09-16 08:45 UTC — Provider health checks: ProviderHealth CRD, per-provider checks, gauges
- Each configured provider is now checked on a schedule and reported in a
cluster-scoped
ProviderHealthobject (kubectl get ph), one per provider, with aReadycondition, a reason, and per-target verdicts. - Checks: kubernetes lists one object from the API server; GCP fetches a single
instance page; KVM checks SSH/libvirt reachability and the storage pool on every
host, in parallel. Failures map to fixed reasons (
Unreachable,SSHHandshakeFailed,PoolNotFound,Timeout, …). - The KVM check can't hurt reconciles: it uses
hostPool.probe, which gives up on a slow RPC without dropping the shared connection, and waiting for a host slot can be interrupted. - Metrics:
proxy_operator_provider_target_up,_check_duration_secondsand_check_timestamp_seconds. The flags are--provider-check-intervaland--provider-check-timeout(defaults tomin(15s, interval)). - Validated with
make test,make test-e2eand the real-hypervisor KVM run against beelink.make lintis blocked by a Go 1.27 toolchain/staticcheck panic that has nothing to do with this change. - Key files:
api/v1alpha1/providerhealth_types.go,internal/provider/check.go,internal/provider/kvm/,internal/providerhealth/,docs/architecture.md.
2026-09-10 22:58 UTC — Code-review fixes on the KVM branch: deletion, validation, lazy SSH, guest agent
Five findings from a review of feat/kvm-provider, all fixed on the branch.
- A Proxy can always finish deleting. The delete path used to drop the finalizer
only when
Getanswered not-found, so a permanent error there retried forever — a KVM host removed from the config was enough. It now callsDeleteanyway, since that is idempotent and can finish whereGetcannot, andcrawl.example.com/skip-cleanup: "true"abandons the instance for whatever stays stuck. In the KVM provider, deleting over an unconfigured host is a no-op success;Getstill fails permanently there so a typo cannot look like a missing VM. - The guest agent ships as a vendor-data script, superseding the cloud-config
merge in the entry below. Merging could never be made safe: cloud-init drops
vendor-data keys the user-data defines, and the strategy cannot be overridden from
either document. The Proxy's cloud-init now reaches the seed byte for byte in any
format — Jinja, MIME, gzip, scripts — where the round-trip previously re-typed
unquoted scalars (
version: 1.10arrived as1.1), dropped comments and reordered keys. NewinstallGuestAgentsetting, default on. - A missing
kvm-sshSecret no longer crash-loops the manager. The SSH material is read on first use and cached, so only KVM Proxies fail and they recover when the Secret appears. A missing file is transient, an unparseable key permanent, and startup still logs a warning. - Host names and image keys are validated. Either can end up in a providerID or a volume file name, where a slash made the VM unaddressable for good.
- Verified end to end: unit suite, the integration test against a real hypervisor
(22 s, agent reporting the IP), both in-cluster Proxies Ready in 2 min with 204
through squid, the guest showing
qemu-guest-agentactive with its cloud-config comments intact, and the manager staying up with the Secret pointed away. - Key files:
internal/provider/kvm/{seed.go,conn.go,kvm.go},internal/controller/proxy_controller.go,internal/provider/config_kvm.go,api/v1alpha1/proxy_types.go.
2026-09-10 21:04 UTC — KVM provider passes in-cluster; guest agent merged into cloud-config
- Two KVM Proxies with real cloud-configs reach
Readyfrom inside the kind cluster (2 min from apply, IP via the guest agent, 204 through squid from the LAN). - The
qemu-guest-agentbootstrap is now merged into each Proxy's cloud-config (packages+runcmd) instead of shipped as vendor-data: cloud-init's default merger drops vendor-data keys the user-data also defines, so any Proxy with its ownpackages:never got the agent and stayedProvisioningforever.vendorDatais now plain extra vendor-data with no default; non-cloud-config user-data (scripts,#include) still gets the bootstrap as vendor-data. A cloud-config that is not a YAML mapping fails the Proxy permanently instead of booting without its config. - The manager pod mounts a 64 MiB
emptyDiron/tmp: the seed ISO writer stages files on disk and the root filesystem is read-only. - Errors already classified with a provider sentinel no longer drop the cached libvirt SSH connection; only libvirt and transport errors do.
- Key files:
internal/provider/kvm/{seed.go,conn.go,errors.go},config/manager/manager.yaml,docs/kvm-provider.md.
2026-09-10 10:07 UTC — KVM provider validated on a real hypervisor; UEFI default; libvirt call tracing
make test-integration-kvmpasses end to end against a libvirt 11.6 / QEMU 10.1 host (Debian 13 image, UEFI, guest agent reporting the IP after ~20 s). Four defects found only on the real host, all fixed on the branch:- domain disks are now
type="file"with resolved paths: pool-volume references get no AppArmor rule fromvirt-aa-helper, so QEMU was denied the disks; - domain metadata is parsed by local name: libvirt returns the element without its namespace declaration;
- guests boot UEFI by default (
images.<key>.firmware: uefi|bios, OVMF now a host prerequisite): Debian 13's image hangs in GRUB under SeaBIOS on a headless VM with a serial console (Debian bug #1111240, open); bookworm is unaffected; - the image cache's raw-upload path is confirmed to produce a byte-identical file.
- domain disks are now
- Every libvirt RPC is logged at
--zap-log-level=debugas onelibvirt callline (rpc=virDomainCreate, identifiers, duration,libvirtErroron failure); level 2 adds XML, metadata and addresses; cloud-init is never logged. The integration test shows the trace in-voutput (KVM_TEST_LOG_LEVEL, default 1). - Key files:
internal/provider/kvm/{domainxml.go,metadata.go,trace.go,conn.go},internal/provider/config_kvm.go,docs/kvm-provider.md.
2026-09-08 11:00 UTC — KVM/libvirt provider
- New
kvmprovider type: Managed proxies as VMs on your own libvirt/KVM hosts. Each host is reached over SSH with the libvirt unix socket forwarded through the connection (pure Go, nothing to expose on the hypervisors beyond sshd); one provider instance can span several hosts, selected byplacement.zone. - Base images (
images:map or a direct URL inplacement.image) are downloaded by the operator into each host's storage pool on first use, verified by sha256 when configured, and cached there. Per-VM disks are qcow2 overlays; cloud-init arrives on a NoCloud seed ISO built in-process. GC identity lives in the domain's<metadata>element. - Wiring:
"kvm": kvm.Newincmd/main.go; optionalkvm-sshSecret mounted at/etc/proxy-operator/kvm; samplesconfig/samples/proxy_kvm.yamland akvm-homeblock inproviders-config.yaml;docs/kvm-provider.md; opt-in integration test (make test-integration-kvm) against a real hypervisor. - Not validated on a real host at the time of this entry; see 2026-09-10 above.
2026-08-10 09:34 CEST — kind e2e verified; fix Squid OOM in containers; quickstart goes in-cluster
- Full end-to-end pass on a throwaway kind cluster: Squid pod Ready with a real CONNECT probe (89 ms), lease grant/report/cooldown-409/release through the discovery API, finalizer cleanup on delete.
- Fixed the kubernetes provider's generated squid.conf:
max_filedescriptors 1024(squid sizes FD tables from the container's effectively-unlimited RLIMIT_NOFILE and was OOM-killed at startup under kind/containerd) +cache_mem 16 MB. - README quickstart now deploys the operator in-cluster:
make run-devon a laptop cannot reach kind pod IPs, so health probes fail by construction there (documented).
2026-08-09 17:27 CEST — Operator wired end to end: reconciler, health, leases, discovery, GC, two providers
cmd/main.gois now the full composition root:--providers-config(required, fail-fast),--discovery-addr,--proxy-namespace,--health-workers,--gc-interval,--gc-min-age,--gc-allow-namespaced,--lease-cooldown,--max-lease-ttl; wires the kubernetes + gcp providers (metrics-instrumented), health engine, lease store, discovery API, orphan GC, and Prometheus metrics onto one manager.- Deploy manifests: providers ConfigMap mount, optional
DISCOVERY_TOKENSecret env, discovery port 8090 + Service; pods RBAC for the kubernetes provider. - Samples for all three proxy flavors + providers-config;
make run-devfor local development. - README rewritten (kind quickstart, GCP setup, the two load-bearing caveats); architecture doc completed with components table and the full decision log.