image: add banger image cache prune for OCI cache cleanup

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>
This commit is contained in:
Thales Maciel 2026-04-28 16:32:57 -03:00
parent 182bccf8af
commit 4d8dca6b72
No known key found for this signature in database
GPG key ID: 33112E6833C34679
7 changed files with 360 additions and 9 deletions

View file

@ -75,12 +75,13 @@ var rpcHandlers = map[string]handler{
"vm.workspace.prepare": paramHandler(workspacePrepareDispatch),
"vm.workspace.export": paramHandler(workspaceExportDispatch),
"image.list": noParamHandler(imageListDispatch),
"image.show": paramHandler(imageShowDispatch),
"image.register": paramHandler(imageRegisterDispatch),
"image.promote": paramHandler(imagePromoteDispatch),
"image.delete": paramHandler(imageDeleteDispatch),
"image.pull": paramHandler(imagePullDispatch),
"image.list": noParamHandler(imageListDispatch),
"image.show": paramHandler(imageShowDispatch),
"image.register": paramHandler(imageRegisterDispatch),
"image.promote": paramHandler(imagePromoteDispatch),
"image.delete": paramHandler(imageDeleteDispatch),
"image.pull": paramHandler(imagePullDispatch),
"image.cache.prune": paramHandler(imageCachePruneDispatch),
"kernel.list": noParamHandler(kernelListDispatch),
"kernel.show": paramHandler(kernelShowDispatch),
@ -209,6 +210,10 @@ func imagePullDispatch(ctx context.Context, d *Daemon, p api.ImagePullParams) (a
return api.ImageShowResult{Image: image}, err
}
func imageCachePruneDispatch(ctx context.Context, d *Daemon, p api.ImageCachePruneParams) (api.ImageCachePruneResult, error) {
return d.img.PruneOCICache(ctx, p)
}
func kernelListDispatch(ctx context.Context, d *Daemon) (api.KernelListResult, error) {
return d.img.KernelList(ctx)
}