package cli import ( "errors" "fmt" "strings" "banger/internal/api" "banger/internal/model" "banger/internal/rpc" "banger/internal/system" "github.com/spf13/cobra" ) func (d *deps) newImageCommand() *cobra.Command { cmd := &cobra.Command{ Use: "image", Short: "Manage images", RunE: helpNoArgs, } cmd.AddCommand( d.newImageRegisterCommand(), d.newImagePullCommand(), d.newImagePromoteCommand(), d.newImageListCommand(), d.newImageShowCommand(), d.newImageDeleteCommand(), ) return cmd } func (d *deps) newImageRegisterCommand() *cobra.Command { var params api.ImageRegisterParams 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 ] | --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 } if err := system.EnsureSudo(cmd.Context()); err != nil { return err } layout, _, err := d.ensureDaemon(cmd.Context()) if err != nil { return err } result, err := rpc.Call[api.ImageShowResult](cmd.Context(), layout.SocketPath, "image.register", params) if err != nil { return err } return printImageSummary(cmd.OutOrStdout(), result.Image) }, } cmd.Flags().StringVar(¶ms.Name, "name", "", "image name") cmd.Flags().StringVar(¶ms.RootfsPath, "rootfs", "", "rootfs path") cmd.Flags().StringVar(¶ms.WorkSeedPath, "work-seed", "", "work-seed path") 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") _ = cmd.RegisterFlagCompletionFunc("kernel-ref", d.completeKernelNames) return cmd } func (d *deps) newImagePullCommand() *cobra.Command { var ( params api.ImagePullParams sizeRaw string ) cmd := &cobra.Command{ Use: "pull ", Short: "Pull an image bundle (catalog name) or OCI image and register it", Long: strings.TrimSpace(` Pull an image into banger. Two paths: • Catalog name (e.g. 'debian-bookworm') Fetches a pre-built bundle from the embedded imagecat catalog. Kernel-ref comes from the catalog entry; --kernel-ref still overrides. • OCI reference (e.g. 'docker.io/library/debian:bookworm') Pulls the image, flattens its layers, fixes ownership, injects banger's guest agents. --kernel-ref or direct --kernel/--initrd/ --modules are required. Use 'banger image catalog' to see available catalog names (once that subcommand lands). `), Example: strings.TrimSpace(` banger image pull debian-bookworm banger image pull debian-bookworm --name sandbox banger image pull docker.io/library/debian:bookworm --kernel-ref generic-6.12 `), Args: exactArgsUsage(1, "usage: banger image pull [--name ] [--kernel-ref ] [--kernel ] [--initrd ] [--modules ] [--size ]"), RunE: func(cmd *cobra.Command, args []string) error { params.Ref = args[0] if strings.TrimSpace(params.KernelRef) != "" && (params.KernelPath != "" || params.InitrdPath != "" || params.ModulesDir != "") { return errors.New("--kernel-ref is mutually exclusive with --kernel/--initrd/--modules") } if strings.TrimSpace(sizeRaw) != "" { size, err := model.ParseSize(sizeRaw) if err != nil { return fmt.Errorf("--size: %w", err) } params.SizeBytes = size } if err := absolutizePaths(¶ms.KernelPath, ¶ms.InitrdPath, ¶ms.ModulesDir); err != nil { return err } if err := system.EnsureSudo(cmd.Context()); err != nil { return err } layout, _, err := d.ensureDaemon(cmd.Context()) if err != nil { return err } var result api.ImageShowResult err = withHeartbeat(cmd.ErrOrStderr(), "image pull", func() error { var callErr error result, callErr = rpc.Call[api.ImageShowResult](cmd.Context(), layout.SocketPath, "image.pull", params) return callErr }) if err != nil { return err } return printImageSummary(cmd.OutOrStdout(), result.Image) }, } cmd.Flags().StringVar(¶ms.Name, "name", "", "image name (defaults to the ref's repo+tag, sanitised)") 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().StringVar(&sizeRaw, "size", "", "ext4 image size (e.g. 4GiB); defaults to content + 25%, min 1GiB") _ = cmd.RegisterFlagCompletionFunc("kernel-ref", d.completeKernelNames) return cmd } func (d *deps) newImagePromoteCommand() *cobra.Command { return &cobra.Command{ Use: "promote ", Short: "Promote an unmanaged image to a managed artifact", Args: exactArgsUsage(1, "usage: banger image promote "), ValidArgsFunction: d.completeImageNameOnlyAtPos0, RunE: func(cmd *cobra.Command, args []string) error { if err := system.EnsureSudo(cmd.Context()); err != nil { return err } layout, _, err := d.ensureDaemon(cmd.Context()) if err != nil { return err } result, err := rpc.Call[api.ImageShowResult](cmd.Context(), layout.SocketPath, "image.promote", api.ImageRefParams{IDOrName: args[0]}) if err != nil { return err } return printImageSummary(cmd.OutOrStdout(), result.Image) }, } } func (d *deps) newImageListCommand() *cobra.Command { return &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "List images", Args: noArgsUsage("usage: banger image list"), RunE: func(cmd *cobra.Command, args []string) error { layout, _, err := d.ensureDaemon(cmd.Context()) if err != nil { return err } result, err := rpc.Call[api.ImageListResult](cmd.Context(), layout.SocketPath, "image.list", api.Empty{}) if err != nil { return err } return printImageListTable(cmd.OutOrStdout(), result.Images) }, } } func (d *deps) newImageShowCommand() *cobra.Command { return &cobra.Command{ Use: "show ", Short: "Show image details", Args: exactArgsUsage(1, "usage: banger image show "), ValidArgsFunction: d.completeImageNameOnlyAtPos0, RunE: func(cmd *cobra.Command, args []string) error { layout, _, err := d.ensureDaemon(cmd.Context()) if err != nil { return err } result, err := rpc.Call[api.ImageShowResult](cmd.Context(), layout.SocketPath, "image.show", api.ImageRefParams{IDOrName: args[0]}) if err != nil { return err } return printJSON(cmd.OutOrStdout(), result.Image) }, } } func (d *deps) newImageDeleteCommand() *cobra.Command { return &cobra.Command{ Use: "delete ", Aliases: []string{"rm"}, Short: "Delete an image", Args: exactArgsUsage(1, "usage: banger image delete "), ValidArgsFunction: d.completeImageNameOnlyAtPos0, RunE: func(cmd *cobra.Command, args []string) error { if err := system.EnsureSudo(cmd.Context()); err != nil { return err } layout, _, err := d.ensureDaemon(cmd.Context()) if err != nil { return err } result, err := rpc.Call[api.ImageShowResult](cmd.Context(), layout.SocketPath, "image.delete", api.ImageRefParams{IDOrName: args[0]}) if err != nil { return err } return printImageSummary(cmd.OutOrStdout(), result.Image) }, } }