Files
egress-proxies-operator/CHANGELOG.md
T
kacerrandClaude Opus 5 1fa3e55b9c docs: start docs/operating.md with the libvirt-RPC log query
A place for helpers against a running operator, as opposed to the design
docs. First entry is the jq filter for timestamp + rpc: both libvirt
providers log the same "libvirt call" msg, so one select covers kvm and
kvm2, and a fromjson? variant survives a stream with non-JSON lines in it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 07:53:12 +00:00

214 lines
15 KiB
Markdown

# Changelog
## 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 the `kvm2-home`
entry, four sharing one labelled cloud-init Secret and one inline (it runs squid on 8080,
so it cannot share a Secret that pins `http_port 3128`). Covers the `tier` attribute the
discovery API selects on, a `medium`/`ubuntu-24.04` member with a raised `maxLeases`, and
a member with a tightened `healthCheck`.
- Kept out of `config/samples/kustomization.yaml` on purpose so `apply -k` doesn't boot a
whole pool; the exclusion is commented there next to the existing `providers-config.yaml`
one. Validated with `kubectl 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.Close` ran 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.markDead` compounded it by
killing inside a `sync.Once` before signalling dead, which made even cancelling the reconcile
useless. With `MaxConcurrentReconciles: 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 `StorageVolUpload` outright and leaked two goroutines plus 4 MiB per occurrence,
against a 128Mi pod limit. The defect is upstream (`requestStream` never drains an unbuffered
`outErr`); 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 `kvm` provider in the same pool, so two entries could share
one file, a concurrent fetch could boot a guest from a half-written base, and a `sha256` edit
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/http` errors now join the structured log stream.** client-go's leader election
wrote `I0917 ...` lines straight to stderr because nothing routed klog into logr, and the
discovery server's `http.Server` had no `ErrorLog`. The deployment also gained
`--zap-encoder=json` and `--zap-time-encoding=rfc3339`, so `kubectl logs | jq` works.
`cmd/main.go`, `internal/discovery/server.go`, `config/manager/manager.yaml`.
- **The `kvm2-home` provider entry is enabled** in `config/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.md`** is 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.md`** is the fill-in questionnaire a new provider's
design answers before implementation; entry numbers match the contract's profile
table.
- **`docs/providers-existing.md`** maps kubernetes, gcp and kvm to every profile and
keeps their per-provider reference notes, shared gaps and proposed features.
- **`docs/providers.md`** is 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 `ListByTag` stalls 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 `ProviderHealth` object (`kubectl get ph`), one per provider, with a
`Ready` condition, 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_seconds` and
`_check_timestamp_seconds`. The flags are `--provider-check-interval` and
`--provider-check-timeout` (defaults to `min(15s, interval)`).
- Validated with `make test`, `make test-e2e` and the real-hypervisor KVM run against
beelink. `make lint` is 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 `Get` answered not-found, so a permanent error there retried forever —
a KVM host removed from the config was enough. It now calls `Delete` anyway, since
that is idempotent and can finish where `Get` cannot, and
`crawl.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;
`Get` still 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.10` arrived as `1.1`), dropped comments and reordered
keys. New `installGuestAgent` setting, default on.
- **A missing `kvm-ssh` Secret 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-agent` active 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 `Ready` from inside the kind cluster
(2 min from apply, IP via the guest agent, 204 through squid from the LAN).
- The `qemu-guest-agent` bootstrap 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 own
`packages:` never got the agent and stayed `Provisioning` forever. `vendorData` is
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 `emptyDir` on `/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-kvm` passes 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 from `virt-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.
- Every libvirt RPC is logged at `--zap-log-level=debug` as one `libvirt call` line
(`rpc=virDomainCreate`, identifiers, duration, `libvirtError` on failure); level 2
adds XML, metadata and addresses; cloud-init is never logged. The integration test
shows the trace in `-v` output (`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 `kvm` provider 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 by `placement.zone`.
- Base images (`images:` map or a direct URL in `placement.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.New` in `cmd/main.go`; optional `kvm-ssh` Secret mounted at
`/etc/proxy-operator/kvm`; samples `config/samples/proxy_kvm.yaml` and a `kvm-home`
block in `providers-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-dev` on 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.go` is 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_TOKEN` Secret env,
discovery port 8090 + Service; pods RBAC for the kubernetes provider.
- Samples for all three proxy flavors + providers-config; `make run-dev` for local development.
- README rewritten (kind quickstart, GCP setup, the two load-bearing caveats); architecture doc
completed with components table and the full decision log.