OCI layer blobs accumulate forever — every pull writes layers to
~/.cache/banger/oci/blobs/sha256/<hex> via go-containerregistry's
filesystem cache, and nothing ever evicts them. The cache is purely
a re-pull-avoidance (every flattened image is independent of the
blobs that sourced it), so it's a perfect candidate for an opt-in
operator-driven prune.
New surface:
* api: ImageCachePruneParams{DryRun}, ImageCachePruneResult
{BytesFreed, BlobsFreed, DryRun, CacheDir}.
* daemon: ImageService.PruneOCICache walks layout.OCICacheDir for
a (bytes, blobs) tally, then — outside dry-run — atomically
renames the cache aside, recreates it empty, and rm -rf's the
aside dir. The rename-then-rm avoids leaving the cache in a
half-removed state if a pull starts mid-prune (the in-flight
pull's open files survive the rename via standard Linux
semantics; it just sees a fresh empty cache afterwards). Missing
cache dir is treated as zero — fresh installs that have never
pulled an OCI image don't error.
* dispatch: image.cache.prune RPC (paramHandler-wrapped, mirroring
every other image RPC). Documented-methods test list updated.
* cli: `banger image cache` group with a `prune` subcommand
(--dry-run flag). Output is a single line: "freed 1.2 GiB
across 47 blob(s) in /var/cache/banger/oci" or "would free …".
formatBytes helper for the size pretty-print.
docs/oci-import.md: replaced the "Tech debt: cache eviction" bullet
with a "Cache lifecycle" section describing the new command and
the in-flight-pull caveat.
Tests: PruneOCICache covers the happy path (real prune empties the
cache, recreates an empty dir, doesn't leak the .pruning- aside),
the dry-run path (returns size, leaves blobs intact), and the
fresh-install path (cache dir absent → zero result, no error).
Smoke at JOBS=4 still green; live exercise against an empty cache
on a system install prints the expected zero summary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6.9 KiB
OCI import (banger image pull)
banger image pull has two paths. The primary one — catalog bundle —
is documented in docs/image-catalog.md. This
doc covers the fallthrough: OCI-registry pull for arbitrary container
images.
When to use it
Use the OCI path when you need a distro or image that isn't in the
catalog. The catalog covers the common happy path
(debian-bookworm); anything else (alpine, fedora, ubuntu,
custom corporate images) goes through OCI pull.
banger image pull docker.io/library/alpine:3.20 --kernel-ref generic-6.12
banger image pull ghcr.io/myorg/devimg:v2 --kernel-ref generic-6.12
banger image pull dispatches based on the reference:
banger image pull debian-bookworm→ catalog (fast path).banger image pull docker.io/library/foo:bar→ OCI (anything not in the catalog).
What works
- Any public OCI image that exposes a
linux/amd64manifest. - Correct layer replay with whiteout semantics (
.wh.*deletes,.wh..wh..opqopaque-dir markers). - Path-traversal, debugfs-hostile filename, and relative-symlink-escape protection.
- Content-aware default sizing (
content × 1.5, floor 1 GiB). - Layer caching on disk, keyed by blob sha256.
- Ownership preservation — tar-header uid/gid/mode captured
during flatten, applied to the ext4 via a
debugfspass, so setuid binaries (sudo,passwd) and root-owned config (/etc/shadow,/etc/sudoers) end up correctly owned. - Pre-injected banger agents — the pulled ext4 ships with
banger-vsock-agent,banger-network.service, and thebanger-first-bootunit already enabled. - First-boot sshd install — a one-shot systemd service installs
openssh-servervia the guest's package manager on first boot. Dispatches on/etc/os-release→apt-get/apk/dnf/pacman/zypper. Subsequent boots skip the install.
What doesn't yet work
- Private registries. Anonymous pulls only. Docker Hub, GHCR
(public), quay.io (public) all work. Adding auth via
authn.DefaultKeychain(fromgo-containerregistry) is a cheap follow-up when someone needs it. - Non-
linux/amd64. The kernel catalog is x86_64-only, so pulled rootfses match.arm64is additive in the schema. - Non-systemd rootfses. The injected units assume systemd as PID 1. Alpine ≥3.20 ships systemd; older alpine + void + busybox- init images won't honour the banger-* units.
- First boot needs network access. The first-boot sshd install reaches out to the distro's package repo. VMs without NAT or without the bridge reaching the internet time out. The marker file stays in place so a later restart retries.
Architecture
internal/imagepull/ owns the mechanics:
Pullwrapsgo-containerregistry'sremote.Imagewith thelinux/amd64platform pinned. Layer blobs cache under~/.cache/banger/oci/blobs/and populate lazily during flatten.Flattenreplays layers oldest-first into a staging directory, applies whiteouts, rejects unsafe paths plus filenames that banger's debugfs ownership fixup cannot encode safely. Returns aMetadatamap of per-file uid/gid/mode from tar headers.BuildExt4runsmkfs.ext4 -F -d <staging> -E root_owner=0:0at the size of the pre-truncated file — no mount, no sudo, no loopback. Requirese2fsprogs ≥ 1.43.ApplyOwnershipstreams a batchedset_inode_fieldscript todebugfs -wto rewrite per-file uid/gid/mode to the captured tar- header values.InjectGuestAgentsuses the samedebugfsscripting to drop banger's guest assets into the ext4 with root ownership: vsock agent binary, network bootstrap + unit, first-boot script + unit,multi-user.target.wantssymlinks, vsock modules-load config,/var/lib/banger/first-boot-pendingmarker.
internal/daemon/images_pull.go orchestrates pullFromOCI:
- Parse + validate the OCI ref, derive a default name when
--nameis omitted (debian-bookwormfromdocker.io/library/debian:bookworm). - Resolve kernel info via
resolveKernelInputs(auto-pulls fromkernelcatif--kernel-refnames a catalog entry that isn't yet local). - Stage at
<ImagesDir>/<id>.staging; extract layers to a temp tree under$TMPDIR. BuildExt4→ApplyOwnership→InjectGuestAgents.imagemgr.StageBootArtifactsstages the kernel triple alongside.- Atomic
os.Renamepublishes the artifact dir. - Persist a
model.Image{Managed: true, …}record.
Guest-side boot sequence
On first boot of a pulled image:
banger-network.service— brings the guest interface up with the IP assigned by banger's VM-create lifecycle.banger-first-boot.service(first boot only) — reads/etc/os-release, dispatches to the native package manager, installsopenssh-server, enablesssh.service.banger-vsock-agent.service— the health-check daemon banger uses to confirm the VM is alive.
Subsequent boots skip step 2.
Adding distro support to first-boot
internal/imagepull/assets/first-boot.sh is the POSIX-sh dispatch.
Add a new ID= branch and its install command, then rebuild banger
(the asset is go:embed-ed).
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 cleanly.
Paths
| What | Where |
|---|---|
| Layer blob cache | ~/.cache/banger/oci/blobs/sha256/<hex> |
| Staging dir | ~/.local/state/banger/images/<id>.staging/ |
| Extraction scratch | $TMPDIR/banger-pull-<rand>/ |
| Published image | ~/.local/state/banger/images/<id>/rootfs.ext4 |
Cache lifecycle
OCI layer blobs accumulate as you pull images. Banger flattens every pull into a self-contained ext4, so the cache is purely a re-pull avoidance — losing it only costs network round-trips on the next pull of the same image. Reclaim disk with:
banger image cache prune --dry-run # report size only
banger image cache prune # remove every cached blob
Run with the daemon idle; an in-flight pull racing against prune may fail and need a retry.
Tech debt
- Auth. When we add private-registry support, the natural path
is
authn.DefaultKeychain, which honours~/.docker/config.jsonand the standard credential helpers. - Non-systemd rootfses. The guest agents assume systemd. Adding
openrc / s6 / busybox-init variants means keeping parallel unit
trees keyed on
/etc/os-release.
Trust model
image pull (OCI path) delegates trust to the 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. Banger does not verify OCI
image signatures (cosign/sigstore) — users who care should verify
references out-of-band.