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>
This commit is contained in:
@@ -6,7 +6,7 @@ Plan: [2026-09-04-1404-kvm-provider.md](../plans/2026-09-04-1404-kvm-provider.md
|
||||
- [x] Step 1 — Config (`KVMConfig` & friends, `validate()` case, tests)
|
||||
- [x] Step 2 — Pure builders (`domainxml.go`, `volume.go`, `seed.go`, `id.go`, `metadata.go`)
|
||||
- [x] Step 3 — Transport + errors (`conn.go`, `errors.go`)
|
||||
- [ ] Step 3b — Image cache (`image.go`)
|
||||
- [x] Step 3b — Image cache (`image.go`)
|
||||
- [ ] Step 4 — Provider methods (`kvm.go`) over a fake API
|
||||
- [ ] Step 5 — Wiring, manifests, samples, docs, CHANGELOG
|
||||
- [ ] Step 6 — Integration test against a real hypervisor
|
||||
@@ -151,3 +151,48 @@ domain that is already off) to Transient; Delete in Step 4 must check that
|
||||
code itself via `hasCode` and treat it as success rather than relying on
|
||||
the classifier. Nothing maps to `ErrQuotaExceeded` — libvirt has no quota
|
||||
concept; an out-of-space pool surfaces as a Transient `ErrInternalError`.
|
||||
|
||||
## Step 3b — Image cache
|
||||
|
||||
`image.go` follows the plan closely: `resolveImage` maps `placement.image`
|
||||
to a configured key or a direct http(s) URL (else Permanent);
|
||||
`imageCache.ensure` checks base volume + marker (+ marker content when a
|
||||
checksum is configured), tears down and re-fetches when anything is off,
|
||||
streams the HTTP body through `StorageVolUpload` with a `sha256` tee,
|
||||
refreshes the pool, then writes the marker last. A per-`(host, volume)`
|
||||
mutex serialises concurrent callers; the pool itself is the only cache
|
||||
state.
|
||||
|
||||
Errors from this file are pre-tagged with the taxonomy sentinel via
|
||||
`fmt.Errorf("%w: …", provider.ErrPermanent, …)` and `classify` now honours
|
||||
such tags before looking at libvirt codes. That keeps one `wrapErr` path in
|
||||
the provider instead of special-casing image errors.
|
||||
|
||||
`fake_test.go` landed with this step rather than Step 4: an in-memory
|
||||
`libvirtAPI` with pools, volumes (content, format, capacity, backing path),
|
||||
domains (state, metadata, per-source interface addresses), per-method error
|
||||
injection (`fail` always / `failOnce`) and an `uploadHook` to break a stream
|
||||
mid-way. It models the libvirt error codes the provider branches on
|
||||
(`ErrStorageVolExist`, `ErrNoStorageVol`, `ErrNoDomain`,
|
||||
`ErrOperationInvalid`, `ErrNoDomainMetadata`, `ErrAgentUnresponsive`).
|
||||
|
||||
Tests cover every case in the plan plus: no checksum configured (marker is
|
||||
still written with the computed digest; second call needs no marker read),
|
||||
403/500 alongside 404/503, missing pool → Permanent, cancelled ctx →
|
||||
Transient with nothing created, and different hosts fetching independently.
|
||||
|
||||
One test-harness lesson worth keeping: `httptest`'s server drops
|
||||
`Content-Length` and switches to chunked encoding for bodies larger than
|
||||
its 2 KiB write buffer, so the fake mirror sets the header explicitly.
|
||||
Real distro mirrors send it; a server that doesn't is reported as
|
||||
Permanent with a message naming the header.
|
||||
|
||||
Worth noting: the base volume is created with `capacity = Content-Length`
|
||||
and the *image's* format (`qcow2`), and the raw bytes are uploaded over it.
|
||||
That relies on libvirt's upload writing the file verbatim (which is how
|
||||
`virsh vol-upload` populates volumes); the following `StoragePoolRefresh`
|
||||
makes libvirt re-read the real virtual size from the qcow2 header. Step 6
|
||||
confirms this on a real host. Also, the whole download runs inside one
|
||||
`pool.call`, so cancellation aborts the HTTP body (ctx-bound) and the
|
||||
upload stream fails; the half-written base volume is deleted on the spot
|
||||
and, failing that, on the next attempt via the missing marker.
|
||||
|
||||
Reference in New Issue
Block a user