feat: pre-bundle control-plane images for air-gap first boot (ADR-16)#22
Merged
Conversation
…4205, #4204) Bake the kubeadm control-plane images into the OS image and import them into containerd at boot, so a first boot converges with no registry access (ADR-16). This resolves the cold-init image strategy and de-risks the #4204 never-hang budget (kubeadm init no longer pulls). - Dockerfile: new image-bundler stage installs a checksum-verified static crane, resolves the image set from the bundled kubeadm (the same invocation as the pause pin, so the two cannot drift), and crane-pulls each ref to a ctr-importable tarball under /opt/provider-kubernetes/images/. Static ctr is added to the containerd build so the import works on the musl Hadron base. Build-time assertion: >=5 tarballs and the pause image present. - internal/imageimport + 'import-images' subcommand: import every *.tar into containerd's k8s.io namespace via argv ctr (no shell), bounded, idempotent, no-op when nothing is bundled, attempt-all-and-report. - provider: emit the import as a yip step ordered BEFORE the reconcile step in the same network.after stage, so images are in containerd before kubeadm init runs. A systemd oneshot alone races the kairos-agent reconcile stage (proven in the air-gap VM: kubeadm init ran with 1/7 images and failed); the yip in-stage ordering is deterministic. The oneshot is kept for the kubelet/reboot path. - reconcile: budget regression test locking the #4204 never-hang invariant (Total is the binding ceiling, below PerAttempt*MaxAttempts). Validated end to end on an isolated (no-NAT) libvirt network on the Hadron v0.4.0 musl base: node reaches Converged with zero containerd image pulls, all control-plane pods Running, and re-converges after reboot. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: William Rizzo <william.rizzo@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Pre-bundle the kubeadm control-plane images into the OS image and import them into
containerd at boot, so a provider-kubernetes node converges on first boot with
no registry access (air-gap). Implements ADR-16; resolves the cold-init image
strategy and de-risks the never-hang budget (kubeadm init no longer pulls).
Closes kairos-io/kairos#4205. Addresses kairos-io/kairos#4204 (never-hang budget).
How
Dockerfile): a newimage-bundlerstage installs achecksum-verified static
crane, resolves the control-plane image set from thebundled
kubeadm config images list(the same invocation as the pause pin, sothe two can never drift), and
crane pulls each ref to a ctr-importable tarballunder
/opt/provider-kubernetes/images/. Staticctris added to the containerdbuild so the import works on the musl Hadron base. Build-time assertion:
>=5tarballs and the pause image present.
internal/imageimport+import-imagessubcommand): imports every*.tarinto containerd'sk8s.ionamespace via argvctr(no shell), boundedby context, idempotent, a no-op when nothing is bundled, attempts every tarball
and reports the first error.
internal/provider): the import is emitted as a yip step orderedbefore the reconcile step in the same
network.afterstage, so images are incontainerd before
kubeadm init/joinruns. A systemd oneshot alone races thekairos-agent reconcile stage. A
systemd/provider-kubernetes-image-import.serviceoneshot is kept (ordered before kubelet) for the kubelet/reboot path.
internal/reconcile/budget_test.go): regression test locking the#4204 never-hang invariant —
Totalis the binding ceiling, belowPerAttempt*MaxAttempts.Validation
go build/go test ./.../vet/golangci-lintall green (imageimport 95%coverage).
End-to-end on an isolated (no-NAT) libvirt network on the Hadron
v0.4.0muslbase:
1.1.1.1fails (registry unreachable).phase=Converged / outcome=success; v1.34.0,containerd://2.1.4, Hadron Linux, kernel 7.1.0-hadron.provider-kubernetes.kairos.io/*annotations present.The first air-gap boot caught a real ordering bug (the systemd oneshot raced the
reconcile yip stage; kubeadm init ran with 1/7 images) — fixed by the yip in-stage
ordering above and re-validated.
Notes / follow-ups (not in this PR)
crane pull(content-addressed, reproducible) — needs security-architect review.
localAPIEndpoint.advertiseAddresstocontrol_plane_hostfor routeless air-gap nodes (kubeadm rejects a
0.0.0.0bind with no defaultroute).