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
125
internal/cli/ssh.go
Normal file
125
internal/cli/ssh.go
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"banger/internal/model"
|
||||
"banger/internal/paths"
|
||||
"banger/internal/system"
|
||||
"banger/internal/vsockagent"
|
||||
)
|
||||
|
||||
// runSSHSession executes ssh with the given args. On exit it decides
|
||||
// whether to print the "vm is still running" reminder: we skip it if
|
||||
// the caller asked (e.g. --rm is about to delete the VM), if the
|
||||
// ctx is already done, or if the ssh error isn't the one that
|
||||
// typically means "user disconnected cleanly".
|
||||
func runSSHSession(ctx context.Context, socketPath, vmRef string, stdin io.Reader, stdout, stderr io.Writer, sshArgs []string, skipReminder bool) error {
|
||||
sshErr := sshExecFunc(ctx, stdin, stdout, stderr, sshArgs)
|
||||
if skipReminder || !shouldCheckSSHReminder(sshErr) || ctx.Err() != nil {
|
||||
return sshErr
|
||||
}
|
||||
pingCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
health, err := vmHealthFunc(pingCtx, socketPath, vmRef)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(stderr, vsockagent.WarningMessage(vmRef, err))
|
||||
return sshErr
|
||||
}
|
||||
if health.Healthy {
|
||||
name := health.Name
|
||||
if strings.TrimSpace(name) == "" {
|
||||
name = vmRef
|
||||
}
|
||||
_, _ = fmt.Fprintln(stderr, vsockagent.ReminderMessage(name))
|
||||
}
|
||||
return sshErr
|
||||
}
|
||||
|
||||
func shouldCheckSSHReminder(err error) bool {
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
var exitErr *exec.ExitError
|
||||
if !errors.As(err, &exitErr) {
|
||||
return false
|
||||
}
|
||||
return exitErr.ExitCode() != 255
|
||||
}
|
||||
|
||||
// sshCommandArgs builds the argv for `ssh` invocations against a VM.
|
||||
// Host-key verification uses a banger-owned known_hosts file
|
||||
// populated by the daemon's first successful Go-SSH dial to each VM
|
||||
// (trust-on-first-use). `accept-new` means: accept-and-pin on first
|
||||
// contact; strict-verify afterwards. The user's own
|
||||
// ~/.ssh/known_hosts is never touched.
|
||||
func sshCommandArgs(cfg model.DaemonConfig, guestIP string, extra []string) ([]string, error) {
|
||||
if guestIP == "" {
|
||||
return nil, errors.New("vm has no guest IP")
|
||||
}
|
||||
args := []string{}
|
||||
args = append(args, "-F", "/dev/null")
|
||||
if cfg.SSHKeyPath != "" {
|
||||
args = append(args, "-i", cfg.SSHKeyPath)
|
||||
}
|
||||
knownHosts, khErr := bangerKnownHostsPath()
|
||||
args = append(
|
||||
args,
|
||||
"-o", "IdentitiesOnly=yes",
|
||||
"-o", "BatchMode=yes",
|
||||
"-o", "PreferredAuthentications=publickey",
|
||||
"-o", "PasswordAuthentication=no",
|
||||
"-o", "KbdInteractiveAuthentication=no",
|
||||
)
|
||||
if khErr == nil {
|
||||
args = append(args,
|
||||
"-o", "UserKnownHostsFile="+knownHosts,
|
||||
"-o", "StrictHostKeyChecking=accept-new",
|
||||
)
|
||||
} else {
|
||||
// If we can't resolve the banger path (unusual — paths.Resolve
|
||||
// basically can't fail), fall through to a hard-fail posture
|
||||
// rather than silently disabling verification.
|
||||
args = append(args,
|
||||
"-o", "StrictHostKeyChecking=yes",
|
||||
)
|
||||
}
|
||||
args = append(args, "root@"+guestIP)
|
||||
args = append(args, extra...)
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// bangerKnownHostsPath resolves the TOFU file the daemon writes into
|
||||
// and the CLI reads back. Both sides must agree on the path or the
|
||||
// pin doesn't round-trip.
|
||||
func bangerKnownHostsPath() (string, error) {
|
||||
layout, err := paths.Resolve()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return layout.KnownHostsPath, nil
|
||||
}
|
||||
|
||||
func validateSSHPrereqs(cfg model.DaemonConfig) error {
|
||||
checks := system.NewPreflight()
|
||||
checks.RequireCommand("ssh", "install openssh-client")
|
||||
if strings.TrimSpace(cfg.SSHKeyPath) != "" {
|
||||
checks.RequireFile(cfg.SSHKeyPath, "ssh private key", `set "ssh_key_path" or let banger create its default key`)
|
||||
}
|
||||
return checks.Err("ssh preflight failed")
|
||||
}
|
||||
|
||||
func validateVMRunPrereqs(cfg model.DaemonConfig) error {
|
||||
checks := system.NewPreflight()
|
||||
checks.RequireCommand("git", "install git")
|
||||
if strings.TrimSpace(cfg.SSHKeyPath) != "" {
|
||||
checks.RequireFile(cfg.SSHKeyPath, "ssh private key", `set "ssh_key_path" or let banger create its default key`)
|
||||
}
|
||||
return checks.Err("vm run preflight failed")
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue