banger/internal/runtimebundle/bundle_test.go
Thales Maciel 430f66d5dd Move helper NAT management into Go
Remove the last shell-owned NAT surface by extracting the iptables logic into a shared Go package and using it from both bangerd and a hidden helper bridge in the CLI.

Route customize.sh and interactive.sh through banger internal nat up/down so the remaining shell helpers reuse the same rule logic, resolve the local banger binary explicitly, and tear NAT back down during cleanup.

Drop nat.sh from the runtime bundle and docs now that NAT is Go-managed everywhere, and keep coverage aligned with the new shared package and helper command.

Validation: go test ./..., bash -n customize.sh interactive.sh verify.sh, make build, and a live ./verify.sh --nat run that installed host rules, reached outbound network access, and cleaned them up successfully.
2026-03-17 15:07:49 -03:00

251 lines
8.8 KiB
Go

package runtimebundle
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
func TestBootstrapExtractsBundleAndValidatesChecksum(t *testing.T) {
manifestDir := t.TempDir()
bundleData := buildArchive(t, map[string]string{
"runtime/firecracker": "fc",
"runtime/id_ed25519": "key",
"runtime/namegen": "namegen",
"runtime/customize.sh": "#!/bin/bash\n",
"runtime/packages.sh": "#!/bin/bash\n",
"runtime/dns.sh": "#!/bin/bash\n",
"runtime/packages.apt": "vim\n",
"runtime/rootfs-docker.ext4": "rootfs",
"runtime/wtf/root/boot/vmlinux-6.8.0-94-generic": "kernel",
"runtime/wtf/root/boot/initrd.img-6.8.0-94-generic": "initrd",
"runtime/wtf/root/lib/modules/6.8.0-94-generic/modules.dep": "dep",
"runtime/bundle.json": mustJSON(t, BundleMetadata{FirecrackerBin: "firecracker", SSHKeyPath: "id_ed25519", NamegenPath: "namegen", CustomizeScript: "customize.sh", DefaultPackages: "packages.apt", DefaultRootfs: "rootfs-docker.ext4", DefaultKernel: "wtf/root/boot/vmlinux-6.8.0-94-generic", DefaultInitrd: "wtf/root/boot/initrd.img-6.8.0-94-generic", DefaultModulesDir: "wtf/root/lib/modules/6.8.0-94-generic"}),
})
archivePath := filepath.Join(manifestDir, "bundle.tar.gz")
if err := os.WriteFile(archivePath, bundleData, 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
manifest := Manifest{
URL: "./bundle.tar.gz",
SHA256: sha256Hex(bundleData),
BundleRoot: "runtime",
RequiredPaths: []string{"firecracker", "customize.sh", "packages.apt", "rootfs-docker.ext4", "wtf/root/boot/vmlinux-6.8.0-94-generic", "wtf/root/lib/modules/6.8.0-94-generic"},
}
outDir := filepath.Join(t.TempDir(), "runtime")
if err := Bootstrap(context.Background(), manifest, filepath.Join(manifestDir, "runtime-bundle.toml"), outDir); err != nil {
t.Fatalf("Bootstrap: %v", err)
}
for _, rel := range manifest.RequiredPaths {
if _, err := os.Stat(filepath.Join(outDir, rel)); err != nil {
t.Fatalf("runtime missing %s: %v", rel, err)
}
}
}
func TestBootstrapRejectsChecksumMismatch(t *testing.T) {
manifestDir := t.TempDir()
archivePath := filepath.Join(manifestDir, "bundle.tar.gz")
if err := os.WriteFile(archivePath, []byte("not-a-tarball"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
manifest := Manifest{
URL: "./bundle.tar.gz",
SHA256: strings.Repeat("0", 64),
BundleRoot: "runtime",
RequiredPaths: []string{"firecracker"},
}
err := Bootstrap(context.Background(), manifest, filepath.Join(manifestDir, "runtime-bundle.toml"), filepath.Join(t.TempDir(), "runtime"))
if err == nil || !strings.Contains(err.Error(), "checksum mismatch") {
t.Fatalf("Bootstrap() error = %v, want checksum mismatch", err)
}
}
func TestBootstrapRejectsMissingURLWithLocalManifestGuidance(t *testing.T) {
manifest := Manifest{
SHA256: strings.Repeat("0", 64),
BundleRoot: "runtime",
RequiredPaths: []string{"firecracker"},
}
err := Bootstrap(context.Background(), manifest, filepath.Join(t.TempDir(), "runtime-bundle.toml"), filepath.Join(t.TempDir(), "runtime"))
if err == nil || !strings.Contains(err.Error(), "local manifest copy") {
t.Fatalf("Bootstrap() error = %v, want local manifest guidance", err)
}
}
func TestBootstrapRejectsMissingSHAWithArchiveGuidance(t *testing.T) {
manifest := Manifest{
URL: "./bundle.tar.gz",
BundleRoot: "runtime",
RequiredPaths: []string{"firecracker"},
}
err := Bootstrap(context.Background(), manifest, filepath.Join(t.TempDir(), "runtime-bundle.toml"), filepath.Join(t.TempDir(), "runtime"))
if err == nil || !strings.Contains(err.Error(), "staged or published runtime bundle archive") {
t.Fatalf("Bootstrap() error = %v, want archive guidance", err)
}
}
func TestPackageWritesArchive(t *testing.T) {
runtimeDir := t.TempDir()
for _, rel := range []string{
"firecracker",
"id_ed25519",
"namegen",
"customize.sh",
"packages.apt",
"rootfs-docker.ext4",
"wtf/root/boot/vmlinux-6.8.0-94-generic",
"wtf/root/boot/initrd.img-6.8.0-94-generic",
"wtf/root/lib/modules/6.8.0-94-generic",
} {
path := filepath.Join(runtimeDir, rel)
if rel == "wtf/root/lib/modules/6.8.0-94-generic" {
if err := os.MkdirAll(path, 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join(path, "modules.dep"), []byte(rel), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
continue
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(path, []byte(rel), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
}
manifest := Manifest{
BundleRoot: "runtime",
BundleMeta: BundleMetadata{
FirecrackerBin: "firecracker",
SSHKeyPath: "id_ed25519",
NamegenPath: "namegen",
CustomizeScript: "customize.sh",
DefaultPackages: "packages.apt",
DefaultRootfs: "rootfs-docker.ext4",
DefaultKernel: "wtf/root/boot/vmlinux-6.8.0-94-generic",
DefaultInitrd: "wtf/root/boot/initrd.img-6.8.0-94-generic",
DefaultModulesDir: "wtf/root/lib/modules/6.8.0-94-generic",
},
RequiredPaths: []string{
"firecracker",
"id_ed25519",
"namegen",
"customize.sh",
"packages.apt",
"rootfs-docker.ext4",
"wtf/root/boot/vmlinux-6.8.0-94-generic",
"wtf/root/boot/initrd.img-6.8.0-94-generic",
"wtf/root/lib/modules/6.8.0-94-generic",
},
}
outArchive := filepath.Join(t.TempDir(), "bundle.tar.gz")
sum, err := Package(runtimeDir, outArchive, manifest)
if err != nil {
t.Fatalf("Package: %v", err)
}
if sum == "" {
t.Fatalf("Package() returned empty checksum")
}
if _, err := os.Stat(outArchive); err != nil {
t.Fatalf("archive missing: %v", err)
}
runtimeOut := filepath.Join(t.TempDir(), "runtime")
if err := Bootstrap(context.Background(), Manifest{
URL: outArchive,
SHA256: sum,
BundleRoot: "runtime",
RequiredPaths: manifest.RequiredPaths,
}, filepath.Join(t.TempDir(), "runtime-bundle.toml"), runtimeOut); err != nil {
t.Fatalf("Bootstrap packaged archive: %v", err)
}
if _, err := os.Stat(filepath.Join(runtimeOut, BundleMetadataFile)); err != nil {
t.Fatalf("bundle metadata missing after bootstrap: %v", err)
}
meta, err := LoadBundleMetadata(runtimeOut)
if err != nil {
t.Fatalf("LoadBundleMetadata: %v", err)
}
if meta.DefaultRootfs != manifest.BundleMeta.DefaultRootfs {
t.Fatalf("DefaultRootfs = %q, want %q", meta.DefaultRootfs, manifest.BundleMeta.DefaultRootfs)
}
}
func TestLoadBundleMetadataRejectsMissingRequiredPath(t *testing.T) {
runtimeDir := t.TempDir()
for _, rel := range []string{"firecracker", "id_ed25519", "namegen", "customize.sh", "packages.apt", "rootfs-docker.ext4"} {
path := filepath.Join(runtimeDir, rel)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(path, []byte(rel), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
}
data := mustJSON(t, BundleMetadata{
FirecrackerBin: "firecracker",
SSHKeyPath: "id_ed25519",
NamegenPath: "namegen",
CustomizeScript: "customize.sh",
DefaultPackages: "packages.apt",
DefaultRootfs: "rootfs-docker.ext4",
DefaultKernel: "missing-kernel",
})
if err := os.WriteFile(filepath.Join(runtimeDir, BundleMetadataFile), []byte(data), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
if _, err := LoadBundleMetadata(runtimeDir); err == nil || !strings.Contains(err.Error(), "default_kernel") {
t.Fatalf("LoadBundleMetadata() error = %v, want default_kernel failure", err)
}
}
func buildArchive(t *testing.T, files map[string]string) []byte {
t.Helper()
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
tw := tar.NewWriter(gz)
for name, contents := range files {
header := &tar.Header{
Name: name,
Mode: 0o644,
Size: int64(len(contents)),
}
if err := tw.WriteHeader(header); err != nil {
t.Fatalf("WriteHeader(%s): %v", name, err)
}
if _, err := tw.Write([]byte(contents)); err != nil {
t.Fatalf("Write(%s): %v", name, err)
}
}
if err := tw.Close(); err != nil {
t.Fatalf("Close tar: %v", err)
}
if err := gz.Close(); err != nil {
t.Fatalf("Close gzip: %v", err)
}
return buf.Bytes()
}
func sha256Hex(data []byte) string {
sum := sha256.Sum256(data)
return hex.EncodeToString(sum[:])
}
func mustJSON(t *testing.T, value any) string {
t.Helper()
data, err := json.Marshal(value)
if err != nil {
t.Fatalf("Marshal: %v", err)
}
return string(data)
}