vm create: auto-pull image and kernel from catalogs if missing

One-command sandbox: `banger vm run` on a fresh host now Just Works.
No prior `banger image pull` or `banger kernel pull` needed.

Changes:

- Default `default_image_name` flips from "default" to "debian-bookworm"
  so the golden image is the implicit target when `--image` is omitted.
- `CreateVM` resolves the image via a new `findOrAutoPullImage`: try
  the local store first, and on miss fall back to the embedded imagecat
  catalog + auto-pull. Emits a vm-create progress stage so the user
  sees "pulling from image catalog" in the create output.
- `resolveKernelInputs` gains context + the same pattern via
  `readOrAutoPullKernel`: try the local kernelcat, and on miss look up
  the embedded kernelcat and auto-pull. Fires whenever a bundle's
  manifest references a kernel the user hasn't pulled yet, not just
  during image pull — any CreateVM with an image that needs a kernel
  not yet local will resolve it.
- `--image` help text updated on both `vm run` and `vm create`.

Six tests cover local-hit-no-pull, auto-pull-on-miss, not-in-catalog
error propagation, and a non-ENOENT kernel read error does NOT trigger
a misleading "not in catalog" claim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-04-18 15:10:26 -03:00
parent 81a27d6648
commit e0894376ea
No known key found for this signature in database
GPG key ID: 33112E6833C34679
7 changed files with 202 additions and 15 deletions

View file

@ -8,6 +8,7 @@ import (
"strings"
"banger/internal/api"
"banger/internal/imagecat"
"banger/internal/model"
"banger/internal/vmdns"
)
@ -35,7 +36,7 @@ func (d *Daemon) CreateVM(ctx context.Context, params api.VMCreateParams) (vm mo
imageName = d.config.DefaultImageName
}
vmCreateStage(ctx, "resolve_image", "resolving image")
image, err := d.FindImage(ctx, imageName)
image, err := d.findOrAutoPullImage(ctx, imageName)
if err != nil {
return model.VMRecord{}, err
}
@ -129,3 +130,29 @@ func (d *Daemon) CreateVM(ctx context.Context, params api.VMCreateParams) (vm mo
}
return d.startVMLocked(ctx, vm, image)
}
// findOrAutoPullImage tries the local image store first; if the name
// isn't registered but matches an entry in the embedded imagecat
// catalog, it auto-pulls the bundle so `vm create --image foo` (and
// therefore `vm run`) works on a fresh host without the user having
// to run `image pull` first.
func (d *Daemon) findOrAutoPullImage(ctx context.Context, idOrName string) (model.Image, error) {
image, err := d.FindImage(ctx, idOrName)
if err == nil {
return image, nil
}
catalog, loadErr := imagecat.LoadEmbedded()
if loadErr != nil {
return model.Image{}, err
}
entry, lookupErr := catalog.Lookup(idOrName)
if lookupErr != nil {
// Not in the catalog either — surface the original not-found.
return model.Image{}, err
}
vmCreateStage(ctx, "auto_pull_image", fmt.Sprintf("pulling %s from image catalog", entry.Name))
if _, pullErr := d.PullImage(ctx, api.ImagePullParams{Ref: entry.Name}); pullErr != nil {
return model.Image{}, fmt.Errorf("auto-pull image %q: %w", entry.Name, pullErr)
}
return d.FindImage(ctx, idOrName)
}