docs/oci-import.md: removed the "Phase A acquisition-only" framing and the bootability-gap warnings. Expanded architecture section with ApplyOwnership + InjectGuestAgents. Added a "guest-side boot sequence" diagram-in-prose showing network → first-boot → vsock- agent unit ordering. Added a "how to add distro support" section pointing at the ID-case dispatch in first-boot.sh. README.md: replaced the experimental-caveat block with an honest "boots as a banger VM directly, no image build step required" description. Pointer to the docs for distro support details. Tech-debt list trimmed — ownership fixup and first-boot install are no longer planned work, they shipped. What remains: private- registry auth (authn.DefaultKeychain), cache eviction, first-boot timeout UX (retry still works but could be smoother with a FirstBootPending flag), non-systemd distros. All 20 packages green. make lint clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8.9 KiB
OCI import (banger image pull)
banger image pull <oci-ref> downloads a container image from any
OCI-compatible registry (Docker Hub, GHCR, quay.io, self-hosted, …),
flattens its layers into an ext4 rootfs, and registers the result as
a managed banger image.
Paired with the kernel catalog, this dissolves the "where do I get a rootfs" bottleneck for most users — any distro that ships an official container image can now boot (eventually) as a banger VM.
banger kernel pull void-6.12
banger image pull docker.io/library/debian:bookworm --kernel-ref void-6.12
banger image list # debian-bookworm appears, Managed=true
What works
- Pulling any public OCI image that exposes a
linux/amd64manifest. - Correct layer replay with whiteout semantics (
.wh.*deletes,.wh..wh..opqopaque-dir markers). - Path-traversal and relative-symlink-escape protection.
- Content-aware default sizing (
content × 1.25, floor 1 GiB). - Layer caching on disk, keyed by blob SHA256.
- File ownership preservation. Tar-header uid/gid/mode is captured
during flatten and applied to the resulting ext4 via a
debugfspass, so setuid binaries (sudo,passwd) and root-owned config files (/etc/shadow,/etc/sudoers) end up correctly owned. - Banger guest agents pre-injected. The pulled ext4 ships with
/usr/local/bin/banger-vsock-agent,banger-network.service, andbanger-vsock-agent.servicealready in place and enabled. - First-boot sshd install. A one-shot systemd service installs
openssh-servervia the guest's package manager on first boot — apt-get / apk / dnf / pacman / zypper dispatch based on/etc/os-release. Subsequent boots skip the install. - Piping pulled images into the existing
banger image build --from-imageflow.
What doesn't yet work
- Private registries. Auth is not implemented; anonymous pulls only. Docker Hub, GHCR (public), quay.io (public), etc. all work.
- Non-
linux/amd64platforms. The kernel catalog is x86_64-only, so pulled rootfses match.arm64is additive in the schema; wire- up lands when a user needs it. - Non-systemd distros. The injected units assume systemd as PID 1. Alpine ≥3.20 ships systemd; older alpine + void + busybox-init images won't honour the banger-network / banger-first-boot units.
- First boot needs network access. The provisioning step reaches out to the distro's package repo to install openssh-server. VMs without NAT or without the bridge reaching the internet will time out on first boot. The marker file stays in place so a later boot retries.
Architecture
internal/imagepull/ owns the pure mechanics:
Pull(imagepull.go) wrapsgo-containerregistry'sremote.Imagewith thelinux/amd64platform pinned. Layer blobs are cached on disk viacache.NewFilesystemCacheunder<OCICacheDir>/blobs/— Pull itself does not drain the layer streams; that happens lazily duringFlatten, and the cache populates on read.Flatten(flatten.go) replays layers oldest-first into a staging directory, applying whiteouts and rejecting unsafe paths. Returns aMetadatamap capturing per-file uid/gid/mode from each tar header.BuildExt4(ext4.go) runsmkfs.ext4 -F -d <staging> -E root_owner=0:0to populate the image file at create time — no mount, no sudo, no loopback. Requirese2fsprogs ≥ 1.43(mkfs.ext4 -dis the populate-at-create flag; nearly all modern distros ship it).ApplyOwnership(ownership.go) streams a batchedset_inode_fieldscript todebugfs -w -f -to rewrite per-file uid/gid/mode to the captured tar-header values. Without this pass the ext4 would carry the runner's on-disk uids.InjectGuestAgents(inject.go) uses the samedebugfsscripting to drop banger's guest-side assets into the pulled ext4 with root ownership:/usr/local/bin/banger-vsock-agent/usr/local/libexec/banger-network-bootstrap/usr/local/libexec/banger-first-boot/etc/systemd/system/banger-{network,vsock-agent,first-boot}.service- enable-at-boot symlinks under
multi-user.target.wants/ /etc/modules-load.d/banger-vsock.conf/var/lib/banger/first-boot-pending(marker file)
internal/daemon/images_pull.go orchestrates:
- Parse + validate the OCI ref.
- Derive a friendly default name (
debian-bookwormfordocker.io/library/debian:bookworm) when--nameis omitted. - Resolve kernel info via the shared
resolveKernelInputshelper (the same code path asimage register --kernel-ref). - Stage at
<ImagesDir>/<id>.staging; extract layers to a temp tree underos.TempDir(bulk transient data stays off the persistent state filesystem). imagepull.BuildExt4produces<staging>/rootfs.ext4.ApplyOwnership+InjectGuestAgentsrun in one finalize step.imagemgr.StageBootArtifactsstages the kernel triple alongside.- Atomic
os.Rename(<staging>, <final>)publishes the artifact dir. - Persist a
model.Image{Managed: true, …}record.
Any failure removes the staging dir. Post-rename failures remove the final dir and roll back the store write.
Guest-side boot sequence
On the first boot of a pulled image, systemd starts three banger units in order:
banger-network.service— runs the bootstrap script that parses/etc/banger-network.conf(written by banger's VM-create lifecycle) and brings the guest interface up with the assigned IP.banger-first-boot.service(only on first boot; removes its own trigger file on success) — reads/etc/os-release, dispatches to the native package manager, installsopenssh-server, enablesssh.service/sshd.service.banger-vsock-agent.service— runs the health-check daemon banger uses to confirm the VM is alive.
After first boot completes, subsequent boots skip the install step
entirely. Banger's host-side SSH polling (guest.WaitForSSH)
naturally retries until sshd is listening.
Adding distro support
internal/imagepull/assets/first-boot.sh is the POSIX-sh dispatch.
Add a new ID= branch and its install command to the case block,
then rebuild banger — the asset is go:embed-ed into the binary.
Supported ID values today: debian, ubuntu, kali, raspbian,
linuxmint, pop, alpine, fedora, rhel, centos, rocky,
almalinux, arch, archlinux, manjaro, opensuse*, suse.
Unknown distros fall back to ID_LIKE, then error clearly with a
pointer to edit the script.
Paths
| What | Where | Purpose |
|---|---|---|
| Layer blob cache | ~/.cache/banger/oci/blobs/sha256/<hex> |
Re-pulls of the same image digest are local-only |
| Staging dir | ~/.local/state/banger/images/<id>.staging/ |
Short-lived; atomic-renamed to <id>/ on success |
| Staging rootfs tree | $TMPDIR/banger-pull-<rand>/ |
Extraction scratch space; removed after ext4 build |
| Published image | ~/.local/state/banger/images/<id>/rootfs.ext4 |
Managed artifact stored alongside the kernel triple |
Composition with image build
A pulled image boots as-is — ownership is correct, sshd installs on
first boot, banger's agents are in place. That means the existing
image build --from-image pipeline composes on top:
banger image build --from-image debian-bookworm --name debian-dev --docker
image build spins up a transient VM using the base image, runs
scripts/customize.sh over it, and saves the result as a new managed
image with the opinionated tooling (mise, opencode, claude, pi, tmux
plugins, optionally docker) layered on top.
Tech debt
-
Auth. When we add private-registry support, the natural path is
authn.DefaultKeychainfromgo-containerregistry, which already honours~/.docker/config.jsonand the standard credential helpers. No banger-specific config needed. -
Cache eviction. Layer blobs under
OCICacheDiraccumulate forever. Abanger image cache prunecommand is a cheap follow-up when disk usage becomes a complaint. -
First-boot timeout UX. If you run
banger vm sshimmediately afterbanger vm create, the package install foropenssh-servermay still be running and SSH will fail. Current mitigation: retry. Better: a per-imageFirstBootPendingflag that tells the daemon to extend its SSH wait timeout for the first boot, cleared on success. Tracked but not implemented. -
Non-systemd distros. The guest agents assume systemd. Adding openrc / s6 / busybox-init variants means keeping parallel unit trees in
inject.gokeyed on/etc/os-release. Only pick up when a user actually wants it.
Trust model
image pull delegates trust to the OCI registry the user selected.
go-containerregistry verifies layer digests against the manifest
during download, so a tampered mirror can't ship modified layers
without breaking the sha256 chain. Beyond that, banger does not
verify OCI image signatures (cosign/sigstore) — users who care should
verify their references out-of-band.