Move the supported systemd path to two services: an owner-user bangerd for orchestration and a narrow root helper for bridge/tap, NAT/resolver, dm/loop, and Firecracker ownership. This removes repeated sudo from daily vm and image flows without leaving the general daemon running as root. Add install metadata, system install/status/restart/uninstall commands, and a system-owned runtime layout. Keep user SSH/config material in the owner home, lock file_sync to the owner home, and move daemon known_hosts handling out of the old root-owned control path. Route privileged lifecycle steps through typed privilegedOps calls, harden the two systemd units, and rewrite smoke plus docs around the supported service model. Verified with make build, make test, make lint, and make smoke on the supported systemd host path.
218 lines
7.7 KiB
Go
218 lines
7.7 KiB
Go
package cli
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"banger/internal/api"
|
|
"banger/internal/model"
|
|
"banger/internal/rpc"
|
|
|
|
"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 <name> --rootfs <path> [--work-seed <path>] (--kernel <path> [--initrd <path>] [--modules <dir>] | --kernel-ref <name>)"),
|
|
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
|
|
}
|
|
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 <name-or-oci-ref>",
|
|
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-or-oci-ref> [--name <name>] [--kernel-ref <name>] [--kernel <path>] [--initrd <path>] [--modules <dir>] [--size <human>]"),
|
|
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
|
|
}
|
|
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 <id-or-name>",
|
|
Short: "Promote an unmanaged image to a managed artifact",
|
|
Args: exactArgsUsage(1, "usage: banger image promote <id-or-name>"),
|
|
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.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 <id-or-name>",
|
|
Short: "Show image details",
|
|
Args: exactArgsUsage(1, "usage: banger image show <id-or-name>"),
|
|
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 <id-or-name>",
|
|
Aliases: []string{"rm"},
|
|
Short: "Delete an image",
|
|
Args: exactArgsUsage(1, "usage: banger image delete <id-or-name>"),
|
|
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.delete", api.ImageRefParams{IDOrName: args[0]})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return printImageSummary(cmd.OutOrStdout(), result.Image)
|
|
},
|
|
}
|
|
}
|