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) + } +}