package cli import ( "context" "errors" "fmt" "io" "os" "os/exec" "path/filepath" "strings" "time" "banger/internal/api" "banger/internal/buildinfo" "banger/internal/daemon" "banger/internal/guest" "banger/internal/paths" "banger/internal/rpc" "banger/internal/toolingplan" "github.com/spf13/cobra" ) var ( bangerdPathFunc = paths.BangerdPath daemonExePath = func(pid int) string { return filepath.Join("/proc", fmt.Sprintf("%d", pid), "exe") } doctorFunc = daemon.Doctor sshExecFunc = func(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer, args []string) error { sshCmd := exec.CommandContext(ctx, "ssh", args...) sshCmd.Stdout = stdout sshCmd.Stderr = stderr sshCmd.Stdin = stdin return sshCmd.Run() } hostCommandOutputFunc = func(ctx context.Context, name string, args ...string) ([]byte, error) { cmd := exec.CommandContext(ctx, name, args...) output, err := cmd.CombinedOutput() if err == nil { return output, nil } command := strings.TrimSpace(strings.Join(append([]string{name}, args...), " ")) detail := strings.TrimSpace(string(output)) if detail == "" { return output, fmt.Errorf("%s: %w", command, err) } return output, fmt.Errorf("%s: %w: %s", command, err, detail) } vmHealthFunc = func(ctx context.Context, socketPath, idOrName string) (api.VMHealthResult, error) { return rpc.Call[api.VMHealthResult](ctx, socketPath, "vm.health", api.VMRefParams{IDOrName: idOrName}) } vmSSHFunc = func(ctx context.Context, socketPath, idOrName string) (api.VMSSHResult, error) { return rpc.Call[api.VMSSHResult](ctx, socketPath, "vm.ssh", api.VMRefParams{IDOrName: idOrName}) } vmDeleteFunc = func(ctx context.Context, socketPath, idOrName string) error { _, err := rpc.Call[api.VMShowResult](ctx, socketPath, "vm.delete", api.VMRefParams{IDOrName: idOrName}) return err } vmListFunc = func(ctx context.Context, socketPath string) (api.VMListResult, error) { return rpc.Call[api.VMListResult](ctx, socketPath, "vm.list", api.Empty{}) } daemonPingFunc = func(ctx context.Context, socketPath string) (api.PingResult, error) { return rpc.Call[api.PingResult](ctx, socketPath, "ping", api.Empty{}) } vmCreateBeginFunc = func(ctx context.Context, socketPath string, params api.VMCreateParams) (api.VMCreateBeginResult, error) { return rpc.Call[api.VMCreateBeginResult](ctx, socketPath, "vm.create.begin", params) } vmCreateStatusFunc = func(ctx context.Context, socketPath, operationID string) (api.VMCreateStatusResult, error) { return rpc.Call[api.VMCreateStatusResult](ctx, socketPath, "vm.create.status", api.VMCreateStatusParams{ID: operationID}) } vmCreateCancelFunc = func(ctx context.Context, socketPath, operationID string) error { _, err := rpc.Call[api.Empty](ctx, socketPath, "vm.create.cancel", api.VMCreateStatusParams{ID: operationID}) return err } vmPortsFunc = func(ctx context.Context, socketPath, idOrName string) (api.VMPortsResult, error) { return rpc.Call[api.VMPortsResult](ctx, socketPath, "vm.ports", api.VMRefParams{IDOrName: idOrName}) } vmWorkspacePrepareFunc = func(ctx context.Context, socketPath string, params api.VMWorkspacePrepareParams) (api.VMWorkspacePrepareResult, error) { return rpc.Call[api.VMWorkspacePrepareResult](ctx, socketPath, "vm.workspace.prepare", params) } vmWorkspaceExportFunc = func(ctx context.Context, socketPath string, params api.WorkspaceExportParams) (api.WorkspaceExportResult, error) { return rpc.Call[api.WorkspaceExportResult](ctx, socketPath, "vm.workspace.export", params) } guestSessionStartFunc = func(ctx context.Context, socketPath string, params api.GuestSessionStartParams) (api.GuestSessionShowResult, error) { return rpc.Call[api.GuestSessionShowResult](ctx, socketPath, "guest.session.start", params) } guestSessionGetFunc = func(ctx context.Context, socketPath string, params api.GuestSessionRefParams) (api.GuestSessionShowResult, error) { return rpc.Call[api.GuestSessionShowResult](ctx, socketPath, "guest.session.get", params) } guestSessionListFunc = func(ctx context.Context, socketPath, idOrName string) (api.GuestSessionListResult, error) { return rpc.Call[api.GuestSessionListResult](ctx, socketPath, "guest.session.list", api.VMRefParams{IDOrName: idOrName}) } guestSessionStopFunc = func(ctx context.Context, socketPath string, params api.GuestSessionRefParams) (api.GuestSessionShowResult, error) { return rpc.Call[api.GuestSessionShowResult](ctx, socketPath, "guest.session.stop", params) } guestSessionKillFunc = func(ctx context.Context, socketPath string, params api.GuestSessionRefParams) (api.GuestSessionShowResult, error) { return rpc.Call[api.GuestSessionShowResult](ctx, socketPath, "guest.session.kill", params) } guestSessionLogsFunc = func(ctx context.Context, socketPath string, params api.GuestSessionLogsParams) (api.GuestSessionLogsResult, error) { return rpc.Call[api.GuestSessionLogsResult](ctx, socketPath, "guest.session.logs", params) } guestSessionAttachBeginFunc = func(ctx context.Context, socketPath string, params api.GuestSessionAttachBeginParams) (api.GuestSessionAttachBeginResult, error) { return rpc.Call[api.GuestSessionAttachBeginResult](ctx, socketPath, "guest.session.attach.begin", params) } guestSessionSendFunc = func(ctx context.Context, socketPath string, params api.GuestSessionSendParams) (api.GuestSessionSendResult, error) { return rpc.Call[api.GuestSessionSendResult](ctx, socketPath, "guest.session.send", params) } guestWaitForSSHFunc = func(ctx context.Context, address, privateKeyPath string, interval time.Duration) error { knownHosts, _ := bangerKnownHostsPath() return guest.WaitForSSH(ctx, address, privateKeyPath, knownHosts, interval) } guestDialFunc = func(ctx context.Context, address, privateKeyPath string) (vmRunGuestClient, error) { knownHosts, _ := bangerKnownHostsPath() return guest.Dial(ctx, address, privateKeyPath, knownHosts) } buildVMRunToolingPlanFunc = toolingplan.Build cwdFunc = os.Getwd ) func NewBangerCommand() *cobra.Command { root := &cobra.Command{ Use: "banger", Short: "Manage development VMs and images", SilenceUsage: true, SilenceErrors: true, RunE: helpNoArgs, } root.AddCommand(newDaemonCommand(), newDoctorCommand(), newImageCommand(), newInternalCommand(), newKernelCommand(), newVersionCommand(), newPSCommand(), newVMCommand()) return root } func newDoctorCommand() *cobra.Command { return &cobra.Command{ Use: "doctor", Short: "Check host and runtime readiness", Args: noArgsUsage("usage: banger doctor"), RunE: func(cmd *cobra.Command, args []string) error { report, err := doctorFunc(cmd.Context()) if err != nil { return err } if err := printDoctorReport(cmd.OutOrStdout(), report); err != nil { return err } if report.HasFailures() { return errors.New("doctor found failing checks") } return nil }, } } func newVersionCommand() *cobra.Command { return &cobra.Command{ Use: "version", Short: "Show banger build information", Args: noArgsUsage("usage: banger version"), RunE: func(cmd *cobra.Command, args []string) error { _, err := fmt.Fprint(cmd.OutOrStdout(), formatBuildInfoBlock(buildinfo.Current())) return err }, } } func helpNoArgs(cmd *cobra.Command, args []string) error { if len(args) != 0 { return fmt.Errorf("unknown arguments: %s", strings.Join(args, " ")) } return cmd.Help() } func noArgsUsage(usage string) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if len(args) != 0 { return errors.New(usage) } return nil } } func exactArgsUsage(n int, usage string) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if len(args) != n { return errors.New(usage) } return nil } } func minArgsUsage(n int, usage string) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if len(args) < n { return errors.New(usage) } return nil } } func maxArgsUsage(n int, usage string) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if len(args) > n { return errors.New(usage) } return nil } } func shellQuote(value string) string { return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'" } func absolutizeImageRegisterPaths(params *api.ImageRegisterParams) error { return absolutizePaths( ¶ms.RootfsPath, ¶ms.WorkSeedPath, ¶ms.KernelPath, ¶ms.InitrdPath, ¶ms.ModulesDir, ) } func absolutizePaths(values ...*string) error { var err error for _, value := range values { if *value == "" || filepath.IsAbs(*value) { continue } *value, err = filepath.Abs(*value) if err != nil { return err } } return nil } func formatBuildInfoBlock(info buildinfo.Info) string { return fmt.Sprintf("version: %s\ncommit: %s\nbuilt_at: %s\n", info.Version, info.Commit, info.BuiltAt) }