Commit Graph
132 Commits
Author SHA1 Message Date
kacerrandClaude Opus 5 18a3378507 Add plan: provider health checks
Periodic, leader-only checks of every configured provider (KVM per host
over the pooled SSH+libvirt connection including the storage pool,
kubernetes apiserver/RBAC, GCP credentials/API), surfaced through a new
cluster-scoped ProviderHealth CRD, Prometheus gauges and transition logs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 18:08:59 +00:00
kacerrandClaude Opus 5 2b4b61cdc4 Record the review-fix validation: integration, in-cluster and the missing-Secret check
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 22:59:01 +00:00
kacerrandClaude Opus 5 c23def0051 Install the guest agent from a vendor-data script instead of editing the user's cloud-config
Merging the bootstrap into each Proxy's cloud-config was the wrong
mechanism twice over. Detection only matched a literal #cloud-config
first line, so a jinja template or a MIME archive still fell back to
vendor-data, where cloud-init's default mergers drop it — the exact
failure the merge existed to prevent. And the YAML round-trip re-typed
unquoted scalars: version: 1.10 reached the guest as 1.1, comments were
dropped and keys reordered.

Neither is worth patching, because vendor-data delivered as a shell
script bypasses the merger entirely: cloud-init hands vendor-data to its
own handler chain, writes scripts to a separate directory and runs them
from cc_scripts_vendor, after the user's packages and before the user's
runcmd. The merge could not have been rescued anyway — merge_how in
user-data governs only the first source, and merge_how in vendor-data is
discarded before the document is stored.

The Proxy's cloud-init now reaches the seed byte for byte in any format.
installGuestAgent (default true) turns the script off; a configured
vendorData travels beside it as a MIME multipart, declared text/plain so
cloud-init re-sniffs it and this code never classifies user content.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 22:42:55 +00:00
kacerrandClaude Opus 5 10ca71331e Load the KVM ssh material on first use instead of at startup
The kvm-ssh Secret is mounted optional, but kvm.New read the key and
known_hosts eagerly and registry.Build aborts the whole provider set on a
constructor error, so a missing or misnamed Secret exited the manager
before any controller started and took the kubernetes and gcp providers
down with it.

lazyAuth loads the material on the first connection and caches it once it
works, so a Secret created after the manager starts heals the affected
proxies on the next reconcile with no restart. A missing file classifies
transient for that reason; a key that does not parse is permanent.

New still checks both files and logs a WARNING: line when they are
unreadable, keeping the startup diagnostic the eager load was there for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 22:34:28 +00:00
kacerrandClaude Opus 5 bbe9b751a9 Reject KVM host names and image keys that would break the identifiers they land in
A host name is pasted into the providerID hosts/<name>/domains/<vm>, which
parseProviderID splits on "/", so a name containing a slash produced an ID
that never parsed again: Get, Delete and GC all failed permanently and the
VM leaked with the Proxy's finalizer stuck.

The image map key has the same defect and lands in a file name rather than
an ID: base-<key>.<format> in the host's storage pool. Both now have to
match a plain identifier pattern. Machine-type keys are left alone; they
are only ever compared, never embedded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 22:31:34 +00:00
kacerrandClaude Opus 5 ba24d7e3d9 Let a Proxy finish deleting when its instance can no longer be reached
reconcileDelete asked Get first and dropped the finalizer only on
not-found, so a permanent Get retried forever: a KVM host removed from
the config, or a providerID the provider cannot resolve, left the Proxy
in Terminating with no way out. It now tries Delete anyway, which is
idempotent and can still finish where Get cannot converge, and releases
the object when that succeeds.

crawl.example.com/skip-cleanup abandons the instance for the cases that
stay stuck, such as revoked credentials. It is checked before the
provider lookup, so it also frees a Proxy whose provider is gone from
the config.

In the KVM provider, Delete over an unconfigured host is now a no-op
success, mirroring removeVolumes over a missing pool. Get keeps failing
permanently there on purpose: the live path reads not-found as "rebuild
it", so a typo in hosts must not look like a missing VM.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 22:30:15 +00:00
kacerrandClaude Opus 5 ce365e2602 Add plan: fix the five code-review findings on the KVM branch
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 22:26:47 +00:00
kacerrandClaude Fable 5.1 0daf56bbad Close out the KVM provider plan: Step 7 done, MR #5 opened
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 21:41:38 +00:00
kacerrandClaude Fable 5.1 366736f53f Merge the qemu-guest-agent bootstrap into each Proxy's cloud-config instead of vendor-data
In the kind cluster two KVM Proxies with real cloud-configs booted fine
but never left Provisioning: the guest agent was not installed, so the
provider could not learn the IP. cloud-init merges vendor-data under
user-data with its default mergers, which keep the user-data's value
for any key both define and never append lists, so a cloud-config with
its own `packages:` silently dropped the vendor-data's packages and
runcmd. The integration test passed only because its user-data was
empty.

withGuestAgent now appends qemu-guest-agent to `packages` and the
enable command to `runcmd` of the Proxy's own cloud-config (skipping
entries already present) and re-renders it. Vendor-data is no longer
defaulted; `vendorData` is plain extra data. User-data that is not a
cloud-config document (script, #include, MIME) still gets the bootstrap
as vendor-data. A cloud-config that is not a YAML mapping, or whose
packages/runcmd is not a list, fails the Proxy as ErrPermanent.

Validated in-cluster: both Proxies Ready and healthy in two minutes, 204
through squid from the LAN. Docs, sample comment, CHANGELOG and the
plan execution log (Run 7) updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 21:06:54 +00:00
kacerrandClaude Fable 5.1 c5ebb2e044 Worked example and make target for the kvm-ssh Secret
docs/kvm-provider.md walks through key generation, ssh-copy-id,
ssh-keyscan under the address the operator dials, and the Secret from
the private key plus ~/.ssh/kvm_known_hosts. make kvm-ssh-secret runs
the same as an apply with KVM_SSH_KEY / KVM_KNOWN_HOSTS overrides and
refuses a .pub, the classic mistake. README mirrors the short form.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 19:13:51 +00:00
kacerrandClaude Fable 5.1 cc0606d24f Record the passing KVM integration run; CHANGELOG for validation, UEFI and tracing
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 19:13:51 +00:00
kacerrandClaude Fable 5.1 c4f59bd153 Name the upstream cause of the Debian 13 BIOS hang: Debian bug #1111240
The known-gaps entry, decision log, sample comment and builder comment
blamed the host; the cause is trixie's GRUB config ("terminal_output
gfxterm serial" with a serial console and no video device), reported
and reproduced upstream and still open. UEFI remains the default for the
reason recorded; bookworm images are noted as unaffected.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 19:13:51 +00:00
kacerrandClaude Fable 5.1 9a393b1df8 Boot KVM guests via UEFI by default; per-image firmware setting
Run 5 on the real host bisected the guest hang: the Debian 13 image
hangs after GRUB under SeaBIOS with or without our seed and with either
CPU model, and boots under OVMF. The image's legacy boot chain is the
problem, not the provider.

images.<key>.firmware is "uefi" (default) or "bios". The domain XML
emits <os firmware="efi"> with secure boot off, so libvirt selects the
OVMF binary and manages the NVRAM file, which the existing nvram undefine
flag removes. Direct-URL images default to UEFI too. OVMF joins the
hypervisor prerequisites; sample, config table, known gaps, decision log
and the execution log record the finding.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 19:13:51 +00:00
kacerrandClaude Fable 5.1 5a6c889531 Make the libvirt trace compact: libvirt API names, one error field, terse test output
The first live run showed the trace working but noisy: testr printed
ts/level/msg and the per-run constants on every line, the error text
appeared twice, host appeared twice on image-cache lines, and the Go
method name was less useful than the libvirt function it maps to.

The tracer now logs rpc=virDomainLookupByName (go-libvirt's names are the
public API names without the vir prefix), error plus libvirtError (code
name) and libvirtCode, and image.go stops re-adding host. The integration
test replaces testr with a compact logr sink: time, logger name, the rpc
as headline, then fields, with provider/host/name omitted since the first
line of the run states them. Docs and execution log updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 19:13:51 +00:00
kacerrandClaude Fable 5.1 f50d30e58e Trace every libvirt call in the KVM provider at V(1), visible in the integration test
A tracingAPI decorator wraps the 17-method libvirt seam and logs one
"libvirt call" line per RPC after it returns: method, the identifiers it
was given, curated results, duration, and on failure the error with its
libvirt code, name and message. At V(2) the same line carries the domain
and volume XML, metadata and addresses; upload bodies are never logged.
The pool applies it in call() when V(1) is enabled and logs connect,
dial-failure and drop events, taking the caller's logger explicitly like
every other helper in the package.

The integration test installs a testr logger at KVM_TEST_LOG_LEVEL
(default 1) so the trace interleaves with t.Log output, which also ends
the "log.SetLogger(...) was never called" warning. Docs gained a Logging
section and the test env var; the KVM provider execution log records run
4 (guest hang at GRUB) that prompted this.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 19:13:51 +00:00
kacerrandClaude Fable 5.1 aa8b36e1d0 Add plan: trace every libvirt call in the KVM provider
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 19:13:51 +00:00
kacerrandClaude Fable 5.1 ffb328661d Parse KVM domain metadata by local name: libvirt returns it without a namespace
Run 3 on the real host got past Create and failed in Get: libvirt 11.6
selects the <instance> element by our URI in DomainGetMetadata but hands
it back with no namespace declaration, and the parser required the URI
on the element. The lookup already guarantees the namespace, so match on
the local name only. The fake now strips the declaration the way libvirt
does, and a new test pins the bare form. Execution log updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 19:13:51 +00:00
kacerrandClaude Fable 5.1 cd51886139 Reference KVM domain disks by host path so AppArmor-confined QEMU can open them
The first run against a real hypervisor (beelink, Ubuntu, libvirt 11.6)
failed at DomainCreate with "Could not open base image: Permission denied"
even with correct DAC ownership. libvirt's virt-aa-helper only grants QEMU
access to disk sources that carry a path; the <disk type="volume"> form
chosen in Step 2 gives it pool and volume names instead, so the overlay and
its backing image get no AppArmor rule on any stock Debian/Ubuntu host.

buildDomainXML now takes DiskPath/SeedPath and emits type="file" disks.
ensureOverlay and writeSeed return the volume handles and Create resolves
both paths with StorageVolGetPath before defining the domain. The domain
XML test rejects any volume/pool reference so this cannot drift back; the
Create test asserts the sources equal the paths libvirt reported.

Execution log: Step 2 note marked reverted, Step 7 entry opened with the
two failed runs and the diagnosis.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 19:13:51 +00:00
kacerrandClaude Fable 5.1 791988b5a0 Wire the KVM provider: main.go, kvm-ssh Secret mount, samples, docs, CHANGELOG
Registers "kvm" in the provider builtins, mounts an optional kvm-ssh
Secret at /etc/proxy-operator/kvm, adds a kvm-home providers-config
sample and a proxy_kvm.yaml Proxy sample, documents the provider in
docs/kvm-provider.md (prerequisites, config reference, image cache and
its do-not-replace-in-use rule, known gaps), and updates the README
and architecture provider sections and decision log.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 19:13:51 +00:00
kacerrandClaude Fable 5.1 b3984ca69d Add KVM provider integration test against a real hypervisor
Behind -tags=integration and self-skipping unless KVM_TEST_* names a
host: one sequential lifecycle (image fetch, Create, idempotent
re-Create, Running with IP, ListByTag, Delete, pool state afterwards),
driven through the provider directly so it needs no Step 5 wiring.
Adds `make test-integration-kvm` and documents the run and the
deviation from the testcontainers convention in docs/testing.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 19:13:51 +00:00
kacerrandClaude Fable 5.1 97cb854693 Add KVM provider methods: Create/Get/Delete/ListByTag over the libvirt seam
Create looks the domain up first and only starts it when it exists;
otherwise it caches the image, reuses or creates the overlay, rebuilds
the seed ISO unconditionally (a cancelled upload can leave a truncated
seed that no later attempt would repair), defines and starts the domain.
Get maps libvirt state plus the first routable IPv4 from ARP or the
guest agent onto the provider states. Delete destroys, undefines and
removes the per-VM volumes by name, tolerating every already-gone case.
ListByTag keeps managed domains across all hosts, skipping domains it
cannot read and failing the sweep only for an unreachable host.

Also corrects the Step 2 seed determinism test: the iso9660 writer
stamps the build time into the volume descriptor, so builds differ by a
few bytes; the test now compares label, files and size.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 19:13:51 +00:00
kacerrandClaude Fable 5.1 6dba33adf2 Add KVM provider image cache: fetch base images into host pools on first use
resolveImage maps placement.image to a configured key or URL; imageCache
streams the download through StorageVolUpload with a sha256 tee, writes
a marker volume when the upload is verified, and re-fetches when the
marker is missing or the configured checksum changed. Errors carry
taxonomy sentinels that classify now honours. Adds the in-memory fake
libvirt API used from here on.

Step 3b of docs/plans/2026-09-04-1404-kvm-provider.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 19:13:51 +00:00
kacerrandClaude Fable 5.1 6322184d50 Add KVM provider transport: SSH-forwarded libvirt connections and error classification
Per-host connection pool over SSH with the libvirt unix socket forwarded
through the session, lazy dial and redial after disconnect, context
cancellation for go-libvirt's context-less RPCs, handshake deadline and
SSH keepalives. Error classification onto the provider taxonomy. Tested
with a fake dialer and an in-process SSH server that forwards unix sockets.

Step 3 of docs/plans/2026-09-04-1404-kvm-provider.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 19:13:51 +00:00
kacerrandClaude Fable 5.1 162607dfe2 Add KVM provider pure builders: domain XML, volumes, seed ISO, metadata
Introduces internal/provider/kvm with the side-effect-free pieces of the
provider: providerID format/parse, the namespaced <metadata> element that
carries the GC contract, q35 domain XML over pool volumes, volume XML for
overlays and raw seeds, the NoCloud seed ISO built with kdomanski/iso9660,
and config defaults. All unit-tested.

Step 2 of docs/plans/2026-09-04-1404-kvm-provider.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 19:13:51 +00:00
kacerrandClaude Fable 5.1 84dce0462e Add KVM provider config: KVMConfig structs and validation
Adds the kvm block to the providers config (SSH access, images map,
machine-type profiles, hosts list) with validation and table tests.
validate() now rejects mismatched type-specific blocks generically via
setBlocks() instead of pairwise checks.

Step 1 of docs/plans/2026-09-04-1404-kvm-provider.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 19:13:51 +00:00
kacerrandClaude Fable 5.1 bdd1c6ce6c Add plan: KVM/libvirt provider
Design for a kvm provider type: SSH-forwarded libvirt socket, multi-host
config with placement.zone selecting the host, bridged networking with
ARP/guest-agent IP discovery, operator-fetched distro cloud images cached
in the host's storage pool, and a NoCloud seed ISO carrying the CRD's
cloud-init user-data.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 19:13:51 +00:00
kacerrandClaude f8b911f1db Record live and negative Tempo e2e runs in the execution log
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 12:39:05 +02:00
kacerrandClaude e691105f89 Add Tempo-gated e2e test for OTel tracing
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 12:31:41 +02:00
kacerrandClaude 026acea279 Add plan: e2e test of OTel tracing against real Tempo
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 12:25:30 +02:00
kacerrandClaude aeb4115c72 Add tracing manifests and docs; clean up branch lint findings
Manager env block (downward-API resource attrs, commented OTLP
examples), architecture §10 + Decisions entries, README section.
Lint: goconst constants, gofmt, logcheck (Setup now takes its logger
from ctx via logf.FromContext).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 11:30:27 +02:00
kacerrandClaude 53c0d77ef5 Enrich GCP wire logs with trace context from the request ctx
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 11:09:48 +02:00
kacerrandClaude bca32f10d3 Trace GC sweeps and (opt-in) health probes
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 11:07:13 +02:00
kacerrandClaude ec7267b8de Trace discovery API requests and enrich request logs
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 10:59:48 +02:00
kacerrandClaude 896bf89a19 Trace reconciles: root span wrapper plus state-machine sub-spans
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 10:53:52 +02:00
kacerrandClaude 8e4f3b9b13 Wire tracing into the composition root
Setup after SetLogger, wrapped rest configs (manager + kubernetes
provider), tracing-outermost provider decorators, and an explicit
trace flush after mgr.Start returns (os.Exit skips defers).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 10:51:24 +02:00
kacerrandClaude d15b06ec45 Add provider.WithTracing decorator mirroring WithMetrics
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 10:50:13 +02:00
kacerrandClaude 6b34f68469 Add internal/tracing: env-gated OTel setup, span/log helpers, decorators
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 10:43:55 +02:00
kacerrandClaude b1a16774bf Align OTel deps at v1.45.0 and add trace exporter modules
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 10:41:06 +02:00
kacerrandClaude 328877e000 Add plan: OTel tracing integrated with logr/zap logging
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 10:26:21 +02:00
kacerrandClaude f6b67006dc Add tmux demo driver and table variant of egress IP check
run-demo.sh opens a 2x2 tmux grid: egress-IP table looping in a
netshoot pod, kubectl get px watch, and both create scripts running
with COUNT (default 4) proxies. show-egress-ips-table.sh is the
pane-sized one-line-per-proxy variant used by the driver.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 23:10:23 +02:00
kacerrandClaude 0d68111bc2 Add demo scripts: egress IP check and bulk proxy creation
docs/demo/show-egress-ips.sh probes each healthy proxy from the
discovery API against an IP-echo site; create-kubernetes-proxies.sh and
create-gcp-proxies.sh bulk-create demo Proxies, the gcp one spreading
them across randomly picked EU zones.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 22:40:24 +02:00
kacerrandClaude e1abac3e8f Add plan: demo script for egress IP check via proxies
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 22:21:28 +02:00
kacerrandClaude f3ff6a0ca2 Use $BASE_URL variable in docs/api.md curl examples
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 22:01:50 +02:00
kacerrandClaude 09845e4eaf Document the discovery API in docs/api.md
Full client-facing reference: auth, all routes with schemas and curl
examples, selection/cooldown semantics, caveats. README and
architecture.md link to it.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 21:55:16 +02:00
kacerrandClaude e7fdae0859 Add plan: discovery API documentation
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 21:53:02 +02:00
kacerrandClaude 57e3ea22cf Record MR creation in plan execution summary
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 19:41:57 +02:00
kacerrandClaude 95c487415b Add Gitea Actions image-build workflow
Distilled from the house pattern across sibling projects: tag push +
workflow_dispatch triggers, REGISTRY_TOKEN login, raw docker build/push
to gitea.home.hrajfrisbee.cz. Adds a lightweight test gate, an immutable
sha-<12> tag, :latest only on real tag pushes, and a concurrency group.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 19:39:23 +02:00
kacerrandClaude 849ec1083e Add plan: distilled Gitea Actions image-build workflow
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 19:37:42 +02:00
kacerrandClaude 19d6a8dfba Add GCP deployment docs, PR review notes, and Claude tooling updates
docs/gcp-in-specific-project.md: SA + firewall setup for the egress-proxy
project, in-kube secret, and apply-ready ConfigMap/Deployment/Proxy
manifests (Ubuntu image — debian-cloud lacks cloud-init).
docs/gcp-vm-validation.md: end-to-end GCP VM validation walkthrough.
docs/reviews/: proxy-operator PR review notes from 2026-08-10.
.claude/: operator-reviewer agent, accumulated permission allowlist.
.gitignore: never commit sa_key.json (live SA key stays untracked).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 19:17:58 +02:00
kacerrandClaude 420c3509b0 Wire logging: drop auth token-exchange records, elide huge payload fields
The option.WithLogger logger also reaches cloud.google.com/go/auth,
which logged its token exchange at Debug — JWT assertion and bearer
token included. wireLogger now allowlists only the compute client's
api request/response records at Debug (fail-closed for future SDK
additions); Warn/Error pass through. String fields over 1KiB (e.g.
Shielded-VM UEFI dbx blobs) are elided recursively by default; the new
--gcp-wire-log-full-payloads flag restores verbatim payloads.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 19:02:28 +02:00