banger/internal/cli/commands_kernel.go
Thales Maciel 003b0488ce
cli,docs: trivial polish for v0.1.0
A pre-release audit collected ~12 trivial-effort UX and code-hygiene
items. Rolling them up here so the v0.1.0 commit log isn't littered
with one-line tweaks.

CLI help / completion:
  * commands_image.go: drop dangling reference to a `banger image
    catalog` subcommand that doesn't exist; replace with a pointer
    to `banger image list`.
  * commands_image.go: --size flag example was "4GiB" but the parser
    rejects that suffix. Change example to "4G". (Parser-side fix
    is in a separate concern.)
  * commands_image.go + completion.go: image pull now wires a
    catalog completer (falls back to local image names since there's
    no image-catalog RPC yet); image show / delete / promote already
    completed local names.
  * commands_kernel.go + completion.go: kernel pull now wires a new
    completeKernelCatalogNameOnlyAtPos0 backed by the kernel.catalog
    RPC, so tab-complete suggests pullable kernels.
  * commands_vm.go: vm stats and vm set now have Long + Example
    blocks (peers all do); --from flag description updated to spell
    out the relationship to --branch.

README:
  * Define "golden image" inline at first use.
  * Add a one-line Requirements block above Quick Start so users
    hit the firecracker / KVM dependency before `make build`.

Code hygiene:
  * dashIfEmpty / emptyDash were the same function. Deleted
    emptyDash, retargeted three call sites.
  * formatBytes (introduced today in image cache prune) duplicated
    humanSize. Consolidated to humanSize, now with a space ("1.2
    GiB" not "1.2GiB"). formatters_test.go expectations updated.

Logging chattiness:
  * "operation started" (logger.go), "daemon request canceled"
    (daemon.go), and "helper rpc completed" (roothelper.go) all
    fired at INFO per RPC. Downgraded to DEBUG so routine shell
    completions don't spam syslog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:31:54 -03:00

185 lines
6.2 KiB
Go

package cli
import (
"errors"
"fmt"
"path/filepath"
"strings"
"banger/internal/api"
"banger/internal/rpc"
"github.com/spf13/cobra"
)
func (d *deps) newKernelCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "kernel",
Short: "Pull and manage Firecracker-compatible kernels",
Long: strings.TrimSpace(`
Banger boots guests with a separate kernel artifact (vmlinux, plus
optional initrd + modules). Kernels are tracked by name in a local
catalog so multiple images can share one.
Most users never run these commands directly: 'banger image pull'
auto-pulls the kernel referenced by the catalog entry. Use these
commands when you want to inspect what's installed, switch a VM to
a different kernel via 'image register --kernel-ref', or import a
kernel built locally with scripts/make-*-kernel.sh.
Subcommands:
pull download a cataloged kernel by name
list show what's installed (or --available for the catalog)
show inspect one entry as JSON
rm remove a local kernel
import register a kernel built from scripts/make-*-kernel.sh
`),
Example: strings.TrimSpace(`
banger kernel list --available
banger kernel pull generic-6.12
banger kernel import void-kernel --from build/manual/void-kernel
`),
RunE: helpNoArgs,
}
cmd.AddCommand(
d.newKernelListCommand(),
d.newKernelShowCommand(),
d.newKernelRmCommand(),
d.newKernelImportCommand(),
d.newKernelPullCommand(),
)
return cmd
}
func (d *deps) newKernelPullCommand() *cobra.Command {
var force bool
cmd := &cobra.Command{
Use: "pull <name>",
Short: "Download a cataloged kernel bundle",
Args: exactArgsUsage(1, "usage: banger kernel pull <name> [--force]"),
ValidArgsFunction: d.completeKernelCatalogNameOnlyAtPos0,
RunE: func(cmd *cobra.Command, args []string) error {
layout, _, err := d.ensureDaemon(cmd.Context())
if err != nil {
return err
}
var result api.KernelShowResult
err = withHeartbeat(cmd.ErrOrStderr(), "kernel pull", func() error {
var callErr error
result, callErr = rpc.Call[api.KernelShowResult](cmd.Context(), layout.SocketPath, "kernel.pull", api.KernelPullParams{Name: args[0], Force: force})
return callErr
})
if err != nil {
return err
}
return printJSON(cmd.OutOrStdout(), result.Entry)
},
}
cmd.Flags().BoolVar(&force, "force", false, "re-pull even if already present")
return cmd
}
func (d *deps) newKernelImportCommand() *cobra.Command {
var params api.KernelImportParams
cmd := &cobra.Command{
Use: "import <name>",
Short: "Import a kernel bundle produced by scripts/make-*-kernel.sh",
Long: "Copy the kernel, optional initrd, and optional modules directory from <from> into the local kernel catalog keyed by <name>. <from> is usually build/manual/void-kernel or build/manual/alpine-kernel.",
Args: exactArgsUsage(1, "usage: banger kernel import <name> --from <dir>"),
RunE: func(cmd *cobra.Command, args []string) error {
params.Name = args[0]
if strings.TrimSpace(params.FromDir) == "" {
return errors.New("--from <dir> is required")
}
abs, err := filepath.Abs(params.FromDir)
if err != nil {
return err
}
params.FromDir = abs
layout, _, err := d.ensureDaemon(cmd.Context())
if err != nil {
return err
}
result, err := rpc.Call[api.KernelShowResult](cmd.Context(), layout.SocketPath, "kernel.import", params)
if err != nil {
return err
}
return printJSON(cmd.OutOrStdout(), result.Entry)
},
}
cmd.Flags().StringVar(&params.FromDir, "from", "", "directory produced by make-*-kernel.sh (e.g. build/manual/void-kernel)")
cmd.Flags().StringVar(&params.Distro, "distro", "", "distribution label stored in the manifest (e.g. void, alpine)")
cmd.Flags().StringVar(&params.Arch, "arch", "", "architecture label stored in the manifest (e.g. x86_64)")
return cmd
}
func (d *deps) newKernelListCommand() *cobra.Command {
var available bool
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List kernels (local by default, or --available for the catalog)",
Args: noArgsUsage("usage: banger kernel list [--available]"),
RunE: func(cmd *cobra.Command, args []string) error {
layout, _, err := d.ensureDaemon(cmd.Context())
if err != nil {
return err
}
if available {
result, err := rpc.Call[api.KernelCatalogResult](cmd.Context(), layout.SocketPath, "kernel.catalog", api.Empty{})
if err != nil {
return err
}
return printKernelCatalogTable(cmd.OutOrStdout(), result.Entries)
}
result, err := rpc.Call[api.KernelListResult](cmd.Context(), layout.SocketPath, "kernel.list", api.Empty{})
if err != nil {
return err
}
return printKernelListTable(cmd.OutOrStdout(), result.Entries)
},
}
cmd.Flags().BoolVar(&available, "available", false, "show the built-in catalog (with pulled/available status) instead of local entries")
return cmd
}
func (d *deps) newKernelShowCommand() *cobra.Command {
return &cobra.Command{
Use: "show <name>",
Short: "Show kernel catalog entry details",
Args: exactArgsUsage(1, "usage: banger kernel show <name>"),
ValidArgsFunction: d.completeKernelNameOnlyAtPos0,
RunE: func(cmd *cobra.Command, args []string) error {
layout, _, err := d.ensureDaemon(cmd.Context())
if err != nil {
return err
}
result, err := rpc.Call[api.KernelShowResult](cmd.Context(), layout.SocketPath, "kernel.show", api.KernelRefParams{Name: args[0]})
if err != nil {
return err
}
return printJSON(cmd.OutOrStdout(), result.Entry)
},
}
}
func (d *deps) newKernelRmCommand() *cobra.Command {
return &cobra.Command{
Use: "rm <name>",
Aliases: []string{"remove", "delete"},
Short: "Remove a kernel catalog entry",
Args: exactArgsUsage(1, "usage: banger kernel rm <name>"),
ValidArgsFunction: d.completeKernelNameOnlyAtPos0,
RunE: func(cmd *cobra.Command, args []string) error {
layout, _, err := d.ensureDaemon(cmd.Context())
if err != nil {
return err
}
if _, err := rpc.Call[api.Empty](cmd.Context(), layout.SocketPath, "kernel.delete", api.KernelRefParams{Name: args[0]}); err != nil {
return err
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "removed %s\n", args[0])
return err
},
}
}