Phase 2: image register --kernel-ref resolves through the catalog

`banger image register --kernel-ref <name>` now substitutes for the
--kernel/--initrd/--modules triple. The daemon looks the name up via
kernelcat.ReadLocal under d.layout.KernelsDir, populates the three
paths from the resolved entry, then continues through the existing
validate/persist flow unchanged.

Passing both --kernel-ref and any of --kernel/--initrd/--modules is
rejected — at the CLI layer (before starting the daemon) and
defensively at the RPC layer. A missing catalog entry produces a clear
"run 'banger kernel list'" message.

Once registered, the image stores the resolved absolute paths, so
deleting the catalog entry later does not invalidate already-registered
images — managed image build still copies the kernel into its artifact
dir per imagemgr.StageBootArtifacts.

Tests cover: resolution success (absolute KernelPath populated from
catalog), mutual-exclusion rejection, and missing-entry error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-04-16 14:25:50 -03:00
parent 83cc3aee15
commit 48e3a938cf
No known key found for this signature in database
GPG key ID: 33112E6833C34679
4 changed files with 98 additions and 4 deletions

View file

@ -12,6 +12,7 @@ import (
"banger/internal/api"
"banger/internal/daemon/imagemgr"
"banger/internal/imagepreset"
"banger/internal/kernelcat"
"banger/internal/model"
"banger/internal/system"
)
@ -179,11 +180,29 @@ func (d *Daemon) RegisterImage(ctx context.Context, params api.ImageRegisterPara
}
}
kernelPath := strings.TrimSpace(params.KernelPath)
if kernelPath == "" {
return model.Image{}, fmt.Errorf("kernel path is required")
}
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>)")
}
if err := imagemgr.ValidateRegisterPaths(rootfsPath, workSeedPath, kernelPath, initrdPath, modulesDir); err != nil {
return model.Image{}, err

View file

@ -97,3 +97,73 @@ func TestKernelDeleteRejectsInvalidName(t *testing.T) {
t.Fatalf("KernelDelete should reject traversal")
}
}
func TestRegisterImageResolvesKernelRef(t *testing.T) {
kernelsDir := t.TempDir()
seedKernelEntry(t, kernelsDir, "void-6.12")
rootfs := filepath.Join(t.TempDir(), "rootfs.ext4")
if err := os.WriteFile(rootfs, []byte("rootfs"), 0o644); err != nil {
t.Fatalf("write rootfs: %v", err)
}
d := &Daemon{
layout: paths.Layout{KernelsDir: kernelsDir},
store: openDaemonStore(t),
}
image, err := d.RegisterImage(context.Background(), api.ImageRegisterParams{
Name: "testbox",
RootfsPath: rootfs,
KernelRef: "void-6.12",
})
if err != nil {
t.Fatalf("RegisterImage: %v", err)
}
want := filepath.Join(kernelsDir, "void-6.12", "vmlinux")
if image.KernelPath != want {
t.Fatalf("image.KernelPath = %q, want %q", image.KernelPath, want)
}
}
func TestRegisterImageRejectsKernelRefAndPath(t *testing.T) {
kernelsDir := t.TempDir()
seedKernelEntry(t, kernelsDir, "void-6.12")
rootfs := filepath.Join(t.TempDir(), "rootfs.ext4")
if err := os.WriteFile(rootfs, []byte("rootfs"), 0o644); err != nil {
t.Fatal(err)
}
d := &Daemon{
layout: paths.Layout{KernelsDir: kernelsDir},
store: openDaemonStore(t),
}
_, err := d.RegisterImage(context.Background(), api.ImageRegisterParams{
Name: "testbox",
RootfsPath: rootfs,
KernelRef: "void-6.12",
KernelPath: "/some/other/vmlinux",
})
if err == nil || !strings.Contains(err.Error(), "mutually exclusive") {
t.Fatalf("RegisterImage kernel-ref+kernel: err=%v, want mutually-exclusive error", err)
}
}
func TestRegisterImageMissingKernelRef(t *testing.T) {
rootfs := filepath.Join(t.TempDir(), "rootfs.ext4")
if err := os.WriteFile(rootfs, []byte("rootfs"), 0o644); err != nil {
t.Fatal(err)
}
d := &Daemon{
layout: paths.Layout{KernelsDir: t.TempDir()},
store: openDaemonStore(t),
}
_, err := d.RegisterImage(context.Background(), api.ImageRegisterParams{
Name: "testbox",
RootfsPath: rootfs,
KernelRef: "never-imported",
})
if err == nil || !strings.Contains(err.Error(), "not found in catalog") {
t.Fatalf("RegisterImage missing kernel-ref: err=%v", err)
}
}