banger/internal/config/config_test.go
Thales Maciel e0894376ea
vm create: auto-pull image and kernel from catalogs if missing
One-command sandbox: `banger vm run` on a fresh host now Just Works.
No prior `banger image pull` or `banger kernel pull` needed.

Changes:

- Default `default_image_name` flips from "default" to "debian-bookworm"
  so the golden image is the implicit target when `--image` is omitted.
- `CreateVM` resolves the image via a new `findOrAutoPullImage`: try
  the local store first, and on miss fall back to the embedded imagecat
  catalog + auto-pull. Emits a vm-create progress stage so the user
  sees "pulling from image catalog" in the create output.
- `resolveKernelInputs` gains context + the same pattern via
  `readOrAutoPullKernel`: try the local kernelcat, and on miss look up
  the embedded kernelcat and auto-pull. Fires whenever a bundle's
  manifest references a kernel the user hasn't pulled yet, not just
  during image pull — any CreateVM with an image that needs a kernel
  not yet local will resolve it.
- `--image` help text updated on both `vm run` and `vm create`.

Six tests cover local-hit-no-pull, auto-pull-on-miss, not-in-catalog
error propagation, and a non-ENOENT kernel read error does NOT trigger
a misleading "not in catalog" claim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:10:26 -03:00

117 lines
3.2 KiB
Go

package config
import (
"os"
"path/filepath"
"testing"
"time"
"banger/internal/paths"
)
func TestLoadDefaultsResolveFirecrackerAndGenerateSSHKey(t *testing.T) {
configDir := t.TempDir()
binDir := t.TempDir()
firecrackerPath := filepath.Join(binDir, "firecracker")
if err := os.WriteFile(firecrackerPath, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
t.Fatalf("write firecracker: %v", err)
}
t.Setenv("PATH", binDir)
cfg, err := Load(paths.Layout{ConfigDir: configDir})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.FirecrackerBin != firecrackerPath {
t.Fatalf("FirecrackerBin = %q, want %q", cfg.FirecrackerBin, firecrackerPath)
}
wantKey := filepath.Join(configDir, "ssh", "id_ed25519")
if cfg.SSHKeyPath != wantKey {
t.Fatalf("SSHKeyPath = %q, want %q", cfg.SSHKeyPath, wantKey)
}
for _, path := range []string{wantKey, wantKey + ".pub"} {
if _, err := os.Stat(path); err != nil {
t.Fatalf("stat %s: %v", path, err)
}
}
if cfg.DefaultImageName != "debian-bookworm" {
t.Fatalf("DefaultImageName = %q, want debian-bookworm", cfg.DefaultImageName)
}
if cfg.WebListenAddr != "127.0.0.1:7777" {
t.Fatalf("WebListenAddr = %q", cfg.WebListenAddr)
}
}
func TestLoadAppliesConfigOverrides(t *testing.T) {
configDir := t.TempDir()
data := []byte(`
log_level = "debug"
web_listen_addr = ""
firecracker_bin = "/opt/firecracker"
ssh_key_path = "/tmp/custom-key"
default_image_name = "void"
auto_stop_stale_after = "1h"
stats_poll_interval = "15s"
metrics_poll_interval = "30s"
bridge_name = "br-test"
bridge_ip = "10.0.0.1"
cidr = "25"
tap_pool_size = 8
default_dns = "9.9.9.9"
`)
if err := os.WriteFile(filepath.Join(configDir, "config.toml"), data, 0o644); err != nil {
t.Fatalf("write config.toml: %v", err)
}
cfg, err := Load(paths.Layout{ConfigDir: configDir})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.LogLevel != "debug" {
t.Fatalf("LogLevel = %q", cfg.LogLevel)
}
if cfg.WebListenAddr != "" {
t.Fatalf("WebListenAddr = %q, want empty", cfg.WebListenAddr)
}
if cfg.FirecrackerBin != "/opt/firecracker" {
t.Fatalf("FirecrackerBin = %q", cfg.FirecrackerBin)
}
if cfg.SSHKeyPath != "/tmp/custom-key" {
t.Fatalf("SSHKeyPath = %q", cfg.SSHKeyPath)
}
if cfg.DefaultImageName != "void" {
t.Fatalf("DefaultImageName = %q", cfg.DefaultImageName)
}
if cfg.AutoStopStaleAfter != time.Hour {
t.Fatalf("AutoStopStaleAfter = %s", cfg.AutoStopStaleAfter)
}
if cfg.StatsPollInterval != 15*time.Second {
t.Fatalf("StatsPollInterval = %s", cfg.StatsPollInterval)
}
if cfg.MetricsPollInterval != 30*time.Second {
t.Fatalf("MetricsPollInterval = %s", cfg.MetricsPollInterval)
}
if cfg.BridgeName != "br-test" || cfg.BridgeIP != "10.0.0.1" || cfg.CIDR != "25" {
t.Fatalf("bridge config = %+v", cfg)
}
if cfg.TapPoolSize != 8 {
t.Fatalf("TapPoolSize = %d", cfg.TapPoolSize)
}
if cfg.DefaultDNS != "9.9.9.9" {
t.Fatalf("DefaultDNS = %q", cfg.DefaultDNS)
}
}
func TestLoadAppliesLogLevelEnvOverride(t *testing.T) {
t.Setenv("BANGER_LOG_LEVEL", "warn")
cfg, err := Load(paths.Layout{ConfigDir: t.TempDir()})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.LogLevel != "warn" {
t.Fatalf("LogLevel = %q, want warn", cfg.LogLevel)
}
}