banger/internal/daemon/imagemgr/paths.go
Thales Maciel a8c9983542
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>
2026-04-16 17:27:32 -03:00

120 lines
4.6 KiB
Go

// Package imagemgr contains the pure helpers of the banger image subsystem:
// path validators, artifact staging, managed-image metadata, and the guest
// provisioning script generator used by image build.
//
// The orchestrator methods (BuildImage, RegisterImage, PromoteImage,
// DeleteImage) still live in the daemon package and compose these helpers.
package imagemgr
import (
"context"
"os"
"path/filepath"
"strings"
"banger/internal/imagepreset"
"banger/internal/system"
)
// ValidateRegisterPaths checks that rootfs + kernel exist and that optional
// artifacts, when provided, also exist.
func ValidateRegisterPaths(rootfsPath, workSeedPath, kernelPath, initrdPath, modulesDir string) error {
checks := system.NewPreflight()
checks.RequireFile(rootfsPath, "rootfs image", `pass --rootfs <path>`)
if workSeedPath != "" {
checks.RequireFile(workSeedPath, "work-seed image", `pass --work-seed <path> or rebuild the image with a work seed`)
}
addKernelChecks(checks, kernelPath, initrdPath, modulesDir)
return checks.Err("image register failed")
}
// ValidateKernelPaths checks the kernel triple alone, used by flows
// (e.g. image pull) that produce the rootfs themselves.
func ValidateKernelPaths(kernelPath, initrdPath, modulesDir string) error {
checks := system.NewPreflight()
addKernelChecks(checks, kernelPath, initrdPath, modulesDir)
return checks.Err("kernel preflight failed")
}
func addKernelChecks(checks *system.Preflight, kernelPath, initrdPath, modulesDir string) {
checks.RequireFile(kernelPath, "kernel image", `pass --kernel <path>`)
if initrdPath != "" {
checks.RequireFile(initrdPath, "initrd image", `pass --initrd <path>`)
}
if modulesDir != "" {
checks.RequireDir(modulesDir, "kernel modules dir", `pass --modules <dir>`)
}
}
// ValidatePromotePaths checks that an existing registered image's artifacts
// are still present before promoting it to daemon-owned storage.
func ValidatePromotePaths(rootfsPath, kernelPath, initrdPath, modulesDir string) error {
checks := system.NewPreflight()
checks.RequireFile(rootfsPath, "rootfs image", `re-register the image with a valid rootfs`)
checks.RequireFile(kernelPath, "kernel image", `re-register the image with a valid kernel`)
if initrdPath != "" {
checks.RequireFile(initrdPath, "initrd image", `re-register the image with a valid initrd`)
}
if modulesDir != "" {
checks.RequireDir(modulesDir, "kernel modules dir", `re-register the image with a valid modules dir`)
}
return checks.Err("image promote failed")
}
// StageBootArtifacts copies kernel/initrd/modules into artifactDir and
// returns the staged paths. initrd and modules are optional; an empty source
// returns an empty staged path.
func StageBootArtifacts(ctx context.Context, runner system.CommandRunner, artifactDir, kernelSource, initrdSource, modulesSource string) (string, string, string, error) {
kernelPath := filepath.Join(artifactDir, "kernel")
if err := system.CopyFilePreferClone(kernelSource, kernelPath); err != nil {
return "", "", "", err
}
initrdPath := ""
if strings.TrimSpace(initrdSource) != "" {
initrdPath = filepath.Join(artifactDir, "initrd.img")
if err := system.CopyFilePreferClone(initrdSource, initrdPath); err != nil {
return "", "", "", err
}
}
modulesDir := ""
if strings.TrimSpace(modulesSource) != "" {
modulesDir = filepath.Join(artifactDir, "modules")
if err := os.MkdirAll(modulesDir, 0o755); err != nil {
return "", "", "", err
}
if err := system.CopyDirContents(ctx, runner, modulesSource, modulesDir, false); err != nil {
return "", "", "", err
}
}
return kernelPath, initrdPath, modulesDir, nil
}
// StageOptionalArtifactPath returns the destination path for an optional
// artifact in artifactDir, or "" when stagedPath is empty (artifact absent).
func StageOptionalArtifactPath(artifactDir, stagedPath, name string) string {
if strings.TrimSpace(stagedPath) == "" {
return ""
}
return filepath.Join(artifactDir, name)
}
// BuildMetadataPackages returns the canonical package set recorded for a
// managed image build. The #feature:docker sentinel is appended when
// docker is requested.
func BuildMetadataPackages(docker bool) []string {
packages := imagepreset.DebianBasePackages()
if docker {
packages = append(packages, "#feature:docker")
}
return packages
}
// WritePackagesMetadata writes the hash of packages next to rootfsPath so
// future builds can detect drift. Empty packages or rootfsPath is a no-op.
func WritePackagesMetadata(rootfsPath string, packages []string) error {
if rootfsPath == "" || len(packages) == 0 {
return nil
}
metadataPath := rootfsPath + ".packages.sha256"
return os.WriteFile(metadataPath, []byte(imagepreset.Hash(packages)+"\n"), 0o644)
}