Phase 2: daemon PullImage orchestration

(d *Daemon).PullImage downloads an OCI image, flattens it into an
ext4 rootfs, and registers the result as a managed banger image.

Flow (internal/daemon/images_pull.go):
 1. Parse + validate the OCI ref via go-containerregistry/name.
 2. Derive a friendly default name from the ref ("debian-bookworm")
    when --name is omitted.
 3. Reject if an image with that name already exists.
 4. Resolve kernel info via the new shared resolveKernelInputs
    helper (refactored out of RegisterImage); ValidateKernelPaths
    checks the kernel triple alone.
 5. Acquire imageOpsMu, generate a fresh image id, and stage at
    <ImagesDir>/<id>.staging.
 6. imagepull.Pull → cache layers under OCICacheDir;
    imagepull.Flatten → temp rootfs tree under os.TempDir (so the
    state filesystem doesn't temporarily double in size).
 7. Default size: max(treeSize × 1.25, 1 GiB); --size override
    accepted.
 8. imagepull.BuildExt4 produces the rootfs.ext4 in the staging dir.
 9. imagemgr.StageBootArtifacts stages the kernel/initrd/modules
    into the same dir (reused unchanged).
 10. Atomic os.Rename(staging, finalDir) publishes the artifact dir.
 11. Persist model.Image with Managed=true. Failure at any step
     removes the staging dir; failure post-rename removes finalDir.

The pullAndFlatten field on Daemon is the test seam: tests stub it
to write a fixture tree into destDir and skip the real registry.

Refactor: extracted the "kernel-ref vs direct paths" resolution
out of RegisterImage into d.resolveKernelInputs so PullImage and
RegisterImage share one source of truth for that policy. Split
ValidateRegisterPaths into a kernel-only ValidateKernelPaths so
PullImage (which produces the rootfs itself) can validate just
the kernel triple without the rootfs check.

API: ImagePullParams { Ref, Name, KernelPath, InitrdPath,
ModulesDir, KernelRef, SizeBytes }. RPC dispatch case image.pull
mirrors image.register.

Tests cover: happy-path producing a managed image with all four
artifacts present + staging cleaned up, name-collision rejection,
missing-kernel rejection, and staging cleanup on a failed pull.
defaultImageNameFromRef handles tag/digest/no-suffix cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-04-16 17:27:32 -03:00
parent 78376ba6ec
commit a8c9983542
No known key found for this signature in database
GPG key ID: 33112E6833C34679
6 changed files with 467 additions and 25 deletions

View file

@ -179,29 +179,9 @@ func (d *Daemon) RegisterImage(ctx context.Context, params api.ImageRegisterPara
}
}
}
kernelPath := strings.TrimSpace(params.KernelPath)
initrdPath := strings.TrimSpace(params.InitrdPath)
modulesDir := strings.TrimSpace(params.ModulesDir)
kernelRef := strings.TrimSpace(params.KernelRef)
if kernelRef != "" {
if kernelPath != "" || initrdPath != "" || modulesDir != "" {
return model.Image{}, fmt.Errorf("--kernel-ref is mutually exclusive with --kernel/--initrd/--modules")
}
entry, err := kernelcat.ReadLocal(d.layout.KernelsDir, kernelRef)
if err != nil {
if os.IsNotExist(err) {
return model.Image{}, fmt.Errorf("kernel %q not found in catalog; run 'banger kernel list' to see available entries", kernelRef)
}
return model.Image{}, fmt.Errorf("resolve kernel %q: %w", kernelRef, err)
}
kernelPath = entry.KernelPath
initrdPath = entry.InitrdPath
modulesDir = entry.ModulesDir
}
if kernelPath == "" {
return model.Image{}, fmt.Errorf("kernel path is required (pass --kernel <path> or --kernel-ref <name>)")
kernelPath, initrdPath, modulesDir, err := d.resolveKernelInputs(params.KernelRef, params.KernelPath, params.InitrdPath, params.ModulesDir)
if err != nil {
return model.Image{}, err
}
if err := imagemgr.ValidateRegisterPaths(rootfsPath, workSeedPath, kernelPath, initrdPath, modulesDir); err != nil {
@ -391,3 +371,31 @@ func firstNonEmpty(values ...string) string {
}
return ""
}
// 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) {
kernelRef = strings.TrimSpace(kernelRef)
kernelPath = strings.TrimSpace(kernelPath)
initrdPath = strings.TrimSpace(initrdPath)
modulesDir = strings.TrimSpace(modulesDir)
if kernelRef != "" {
if kernelPath != "" || initrdPath != "" || modulesDir != "" {
return "", "", "", fmt.Errorf("--kernel-ref is mutually exclusive with --kernel/--initrd/--modules")
}
entry, err := kernelcat.ReadLocal(d.layout.KernelsDir, 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 entry.KernelPath, entry.InitrdPath, entry.ModulesDir, nil
}
if kernelPath == "" {
return "", "", "", fmt.Errorf("kernel path is required (pass --kernel <path> or --kernel-ref <name>)")
}
return kernelPath, initrdPath, modulesDir, nil
}