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

@ -179,7 +179,7 @@ func (d *Daemon) RegisterImage(ctx context.Context, params api.ImageRegisterPara
}
}
}
kernelPath, initrdPath, modulesDir, err := d.resolveKernelInputs(params.KernelRef, params.KernelPath, params.InitrdPath, params.ModulesDir)
kernelPath, initrdPath, modulesDir, err := d.resolveKernelInputs(ctx, params.KernelRef, params.KernelPath, params.InitrdPath, params.ModulesDir)
if err != nil {
return model.Image{}, err
}
@ -374,7 +374,10 @@ func firstNonEmpty(values ...string) string {
// resolveKernelInputs canonicalises user-supplied kernel info: either direct
// paths or a kernel-catalog ref. Shared by RegisterImage and PullImage.
func (d *Daemon) resolveKernelInputs(kernelRef, kernelPath, initrdPath, modulesDir string) (string, string, string, error) {
// When kernelRef is given but not yet pulled locally, an auto-pull from the
// embedded kernelcat catalog fires so the caller doesn't have to manage
// kernel/image ordering by hand.
func (d *Daemon) resolveKernelInputs(ctx context.Context, kernelRef, kernelPath, initrdPath, modulesDir string) (string, string, string, error) {
kernelRef = strings.TrimSpace(kernelRef)
kernelPath = strings.TrimSpace(kernelPath)
initrdPath = strings.TrimSpace(initrdPath)
@ -384,12 +387,9 @@ func (d *Daemon) resolveKernelInputs(kernelRef, kernelPath, initrdPath, modulesD
if kernelPath != "" || initrdPath != "" || modulesDir != "" {
return "", "", "", fmt.Errorf("--kernel-ref is mutually exclusive with --kernel/--initrd/--modules")
}
entry, err := kernelcat.ReadLocal(d.layout.KernelsDir, kernelRef)
entry, err := d.readOrAutoPullKernel(ctx, kernelRef)
if err != nil {
if os.IsNotExist(err) {
return "", "", "", fmt.Errorf("kernel %q not found in catalog; run 'banger kernel list' to see available entries", kernelRef)
}
return "", "", "", fmt.Errorf("resolve kernel %q: %w", kernelRef, err)
return "", "", "", err
}
return entry.KernelPath, entry.InitrdPath, entry.ModulesDir, nil
}
@ -399,3 +399,27 @@ func (d *Daemon) resolveKernelInputs(kernelRef, kernelPath, initrdPath, modulesD
}
return kernelPath, initrdPath, modulesDir, nil
}
// readOrAutoPullKernel tries the local kernelcat first; on miss, checks
// the embedded catalog and auto-pulls the bundle.
func (d *Daemon) readOrAutoPullKernel(ctx context.Context, kernelRef string) (kernelcat.Entry, error) {
entry, err := kernelcat.ReadLocal(d.layout.KernelsDir, kernelRef)
if err == nil {
return entry, nil
}
if !os.IsNotExist(err) {
return kernelcat.Entry{}, fmt.Errorf("resolve kernel %q: %w", kernelRef, err)
}
catalog, loadErr := kernelcat.LoadEmbedded()
if loadErr != nil {
return kernelcat.Entry{}, fmt.Errorf("kernel %q not found locally: %w", kernelRef, loadErr)
}
if _, lookupErr := catalog.Lookup(kernelRef); lookupErr != nil {
return kernelcat.Entry{}, fmt.Errorf("kernel %q not found in catalog; run 'banger kernel list --available' to browse", kernelRef)
}
vmCreateStage(ctx, "auto_pull_kernel", fmt.Sprintf("pulling kernel %s from catalog", kernelRef))
if _, pullErr := d.KernelPull(ctx, api.KernelPullParams{Name: kernelRef}); pullErr != nil {
return kernelcat.Entry{}, fmt.Errorf("auto-pull kernel %q: %w", kernelRef, pullErr)
}
return kernelcat.ReadLocal(d.layout.KernelsDir, kernelRef)
}