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>
This commit is contained in:
@@ -4,7 +4,7 @@ Plan: [docs/plans/2026-09-10-2222-kvm-review-fixes.md](../plans/2026-09-10-2222-
|
||||
- [x] Step 0 — Save plan into the repo
|
||||
- [x] Step 1 — Unwedge deletion (finding 1)
|
||||
- [x] Step 2 — Validate names that become identifiers (finding 2)
|
||||
- [ ] Step 3 — Read the SSH material lazily (finding 3)
|
||||
- [x] Step 3 — Read the SSH material lazily (finding 3)
|
||||
- [ ] Step 4 — Guest agent via vendor-data script (findings 4 and 5)
|
||||
- [ ] Step 5 — Validate, record, push
|
||||
|
||||
@@ -65,6 +65,39 @@ boundary. It is still cheaper to reject at load than to explain a stuck
|
||||
Proxy later. Both new table cases assert the message text, so the wording
|
||||
that names the consequence is now pinned by tests.
|
||||
|
||||
## Step 3 — Read the SSH material lazily
|
||||
|
||||
`kvm.New` no longer loads the key; `lazyAuth` does, on the first connection,
|
||||
caching the result once it works. `registry.Build` is all-or-nothing by
|
||||
design, so the old constructor error did not merely fail the KVM provider,
|
||||
it exited the manager before any controller started.
|
||||
|
||||
The plan said "lazy dialer", and the first attempt was exactly that — one
|
||||
closure wrapping both the loading and the dial. It could not be tested: the
|
||||
fixture's SSH server is real, but the socket behind it is an echo server, so
|
||||
every successful dial dies in the libvirt handshake and the assertion could
|
||||
not tell "the key worked" from "the key did not". Splitting the seam in two,
|
||||
`lazyAuth` for the material and `lazyDialer` for the wiring, made both
|
||||
testable: the healing path against `lazyAuth`, and a separate test that the
|
||||
dialer really authenticates, asserted by the error being past SSH and by the
|
||||
server having seen the forward request.
|
||||
|
||||
Error classes carry the intent. A missing file stays unclassified, so
|
||||
`provider.Class` defaults it to transient and a Proxy heals on its own when
|
||||
the Secret appears — the common case, since the Secret is usually created
|
||||
after the manager. A key that does not parse wraps `ErrPermanent`, because
|
||||
retrying cannot fix it. `newSSHAuth` distinguishes the two with
|
||||
`errors.Is(err, fs.ErrNotExist)`.
|
||||
|
||||
Worth noting: the startup check is kept, just non-fatally, logged with the
|
||||
`WARNING:` prefix the repo already uses where logr has no warn level. That
|
||||
preserves the diagnostic the eager version was written for. The caching test
|
||||
deliberately deletes the key file after a successful load and asserts the
|
||||
cached value is reused, which pins the rotation behaviour the docs promise:
|
||||
a rotated Secret needs a manager restart. `TestNew_badKeyPathFailsFast` was
|
||||
inverted rather than deleted, so the old contract stays visible in the diff
|
||||
next to the new one.
|
||||
|
||||
Worth noting: the review filed this as a KVM finding, but the fix is mostly
|
||||
in the controller, because KVM is the only provider that resolves a
|
||||
providerID segment against operator config at all. GCP and Kubernetes hand
|
||||
|
||||
Reference in New Issue
Block a user