Stop presenting make runtime-bundle as a turnkey fresh-checkout bootstrap\nwhen the checked-in manifest is intentionally empty. The manifest comments,\nruntimebundle error messages, Make help, README, and AGENTS docs now all\ndescribe the same local-first flow: stage an archive, use a separate local\nmanifest copy with url/sha256, then bootstrap ./runtime from that manifest.\n\nKeep the existing package/fetch commands intact, and add a small runtimebundle\nregression test so the local-manifest guidance does not drift again.\n\nValidated with make help and GOCACHE=/tmp/banger-gocache go test\n./internal/runtimebundle.
252 lines
8.9 KiB
Go
252 lines
8.9 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/nat.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)
|
|
}
|