From 48e3a938cf3038f83e3c60f1b40bbbffff7a146b Mon Sep 17 00:00:00 2001 From: Thales Maciel Date: Thu, 16 Apr 2026 14:25:50 -0300 Subject: [PATCH] Phase 2: image register --kernel-ref resolves through the catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `banger image register --kernel-ref ` 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 --- internal/api/types.go | 1 + internal/cli/banger.go | 6 ++- internal/daemon/images.go | 25 ++++++++++-- internal/daemon/kernels_test.go | 70 +++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 4 deletions(-) diff --git a/internal/api/types.go b/internal/api/types.go index 8d6c1aa..8a043be 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -259,6 +259,7 @@ type ImageRegisterParams struct { KernelPath string `json:"kernel_path,omitempty"` InitrdPath string `json:"initrd_path,omitempty"` ModulesDir string `json:"modules_dir,omitempty"` + KernelRef string `json:"kernel_ref,omitempty"` Docker bool `json:"docker,omitempty"` } diff --git a/internal/cli/banger.go b/internal/cli/banger.go index 7f93a2d..e37a5d5 100644 --- a/internal/cli/banger.go +++ b/internal/cli/banger.go @@ -1474,8 +1474,11 @@ func newImageRegisterCommand() *cobra.Command { cmd := &cobra.Command{ Use: "register", Short: "Register or update an unmanaged image", - Args: noArgsUsage("usage: banger image register --name --rootfs [--work-seed ] --kernel [--initrd ] [--modules ]"), + Args: noArgsUsage("usage: banger image register --name --rootfs [--work-seed ] (--kernel [--initrd ] [--modules ] | --kernel-ref )"), RunE: func(cmd *cobra.Command, args []string) error { + if strings.TrimSpace(params.KernelRef) != "" && (params.KernelPath != "" || params.InitrdPath != "" || params.ModulesDir != "") { + return errors.New("--kernel-ref is mutually exclusive with --kernel/--initrd/--modules") + } if err := absolutizeImageRegisterPaths(¶ms); err != nil { return err } @@ -1499,6 +1502,7 @@ func newImageRegisterCommand() *cobra.Command { cmd.Flags().StringVar(¶ms.KernelPath, "kernel", "", "kernel path") cmd.Flags().StringVar(¶ms.InitrdPath, "initrd", "", "initrd path") cmd.Flags().StringVar(¶ms.ModulesDir, "modules", "", "modules dir") + cmd.Flags().StringVar(¶ms.KernelRef, "kernel-ref", "", "name of a cataloged kernel (see 'banger kernel list')") cmd.Flags().BoolVar(¶ms.Docker, "docker", false, "mark image as docker-prepared") return cmd } diff --git a/internal/daemon/images.go b/internal/daemon/images.go index d724b76..bfc8448 100644 --- a/internal/daemon/images.go +++ b/internal/daemon/images.go @@ -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 or --kernel-ref )") + } if err := imagemgr.ValidateRegisterPaths(rootfsPath, workSeedPath, kernelPath, initrdPath, modulesDir); err != nil { return model.Image{}, err diff --git a/internal/daemon/kernels_test.go b/internal/daemon/kernels_test.go index 2ab9f03..369afef 100644 --- a/internal/daemon/kernels_test.go +++ b/internal/daemon/kernels_test.go @@ -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) + } +}