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
277
internal/cli/vm_create.go
Normal file
277
internal/cli/vm_create.go
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"banger/internal/api"
|
||||
"banger/internal/config"
|
||||
"banger/internal/model"
|
||||
"banger/internal/paths"
|
||||
"banger/internal/system"
|
||||
)
|
||||
|
||||
// effectiveVMDefaults resolves the default VM sizing applied when
|
||||
// --vcpu/--memory/--disk-size aren't given: config overrides win
|
||||
// over host-derived heuristics, both fall back to baked-in
|
||||
// constants. Called at command-build time so the cobra flag defaults
|
||||
// reflect the resolved values.
|
||||
func effectiveVMDefaults() model.VMDefaults {
|
||||
var override model.VMDefaultsOverride
|
||||
if layout, err := paths.Resolve(); err == nil {
|
||||
if cfg, err := config.Load(layout); err == nil {
|
||||
override = cfg.VMDefaults
|
||||
}
|
||||
}
|
||||
host, err := system.ReadHostResources()
|
||||
if err != nil {
|
||||
return model.ResolveVMDefaults(override, 0, 0)
|
||||
}
|
||||
return model.ResolveVMDefaults(override, host.CPUCount, host.TotalMemoryBytes)
|
||||
}
|
||||
|
||||
// printVMSpecLine writes a one-line sizing summary to out. Always
|
||||
// emitted (even non-TTY) so logs and CI output carry the numbers.
|
||||
func printVMSpecLine(out io.Writer, params api.VMCreateParams) {
|
||||
vcpu := model.DefaultVCPUCount
|
||||
if params.VCPUCount != nil {
|
||||
vcpu = *params.VCPUCount
|
||||
}
|
||||
memory := model.DefaultMemoryMiB
|
||||
if params.MemoryMiB != nil {
|
||||
memory = *params.MemoryMiB
|
||||
}
|
||||
diskBytes := int64(model.DefaultWorkDiskSize)
|
||||
if strings.TrimSpace(params.WorkDiskSize) != "" {
|
||||
if parsed, err := model.ParseSize(params.WorkDiskSize); err == nil {
|
||||
diskBytes = parsed
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprintf(out, "spec: %d vcpu · %d MiB · %s disk\n",
|
||||
vcpu, memory, model.FormatSizeBytes(diskBytes))
|
||||
}
|
||||
|
||||
// runVMCreate drives the create RPC + polls for progress. stderr
|
||||
// gets the spec line up front and the progress renderer thereafter.
|
||||
// On context cancel we cooperate with the daemon to cancel the
|
||||
// in-flight op so it doesn't leak partially-created VM state.
|
||||
func runVMCreate(ctx context.Context, socketPath string, stderr io.Writer, params api.VMCreateParams) (model.VMRecord, error) {
|
||||
printVMSpecLine(stderr, params)
|
||||
begin, err := vmCreateBeginFunc(ctx, socketPath, params)
|
||||
if err != nil {
|
||||
return model.VMRecord{}, err
|
||||
}
|
||||
renderer := newVMCreateProgressRenderer(stderr)
|
||||
renderer.render(begin.Operation)
|
||||
|
||||
op := begin.Operation
|
||||
for {
|
||||
if op.Done {
|
||||
renderer.render(op)
|
||||
if op.Success && op.VM != nil {
|
||||
return *op.VM, nil
|
||||
}
|
||||
if strings.TrimSpace(op.Error) == "" {
|
||||
return model.VMRecord{}, errors.New("vm create failed")
|
||||
}
|
||||
return model.VMRecord{}, errors.New(op.Error)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
cancelCtx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
_ = vmCreateCancelFunc(cancelCtx, socketPath, op.ID)
|
||||
return model.VMRecord{}, ctx.Err()
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
}
|
||||
|
||||
status, err := vmCreateStatusFunc(ctx, socketPath, op.ID)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
cancelCtx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
_ = vmCreateCancelFunc(cancelCtx, socketPath, op.ID)
|
||||
return model.VMRecord{}, ctx.Err()
|
||||
}
|
||||
return model.VMRecord{}, err
|
||||
}
|
||||
op = status.Operation
|
||||
renderer.render(op)
|
||||
}
|
||||
}
|
||||
|
||||
type vmCreateProgressRenderer struct {
|
||||
out io.Writer
|
||||
enabled bool
|
||||
lastLine string
|
||||
}
|
||||
|
||||
func newVMCreateProgressRenderer(out io.Writer) *vmCreateProgressRenderer {
|
||||
return &vmCreateProgressRenderer{
|
||||
out: out,
|
||||
enabled: writerSupportsProgress(out),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *vmCreateProgressRenderer) render(op api.VMCreateOperation) {
|
||||
if r == nil || !r.enabled {
|
||||
return
|
||||
}
|
||||
line := formatVMCreateProgress(op)
|
||||
if line == "" || line == r.lastLine {
|
||||
return
|
||||
}
|
||||
r.lastLine = line
|
||||
_, _ = fmt.Fprintln(r.out, line)
|
||||
}
|
||||
|
||||
// writerSupportsProgress returns true only when out is a terminal.
|
||||
// Keeps stage lines + heartbeat dots out of piped / logged output
|
||||
// where they'd just be noise.
|
||||
func writerSupportsProgress(out io.Writer) bool {
|
||||
file, ok := out.(*os.File)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return info.Mode()&os.ModeCharDevice != 0
|
||||
}
|
||||
|
||||
// withHeartbeat runs fn while emitting a dot to stderr every 2
|
||||
// seconds so the user sees long-running RPCs (bundle downloads, etc.)
|
||||
// aren't wedged. No-op when stderr isn't a terminal, so piped or
|
||||
// logged output stays clean.
|
||||
func withHeartbeat(stderr io.Writer, label string, fn func() error) error {
|
||||
if !writerSupportsProgress(stderr) {
|
||||
return fn()
|
||||
}
|
||||
fmt.Fprintf(stderr, "[%s] ", label)
|
||||
stop := make(chan struct{})
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-ticker.C:
|
||||
fmt.Fprint(stderr, ".")
|
||||
}
|
||||
}
|
||||
}()
|
||||
err := fn()
|
||||
close(stop)
|
||||
<-done
|
||||
fmt.Fprintln(stderr)
|
||||
return err
|
||||
}
|
||||
|
||||
func formatVMCreateProgress(op api.VMCreateOperation) string {
|
||||
stage := strings.TrimSpace(op.Stage)
|
||||
detail := strings.TrimSpace(op.Detail)
|
||||
label := vmCreateStageLabel(stage)
|
||||
if label == "" && detail == "" {
|
||||
return ""
|
||||
}
|
||||
if label == "" {
|
||||
return "[vm create] " + detail
|
||||
}
|
||||
if detail == "" {
|
||||
return "[vm create] " + label
|
||||
}
|
||||
return "[vm create] " + label + ": " + detail
|
||||
}
|
||||
|
||||
// vmCreateStageLabel humanises the daemon-side stage IDs. Anything
|
||||
// unknown falls through to `strings.ReplaceAll(_, "_", " ")` so new
|
||||
// stages still render meaningfully without a code change.
|
||||
func vmCreateStageLabel(stage string) string {
|
||||
switch strings.TrimSpace(stage) {
|
||||
case "queued":
|
||||
return "queued"
|
||||
case "resolve_image":
|
||||
return "resolving image"
|
||||
case "reserve_vm":
|
||||
return "allocating vm"
|
||||
case "preflight":
|
||||
return "checking host prerequisites"
|
||||
case "prepare_rootfs":
|
||||
return "preparing root filesystem"
|
||||
case "prepare_host_features":
|
||||
return "preparing host features"
|
||||
case "prepare_work_disk":
|
||||
return "preparing work disk"
|
||||
case "boot_firecracker":
|
||||
return "starting firecracker"
|
||||
case "wait_vsock_agent":
|
||||
return "waiting for vsock agent"
|
||||
case "wait_guest_ready":
|
||||
return "waiting for guest services"
|
||||
case "apply_dns":
|
||||
return "publishing dns"
|
||||
case "apply_nat":
|
||||
return "configuring nat"
|
||||
case "finalize":
|
||||
return "finalizing"
|
||||
case "ready":
|
||||
return "ready"
|
||||
default:
|
||||
return strings.ReplaceAll(stage, "_", " ")
|
||||
}
|
||||
}
|
||||
|
||||
func validatePositiveSetting(label string, value int) error {
|
||||
if value <= 0 {
|
||||
return fmt.Errorf("%s must be a positive integer", label)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// shortID and relativeTime are small display helpers used across
|
||||
// every printer; kept here alongside the other render-time helpers.
|
||||
func shortID(id string) string {
|
||||
if len(id) <= 12 {
|
||||
return id
|
||||
}
|
||||
return id[:12]
|
||||
}
|
||||
|
||||
func relativeTime(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return "-"
|
||||
}
|
||||
delta := time.Since(t)
|
||||
switch {
|
||||
case delta < 30*time.Second:
|
||||
return "moments ago"
|
||||
case delta < time.Minute:
|
||||
return fmt.Sprintf("%d seconds ago", int(delta.Seconds()))
|
||||
case delta < 2*time.Minute:
|
||||
return "1 minute ago"
|
||||
case delta < time.Hour:
|
||||
return fmt.Sprintf("%d minutes ago", int(delta.Minutes()))
|
||||
case delta < 2*time.Hour:
|
||||
return "1 hour ago"
|
||||
case delta < 24*time.Hour:
|
||||
return fmt.Sprintf("%d hours ago", int(delta.Hours()))
|
||||
case delta < 48*time.Hour:
|
||||
return "1 day ago"
|
||||
case delta < 7*24*time.Hour:
|
||||
return fmt.Sprintf("%d days ago", int(delta.Hours()/24))
|
||||
case delta < 14*24*time.Hour:
|
||||
return "1 week ago"
|
||||
default:
|
||||
return fmt.Sprintf("%d weeks ago", int(delta.Hours()/(24*7)))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue