cli: split banger.go god file into focused files
Pure code motion — banger.go 3508→240 LOC, same-package decomposition keeps all identifiers visible without export changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3a5f4cd40d
commit
3f6ecb4376
12 changed files with 3478 additions and 3268 deletions
441
internal/cli/commands_internal.go
Normal file
441
internal/cli/commands_internal.go
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"banger/internal/config"
|
||||
"banger/internal/hostnat"
|
||||
"banger/internal/imagecat"
|
||||
"banger/internal/imagepull"
|
||||
"banger/internal/model"
|
||||
"banger/internal/paths"
|
||||
"banger/internal/system"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newInternalCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "internal",
|
||||
Hidden: true,
|
||||
RunE: helpNoArgs,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newInternalNATCommand(),
|
||||
newInternalWorkSeedCommand(),
|
||||
newInternalSSHKeyPathCommand(),
|
||||
newInternalFirecrackerPathCommand(),
|
||||
newInternalVSockAgentPathCommand(),
|
||||
newInternalMakeBundleCommand(),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newInternalSSHKeyPathCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "ssh-key-path",
|
||||
Hidden: true,
|
||||
Args: noArgsUsage("usage: banger internal ssh-key-path"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
layout, err := paths.Resolve()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg, err := config.Load(layout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprintln(cmd.OutOrStdout(), cfg.SSHKeyPath)
|
||||
return err
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newInternalFirecrackerPathCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "firecracker-path",
|
||||
Hidden: true,
|
||||
Args: noArgsUsage("usage: banger internal firecracker-path"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
layout, err := paths.Resolve()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg, err := config.Load(layout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(cfg.FirecrackerBin) == "" {
|
||||
return errors.New("firecracker binary not configured; install firecracker or set firecracker_bin")
|
||||
}
|
||||
_, err = fmt.Fprintln(cmd.OutOrStdout(), cfg.FirecrackerBin)
|
||||
return err
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newInternalVSockAgentPathCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "vsock-agent-path",
|
||||
Hidden: true,
|
||||
Args: noArgsUsage("usage: banger internal vsock-agent-path"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
path, err := paths.CompanionBinaryPath("banger-vsock-agent")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprintln(cmd.OutOrStdout(), path)
|
||||
return err
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newInternalMakeBundleCommand() *cobra.Command {
|
||||
var (
|
||||
rootfsTarPath string
|
||||
name string
|
||||
distro string
|
||||
arch string
|
||||
kernelRef string
|
||||
description string
|
||||
sizeSpec string
|
||||
outPath string
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "make-bundle",
|
||||
Hidden: true,
|
||||
Short: "Build a banger image bundle (.tar.zst) from a flat rootfs tar",
|
||||
Args: noArgsUsage("usage: banger internal make-bundle --rootfs-tar <file|-> --name <n> --out <bundle.tar.zst>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runInternalMakeBundle(cmd, internalMakeBundleOpts{
|
||||
rootfsTarPath: rootfsTarPath,
|
||||
name: name,
|
||||
distro: distro,
|
||||
arch: arch,
|
||||
kernelRef: kernelRef,
|
||||
description: description,
|
||||
sizeSpec: sizeSpec,
|
||||
outPath: outPath,
|
||||
})
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&rootfsTarPath, "rootfs-tar", "", "flat rootfs tar file, or '-' for stdin")
|
||||
cmd.Flags().StringVar(&name, "name", "", "bundle name (filesystem-safe identifier)")
|
||||
cmd.Flags().StringVar(&distro, "distro", "", "distro label (e.g. debian)")
|
||||
cmd.Flags().StringVar(&arch, "arch", "x86_64", "architecture label")
|
||||
cmd.Flags().StringVar(&kernelRef, "kernel-ref", "", "kernelcat entry name this image pairs with")
|
||||
cmd.Flags().StringVar(&description, "description", "", "short description")
|
||||
cmd.Flags().StringVar(&sizeSpec, "size", "", "rootfs ext4 size (e.g. 4G); defaults to tree size + 25%")
|
||||
cmd.Flags().StringVar(&outPath, "out", "", "output bundle path (.tar.zst)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
type internalMakeBundleOpts struct {
|
||||
rootfsTarPath string
|
||||
name string
|
||||
distro string
|
||||
arch string
|
||||
kernelRef string
|
||||
description string
|
||||
sizeSpec string
|
||||
outPath string
|
||||
}
|
||||
|
||||
func runInternalMakeBundle(cmd *cobra.Command, opts internalMakeBundleOpts) error {
|
||||
if err := imagecat.ValidateName(opts.name); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(opts.rootfsTarPath) == "" {
|
||||
return errors.New("--rootfs-tar is required")
|
||||
}
|
||||
if strings.TrimSpace(opts.outPath) == "" {
|
||||
return errors.New("--out is required")
|
||||
}
|
||||
if strings.TrimSpace(opts.arch) == "" {
|
||||
opts.arch = "x86_64"
|
||||
}
|
||||
|
||||
var sizeBytes int64
|
||||
if s := strings.TrimSpace(opts.sizeSpec); s != "" {
|
||||
n, err := model.ParseSize(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse --size: %w", err)
|
||||
}
|
||||
sizeBytes = n
|
||||
}
|
||||
|
||||
ctx := cmd.Context()
|
||||
stagingRoot, err := os.MkdirTemp("", "banger-mkbundle-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(stagingRoot)
|
||||
rootfsTree := filepath.Join(stagingRoot, "rootfs")
|
||||
if err := os.MkdirAll(rootfsTree, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var tarReader io.Reader
|
||||
if opts.rootfsTarPath == "-" {
|
||||
tarReader = cmd.InOrStdin()
|
||||
} else {
|
||||
f, err := os.Open(opts.rootfsTarPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open rootfs tar: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
tarReader = f
|
||||
}
|
||||
|
||||
fmt.Fprintln(cmd.ErrOrStderr(), "[make-bundle] extracting rootfs")
|
||||
meta, err := imagepull.FlattenTar(ctx, tarReader, rootfsTree)
|
||||
if err != nil {
|
||||
return fmt.Errorf("flatten rootfs: %w", err)
|
||||
}
|
||||
|
||||
// docker create drops /.dockerenv (and containerd drops
|
||||
// /run/.containerenv) into the container's writable layer, so
|
||||
// `docker export` includes them in the tar. systemd-detect-virt
|
||||
// reads those files and flags the boot as virtualization=docker,
|
||||
// which disables udev device-unit activation (including the work-
|
||||
// disk dev-vdb.device) and leaves systemd waiting forever. Strip
|
||||
// them before building the ext4.
|
||||
for _, marker := range []string{".dockerenv", "run/.containerenv"} {
|
||||
path := filepath.Join(rootfsTree, marker)
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("strip %s: %w", marker, err)
|
||||
}
|
||||
delete(meta.Entries, marker)
|
||||
}
|
||||
|
||||
if sizeBytes <= 0 {
|
||||
treeSize, err := dirSize(rootfsTree)
|
||||
if err != nil {
|
||||
return fmt.Errorf("size rootfs tree: %w", err)
|
||||
}
|
||||
// +50% headroom for ext4 overhead (inode tables, block-group
|
||||
// descriptors, journal, 5% reserved margin).
|
||||
sizeBytes = treeSize + treeSize/2
|
||||
if sizeBytes < imagepull.MinExt4Size {
|
||||
sizeBytes = imagepull.MinExt4Size
|
||||
}
|
||||
}
|
||||
|
||||
ext4Path := filepath.Join(stagingRoot, imagecat.RootfsFilename)
|
||||
runner := system.NewRunner()
|
||||
fmt.Fprintf(cmd.ErrOrStderr(), "[make-bundle] building rootfs.ext4 (%d bytes)\n", sizeBytes)
|
||||
if err := imagepull.BuildExt4(ctx, runner, rootfsTree, ext4Path, sizeBytes); err != nil {
|
||||
return fmt.Errorf("build ext4: %w", err)
|
||||
}
|
||||
fmt.Fprintln(cmd.ErrOrStderr(), "[make-bundle] applying ownership fixup")
|
||||
if err := imagepull.ApplyOwnership(ctx, runner, ext4Path, meta); err != nil {
|
||||
return fmt.Errorf("apply ownership: %w", err)
|
||||
}
|
||||
fmt.Fprintln(cmd.ErrOrStderr(), "[make-bundle] injecting guest agents")
|
||||
vsockBin, err := paths.CompanionBinaryPath("banger-vsock-agent")
|
||||
if err != nil {
|
||||
return fmt.Errorf("locate vsock agent: %w", err)
|
||||
}
|
||||
if err := imagepull.InjectGuestAgents(ctx, runner, ext4Path, imagepull.GuestAgentAssets{VsockAgentBin: vsockBin}); err != nil {
|
||||
return fmt.Errorf("inject guest agents: %w", err)
|
||||
}
|
||||
|
||||
manifest := imagecat.Manifest{
|
||||
Name: opts.name,
|
||||
Distro: strings.TrimSpace(opts.distro),
|
||||
Arch: opts.arch,
|
||||
KernelRef: strings.TrimSpace(opts.kernelRef),
|
||||
Description: strings.TrimSpace(opts.description),
|
||||
}
|
||||
manifestPath := filepath.Join(stagingRoot, imagecat.ManifestFilename)
|
||||
manifestData, err := json.MarshalIndent(manifest, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(manifestPath, append(manifestData, '\n'), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(cmd.ErrOrStderr(), "[make-bundle] packaging bundle")
|
||||
if err := writeBundleTarZst(opts.outPath, ext4Path, manifestPath); err != nil {
|
||||
return fmt.Errorf("write bundle: %w", err)
|
||||
}
|
||||
|
||||
sum, err := sha256HexFile(opts.outPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stat, err := os.Stat(opts.outPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "bundle: %s\nsha256: %s\nsize: %d\n", opts.outPath, sum, stat.Size())
|
||||
return nil
|
||||
}
|
||||
|
||||
func dirSize(root string) (int64, error) {
|
||||
var total int64
|
||||
err := filepath.WalkDir(root, func(_ string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.Type().IsRegular() {
|
||||
return nil
|
||||
}
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
total += info.Size()
|
||||
return nil
|
||||
})
|
||||
return total, err
|
||||
}
|
||||
|
||||
func writeBundleTarZst(outPath, rootfsPath, manifestPath string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := os.OpenFile(outPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
zw, err := zstd.NewWriter(out, zstd.WithEncoderLevel(zstd.SpeedBestCompression))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tw := tar.NewWriter(zw)
|
||||
for _, src := range []struct{ path, name string }{
|
||||
{rootfsPath, imagecat.RootfsFilename},
|
||||
{manifestPath, imagecat.ManifestFilename},
|
||||
} {
|
||||
if err := writeBundleFile(tw, src.path, src.name); err != nil {
|
||||
_ = tw.Close()
|
||||
_ = zw.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tw.Close(); err != nil {
|
||||
_ = zw.Close()
|
||||
return err
|
||||
}
|
||||
if err := zw.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return out.Close()
|
||||
}
|
||||
|
||||
func writeBundleFile(tw *tar.Writer, src, name string) error {
|
||||
f, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tw.WriteHeader(&tar.Header{
|
||||
Name: name,
|
||||
Size: fi.Size(),
|
||||
Mode: 0o644,
|
||||
Typeflag: tar.TypeReg,
|
||||
ModTime: fi.ModTime(),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(tw, f)
|
||||
return err
|
||||
}
|
||||
|
||||
func sha256HexFile(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func newInternalWorkSeedCommand() *cobra.Command {
|
||||
var rootfsPath string
|
||||
var outPath string
|
||||
cmd := &cobra.Command{
|
||||
Use: "work-seed",
|
||||
Hidden: true,
|
||||
Args: noArgsUsage("usage: banger internal work-seed --rootfs <path> [--out <path>]"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
rootfsPath = strings.TrimSpace(rootfsPath)
|
||||
outPath = strings.TrimSpace(outPath)
|
||||
if rootfsPath == "" {
|
||||
return errors.New("rootfs path is required")
|
||||
}
|
||||
if outPath == "" {
|
||||
outPath = system.WorkSeedPath(rootfsPath)
|
||||
}
|
||||
if err := system.EnsureSudo(cmd.Context()); err != nil {
|
||||
return err
|
||||
}
|
||||
return system.BuildWorkSeedImage(cmd.Context(), system.NewRunner(), rootfsPath, outPath)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&rootfsPath, "rootfs", "", "rootfs image path")
|
||||
cmd.Flags().StringVar(&outPath, "out", "", "output work-seed image path")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newInternalNATCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "nat",
|
||||
Hidden: true,
|
||||
RunE: helpNoArgs,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newInternalNATActionCommand("up", true),
|
||||
newInternalNATActionCommand("down", false),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newInternalNATActionCommand(use string, enable bool) *cobra.Command {
|
||||
var guestIP string
|
||||
var tapDevice string
|
||||
cmd := &cobra.Command{
|
||||
Use: use,
|
||||
Hidden: true,
|
||||
Args: noArgsUsage("usage: banger internal nat " + use + " --guest-ip <ip> --tap <tap-device>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
guestIP = strings.TrimSpace(guestIP)
|
||||
tapDevice = strings.TrimSpace(tapDevice)
|
||||
if guestIP == "" {
|
||||
return errors.New("guest IP is required")
|
||||
}
|
||||
if tapDevice == "" {
|
||||
return errors.New("tap device is required")
|
||||
}
|
||||
if err := system.EnsureSudo(cmd.Context()); err != nil {
|
||||
return err
|
||||
}
|
||||
return hostnat.Ensure(cmd.Context(), system.NewRunner(), guestIP, tapDevice, enable)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&guestIP, "guest-ip", "", "guest IPv4 address")
|
||||
cmd.Flags().StringVar(&tapDevice, "tap", "", "tap device name")
|
||||
return cmd
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue