Remove runtime-bundle image dependencies

Hard-cut banger away from source-checkout runtime bundles as an implicit source of\nimage and host defaults. Managed images now own their full boot set,\nimage build starts from an existing registered image, and daemon startup\nno longer synthesizes a default image from host paths.\n\nResolve Firecracker from PATH or firecracker_bin, make SSH keys config-owned\nwith an auto-managed XDG default, replace the external name generator and\npackage manifests with Go code, and keep the vsock helper as a companion\nbinary instead of a user-managed runtime asset.\n\nUpdate the manual scripts, web/CLI forms, config surface, and docs around\nthe new build/manual flow and explicit image registration semantics.\n\nValidation: GOCACHE=/tmp/banger-gocache go test ./..., bash -n scripts/*.sh,\nand make build.
This commit is contained in:
Thales Maciel 2026-03-21 18:34:53 -03:00
parent 01c7cb5e65
commit 572bf32424
No known key found for this signature in database
GPG key ID: 33112E6833C34679
44 changed files with 1194 additions and 3456 deletions

View file

@ -1,722 +1,106 @@
package daemon
import (
"bufio"
"bytes"
"context"
"encoding/json"
"net"
"os"
"path/filepath"
"strings"
"testing"
"time"
"banger/internal/api"
"banger/internal/model"
"banger/internal/paths"
"banger/internal/rpc"
"banger/internal/store"
"banger/internal/system"
)
func TestEnsureDefaultImageUsesConfiguredDefaultRootfs(t *testing.T) {
dir := t.TempDir()
rootfs, kernel, _, _, _ := writeDefaultImageArtifacts(t, dir)
db := openDefaultImageStore(t, dir)
func TestBuildImageRequiresFromImage(t *testing.T) {
d := &Daemon{
config: model.DaemonConfig{
DefaultImageName: "default",
DefaultRootfs: rootfs,
DefaultKernel: kernel,
},
store: db,
layout: paths.Layout{ImagesDir: t.TempDir(), StateDir: t.TempDir()},
store: openDaemonStore(t),
runner: system.NewRunner(),
}
if err := d.ensureDefaultImage(context.Background()); err != nil {
t.Fatalf("ensureDefaultImage: %v", err)
}
image, err := db.GetImageByName(context.Background(), "default")
if err != nil {
t.Fatalf("GetImageByName: %v", err)
}
if image.RootfsPath != rootfs {
t.Fatalf("RootfsPath = %q, want %q", image.RootfsPath, rootfs)
}
if image.KernelPath != kernel {
t.Fatalf("KernelPath = %q, want %q", image.KernelPath, kernel)
}
if image.Managed {
t.Fatal("default image should be unmanaged")
_, err := d.BuildImage(context.Background(), api.ImageBuildParams{Name: "missing-base"})
if err == nil || !strings.Contains(err.Error(), "from-image is required") {
t.Fatalf("BuildImage() error = %v", err)
}
}
func TestEnsureDefaultImageLeavesCurrentUnmanagedDefaultUntouched(t *testing.T) {
func TestRegisterImageRequiresKernel(t *testing.T) {
rootfs := filepath.Join(t.TempDir(), "rootfs.ext4")
if err := os.WriteFile(rootfs, []byte("rootfs"), 0o644); err != nil {
t.Fatalf("write rootfs: %v", err)
}
d := &Daemon{store: openDaemonStore(t)}
_, err := d.RegisterImage(context.Background(), api.ImageRegisterParams{
Name: "missing-kernel",
RootfsPath: rootfs,
})
if err == nil || !strings.Contains(err.Error(), "kernel path is required") {
t.Fatalf("RegisterImage() error = %v", err)
}
}
func TestPromoteImageCopiesBootArtifactsIntoArtifactDir(t *testing.T) {
dir := t.TempDir()
rootfs, kernel, initrd, modulesDir, packages := writeDefaultImageArtifacts(t, dir)
db := openDefaultImageStore(t, dir)
now := time.Date(2026, time.March, 16, 12, 0, 0, 0, time.UTC)
rootfs := filepath.Join(dir, "rootfs.ext4")
kernel := filepath.Join(dir, "vmlinux")
initrd := filepath.Join(dir, "initrd.img")
modulesDir := filepath.Join(dir, "modules")
if err := os.MkdirAll(modulesDir, 0o755); err != nil {
t.Fatalf("mkdir modules: %v", err)
}
for path, data := range map[string]string{
rootfs: "rootfs",
kernel: "kernel",
initrd: "initrd",
filepath.Join(modulesDir, "depmod"): "modules",
} {
if err := os.WriteFile(path, []byte(data), 0o644); err != nil {
t.Fatalf("write %s: %v", path, err)
}
}
db := openDaemonStore(t)
image := model.Image{
ID: "default-id",
Name: "default",
Managed: false,
RootfsPath: rootfs,
KernelPath: kernel,
InitrdPath: initrd,
ModulesDir: modulesDir,
PackagesPath: packages,
Docker: true,
CreatedAt: now,
UpdatedAt: now,
ID: "img-promote",
Name: "void-exp",
Managed: false,
RootfsPath: rootfs,
KernelPath: kernel,
InitrdPath: initrd,
ModulesDir: modulesDir,
CreatedAt: model.Now(),
UpdatedAt: model.Now(),
}
if err := db.UpsertImage(context.Background(), image); err != nil {
t.Fatalf("UpsertImage: %v", err)
}
d := &Daemon{
config: model.DaemonConfig{
DefaultImageName: "default",
DefaultRootfs: rootfs,
DefaultKernel: kernel,
DefaultInitrd: initrd,
DefaultModulesDir: modulesDir,
DefaultPackagesFile: packages,
},
store: db,
}
if err := d.ensureDefaultImage(context.Background()); err != nil {
t.Fatalf("ensureDefaultImage: %v", err)
}
got, err := db.GetImageByName(context.Background(), "default")
if err != nil {
t.Fatalf("GetImageByName: %v", err)
}
if got.ID != image.ID {
t.Fatalf("ID = %q, want %q", got.ID, image.ID)
}
if !got.UpdatedAt.Equal(image.UpdatedAt) {
t.Fatalf("UpdatedAt = %s, want unchanged %s", got.UpdatedAt, image.UpdatedAt)
}
}
func TestEnsureDefaultImageReconcilesStaleUnmanagedDefaultInPlace(t *testing.T) {
dir := t.TempDir()
rootfs, kernel, initrd, modulesDir, packages := writeDefaultImageArtifacts(t, dir)
db := openDefaultImageStore(t, dir)
now := time.Date(2026, time.March, 16, 12, 0, 0, 0, time.UTC)
stale := model.Image{
ID: "default-id",
Name: "default",
Managed: false,
RootfsPath: "/home/thales/projects/personal/banger/build/runtime/rootfs-docker.ext4",
KernelPath: "/home/thales/projects/personal/banger/build/runtime/wtf/root/boot/vmlinux-6.8.0-94-generic",
InitrdPath: "/home/thales/projects/personal/banger/build/runtime/wtf/root/boot/initrd.img-6.8.0-94-generic",
ModulesDir: "/home/thales/projects/personal/banger/build/runtime/wtf/root/lib/modules/6.8.0-94-generic",
PackagesPath: "/home/thales/projects/personal/banger/build/runtime/packages.apt",
Docker: true,
CreatedAt: now,
UpdatedAt: now,
}
if err := db.UpsertImage(context.Background(), stale); err != nil {
t.Fatalf("UpsertImage: %v", err)
}
vm := testVM("uses-default", stale.ID, "172.16.0.25")
if err := db.UpsertVM(context.Background(), vm); err != nil {
t.Fatalf("UpsertVM: %v", err)
imagesDir := filepath.Join(dir, "images")
if err := os.MkdirAll(imagesDir, 0o755); err != nil {
t.Fatalf("mkdir images dir: %v", err)
}
d := &Daemon{
config: model.DaemonConfig{
DefaultImageName: "default",
DefaultRootfs: rootfs,
DefaultKernel: kernel,
DefaultInitrd: initrd,
DefaultModulesDir: modulesDir,
DefaultPackagesFile: packages,
},
store: db,
}
if err := d.ensureDefaultImage(context.Background()); err != nil {
t.Fatalf("ensureDefaultImage: %v", err)
}
got, err := db.GetImageByName(context.Background(), "default")
if err != nil {
t.Fatalf("GetImageByName: %v", err)
}
if got.ID != stale.ID {
t.Fatalf("ID = %q, want preserved %q", got.ID, stale.ID)
}
if !got.CreatedAt.Equal(stale.CreatedAt) {
t.Fatalf("CreatedAt = %s, want preserved %s", got.CreatedAt, stale.CreatedAt)
}
if got.RootfsPath != rootfs || got.KernelPath != kernel || got.InitrdPath != initrd || got.ModulesDir != modulesDir || got.PackagesPath != packages {
t.Fatalf("stale default not reconciled: %+v", got)
}
if !got.UpdatedAt.After(stale.UpdatedAt) {
t.Fatalf("UpdatedAt = %s, want newer than %s", got.UpdatedAt, stale.UpdatedAt)
}
gotVM, err := db.GetVMByID(context.Background(), vm.ID)
if err != nil {
t.Fatalf("GetVMByID: %v", err)
}
if gotVM.ImageID != stale.ID {
t.Fatalf("VM image ID = %q, want preserved %q", gotVM.ImageID, stale.ID)
}
}
func TestEnsureDefaultImageLeavesManagedDefaultUntouched(t *testing.T) {
dir := t.TempDir()
rootfs, kernel, _, _, _ := writeDefaultImageArtifacts(t, dir)
db := openDefaultImageStore(t, dir)
now := time.Date(2026, time.March, 16, 12, 0, 0, 0, time.UTC)
managed := model.Image{
ID: "managed-default",
Name: "default",
Managed: true,
RootfsPath: "/managed/rootfs.ext4",
KernelPath: "/managed/vmlinux",
CreatedAt: now,
UpdatedAt: now,
}
if err := db.UpsertImage(context.Background(), managed); err != nil {
t.Fatalf("UpsertImage: %v", err)
}
d := &Daemon{
config: model.DaemonConfig{
DefaultImageName: "default",
DefaultRootfs: rootfs,
DefaultKernel: kernel,
},
store: db,
}
if err := d.ensureDefaultImage(context.Background()); err != nil {
t.Fatalf("ensureDefaultImage: %v", err)
}
got, err := db.GetImageByName(context.Background(), "default")
if err != nil {
t.Fatalf("GetImageByName: %v", err)
}
if got.RootfsPath != managed.RootfsPath || got.KernelPath != managed.KernelPath {
t.Fatalf("managed default was rewritten: %+v", got)
}
}
func TestEnsureDefaultImageSkipsRewriteWhenCurrentArtifactsMissing(t *testing.T) {
dir := t.TempDir()
db := openDefaultImageStore(t, dir)
now := time.Date(2026, time.March, 16, 12, 0, 0, 0, time.UTC)
stale := model.Image{
ID: "default-id",
Name: "default",
Managed: false,
RootfsPath: "/old/rootfs.ext4",
KernelPath: "/old/vmlinux",
CreatedAt: now,
UpdatedAt: now,
}
if err := db.UpsertImage(context.Background(), stale); err != nil {
t.Fatalf("UpsertImage: %v", err)
}
d := &Daemon{
config: model.DaemonConfig{
DefaultImageName: "default",
DefaultRootfs: filepath.Join(dir, "missing-rootfs.ext4"),
DefaultKernel: filepath.Join(dir, "missing-vmlinux"),
},
store: db,
}
if err := d.ensureDefaultImage(context.Background()); err != nil {
t.Fatalf("ensureDefaultImage: %v", err)
}
got, err := db.GetImageByName(context.Background(), "default")
if err != nil {
t.Fatalf("GetImageByName: %v", err)
}
if got.RootfsPath != stale.RootfsPath || got.KernelPath != stale.KernelPath {
t.Fatalf("default image should have stayed stale when no current artifacts exist: %+v", got)
}
}
func TestRegisterImageCreatesUnmanagedImage(t *testing.T) {
dir := t.TempDir()
rootfs, kernel, initrd, modulesDir, _ := writeDefaultImageArtifacts(t, dir)
workSeed := filepath.Join(dir, "rootfs-void.work-seed.ext4")
packages := filepath.Join(dir, "packages.void")
if err := os.WriteFile(workSeed, []byte("seed"), 0o644); err != nil {
t.Fatalf("WriteFile(workSeed): %v", err)
}
if err := os.WriteFile(packages, []byte("base-minimal\nopenssh\n"), 0o644); err != nil {
t.Fatalf("WriteFile(packages): %v", err)
}
db := openDefaultImageStore(t, dir)
d := &Daemon{
config: model.DaemonConfig{
DefaultKernel: kernel,
DefaultInitrd: initrd,
DefaultModulesDir: modulesDir,
},
store: db,
}
image, err := d.RegisterImage(context.Background(), api.ImageRegisterParams{
Name: "void-exp",
RootfsPath: rootfs,
WorkSeedPath: workSeed,
PackagesPath: packages,
})
if err != nil {
t.Fatalf("RegisterImage: %v", err)
}
if image.Managed {
t.Fatal("registered image should be unmanaged")
}
if image.Name != "void-exp" || image.RootfsPath != rootfs || image.WorkSeedPath != workSeed || image.KernelPath != kernel {
t.Fatalf("registered image = %+v", image)
}
}
func TestRegisterImageUpdatesExistingUnmanagedImageInPlace(t *testing.T) {
dir := t.TempDir()
_, kernel, initrd, modulesDir, _ := writeDefaultImageArtifacts(t, dir)
newRootfs := filepath.Join(dir, "rootfs-void-next.ext4")
newWorkSeed := filepath.Join(dir, "rootfs-void-next.work-seed.ext4")
packages := filepath.Join(dir, "packages.void")
for _, path := range []string{newRootfs, newWorkSeed} {
if err := os.WriteFile(path, []byte("next"), 0o644); err != nil {
t.Fatalf("WriteFile(%s): %v", path, err)
}
}
if err := os.WriteFile(packages, []byte("base-minimal\n"), 0o644); err != nil {
t.Fatalf("WriteFile(packages): %v", err)
}
db := openDefaultImageStore(t, dir)
now := time.Date(2026, time.March, 16, 12, 0, 0, 0, time.UTC)
existing := model.Image{
ID: "void-image-id",
Name: "void-exp",
Managed: false,
RootfsPath: filepath.Join(dir, "old-rootfs.ext4"),
KernelPath: kernel,
InitrdPath: initrd,
ModulesDir: modulesDir,
PackagesPath: packages,
CreatedAt: now,
UpdatedAt: now,
}
if err := db.UpsertImage(context.Background(), existing); err != nil {
t.Fatalf("UpsertImage: %v", err)
}
d := &Daemon{
config: model.DaemonConfig{
DefaultKernel: kernel,
DefaultInitrd: initrd,
DefaultModulesDir: modulesDir,
},
store: db,
}
image, err := d.RegisterImage(context.Background(), api.ImageRegisterParams{
Name: "void-exp",
RootfsPath: newRootfs,
WorkSeedPath: newWorkSeed,
PackagesPath: packages,
})
if err != nil {
t.Fatalf("RegisterImage: %v", err)
}
if image.ID != existing.ID || !image.CreatedAt.Equal(existing.CreatedAt) {
t.Fatalf("updated image identity changed: %+v", image)
}
if image.RootfsPath != newRootfs || image.WorkSeedPath != newWorkSeed {
t.Fatalf("updated image paths not applied: %+v", image)
}
}
func TestRegisterImageRejectsManagedOverwrite(t *testing.T) {
dir := t.TempDir()
rootfs, kernel, _, _, _ := writeDefaultImageArtifacts(t, dir)
db := openDefaultImageStore(t, dir)
now := time.Date(2026, time.March, 16, 12, 0, 0, 0, time.UTC)
if err := db.UpsertImage(context.Background(), model.Image{
ID: "managed-id",
Name: "void-exp",
Managed: true,
RootfsPath: rootfs,
KernelPath: kernel,
CreatedAt: now,
UpdatedAt: now,
}); err != nil {
t.Fatalf("UpsertImage: %v", err)
}
d := &Daemon{config: model.DaemonConfig{DefaultKernel: kernel}, store: db}
_, err := d.RegisterImage(context.Background(), api.ImageRegisterParams{
Name: "void-exp",
RootfsPath: rootfs,
})
if err == nil || !strings.Contains(err.Error(), "cannot be updated via register") {
t.Fatalf("RegisterImage(managed) error = %v", err)
}
}
func TestPromoteImageCopiesArtifactsAndPreservesIdentity(t *testing.T) {
dir := t.TempDir()
rootfs, kernel, initrd, modulesDir, packages := writeDefaultImageArtifacts(t, dir)
workSeed := filepath.Join(dir, "rootfs-docker.work-seed.ext4")
workSeedContent := []byte("seed-data")
if err := os.WriteFile(workSeed, workSeedContent, 0o644); err != nil {
t.Fatalf("WriteFile(workSeed): %v", err)
}
db := openDefaultImageStore(t, dir)
now := time.Date(2026, time.March, 20, 12, 0, 0, 0, time.UTC)
existing := model.Image{
ID: "promote-image-id",
Name: "default",
Managed: false,
RootfsPath: rootfs,
WorkSeedPath: workSeed,
KernelPath: kernel,
InitrdPath: initrd,
ModulesDir: modulesDir,
PackagesPath: packages,
Docker: true,
CreatedAt: now,
UpdatedAt: now,
}
if err := db.UpsertImage(context.Background(), existing); err != nil {
t.Fatalf("UpsertImage: %v", err)
}
vm := testVM("uses-default", existing.ID, "172.16.0.44")
if err := db.UpsertVM(context.Background(), vm); err != nil {
t.Fatalf("UpsertVM: %v", err)
}
d := &Daemon{
layout: modelPathsLayoutForTest(dir),
layout: paths.Layout{ImagesDir: imagesDir},
store: db,
runner: system.NewRunner(),
}
image, err := d.PromoteImage(context.Background(), "default")
got, err := d.PromoteImage(context.Background(), image.Name)
if err != nil {
t.Fatalf("PromoteImage: %v", err)
}
if !image.Managed {
if !got.Managed {
t.Fatal("promoted image should be managed")
}
if image.ID != existing.ID || image.Name != existing.Name {
t.Fatalf("promoted image identity changed: %+v", image)
}
if !image.CreatedAt.Equal(existing.CreatedAt) {
t.Fatalf("CreatedAt = %s, want preserved %s", image.CreatedAt, existing.CreatedAt)
}
if !image.UpdatedAt.After(existing.UpdatedAt) {
t.Fatalf("UpdatedAt = %s, want newer than %s", image.UpdatedAt, existing.UpdatedAt)
}
wantArtifactDir := filepath.Join(d.layout.ImagesDir, existing.ID)
if image.ArtifactDir != wantArtifactDir {
t.Fatalf("ArtifactDir = %q, want %q", image.ArtifactDir, wantArtifactDir)
}
if image.RootfsPath != filepath.Join(wantArtifactDir, "rootfs.ext4") {
t.Fatalf("RootfsPath = %q, want managed copy", image.RootfsPath)
}
if image.WorkSeedPath != filepath.Join(wantArtifactDir, "work-seed.ext4") {
t.Fatalf("WorkSeedPath = %q, want managed copy", image.WorkSeedPath)
}
if image.KernelPath != kernel || image.InitrdPath != initrd || image.ModulesDir != modulesDir || image.PackagesPath != packages {
t.Fatalf("boot support paths changed unexpectedly: %+v", image)
}
rootfsContent, err := os.ReadFile(rootfs)
if err != nil {
t.Fatalf("ReadFile(rootfs): %v", err)
}
managedRootfsContent, err := os.ReadFile(image.RootfsPath)
if err != nil {
t.Fatalf("ReadFile(managed rootfs): %v", err)
}
if !bytes.Equal(managedRootfsContent, rootfsContent) {
t.Fatal("managed rootfs copy content mismatch")
}
managedWorkSeedContent, err := os.ReadFile(image.WorkSeedPath)
if err != nil {
t.Fatalf("ReadFile(managed work seed): %v", err)
}
if !bytes.Equal(managedWorkSeedContent, workSeedContent) {
t.Fatal("managed work seed copy content mismatch")
}
got, err := db.GetImageByName(context.Background(), "default")
if err != nil {
t.Fatalf("GetImageByName: %v", err)
}
if got.RootfsPath != image.RootfsPath || !got.Managed || got.ArtifactDir != image.ArtifactDir {
t.Fatalf("stored promoted image = %+v, want %+v", got, image)
}
gotVM, err := db.GetVMByID(context.Background(), vm.ID)
if err != nil {
t.Fatalf("GetVMByID: %v", err)
}
if gotVM.ImageID != existing.ID {
t.Fatalf("VM image ID = %q, want preserved %q", gotVM.ImageID, existing.ID)
}
}
func TestPromoteImageRejectsManagedImage(t *testing.T) {
dir := t.TempDir()
rootfs, kernel, initrd, modulesDir, packages := writeDefaultImageArtifacts(t, dir)
db := openDefaultImageStore(t, dir)
now := time.Date(2026, time.March, 20, 12, 0, 0, 0, time.UTC)
if err := db.UpsertImage(context.Background(), model.Image{
ID: "managed-id",
Name: "default",
Managed: true,
ArtifactDir: filepath.Join(dir, "images", "managed-id"),
RootfsPath: rootfs,
KernelPath: kernel,
InitrdPath: initrd,
ModulesDir: modulesDir,
PackagesPath: packages,
CreatedAt: now,
UpdatedAt: now,
}); err != nil {
t.Fatalf("UpsertImage: %v", err)
}
d := &Daemon{
layout: modelPathsLayoutForTest(dir),
store: db,
}
_, err := d.PromoteImage(context.Background(), "default")
if err == nil || !strings.Contains(err.Error(), "already managed") {
t.Fatalf("PromoteImage(managed) error = %v", err)
}
}
func TestPromoteImageSkipsMissingWorkSeed(t *testing.T) {
dir := t.TempDir()
rootfs, kernel, initrd, modulesDir, packages := writeDefaultImageArtifacts(t, dir)
db := openDefaultImageStore(t, dir)
now := time.Date(2026, time.March, 20, 12, 0, 0, 0, time.UTC)
existing := model.Image{
ID: "promote-missing-seed",
Name: "default",
Managed: false,
RootfsPath: rootfs,
WorkSeedPath: filepath.Join(dir, "missing.work-seed.ext4"),
KernelPath: kernel,
InitrdPath: initrd,
ModulesDir: modulesDir,
PackagesPath: packages,
CreatedAt: now,
UpdatedAt: now,
}
if err := db.UpsertImage(context.Background(), existing); err != nil {
t.Fatalf("UpsertImage: %v", err)
}
d := &Daemon{
layout: modelPathsLayoutForTest(dir),
store: db,
}
image, err := d.PromoteImage(context.Background(), "default")
if err != nil {
t.Fatalf("PromoteImage: %v", err)
}
if image.WorkSeedPath != "" {
t.Fatalf("WorkSeedPath = %q, want empty for missing source work seed", image.WorkSeedPath)
}
if _, err := os.Stat(filepath.Join(image.ArtifactDir, "work-seed.ext4")); !os.IsNotExist(err) {
t.Fatalf("managed work-seed should not exist, stat error = %v", err)
}
}
func openDefaultImageStore(t *testing.T, dir string) *store.Store {
t.Helper()
db, err := store.Open(filepath.Join(dir, "state.db"))
if err != nil {
t.Fatalf("open store: %v", err)
}
t.Cleanup(func() {
_ = db.Close()
})
return db
}
func writeDefaultImageArtifacts(t *testing.T, dir string) (rootfs, kernel, initrd, modulesDir, packages string) {
t.Helper()
rootfs = filepath.Join(dir, "rootfs-docker.ext4")
kernel = filepath.Join(dir, "vmlinux")
initrd = filepath.Join(dir, "initrd.img")
modulesDir = filepath.Join(dir, "modules")
packages = filepath.Join(dir, "packages.apt")
files := []string{
rootfs,
kernel,
initrd,
packages,
filepath.Join(modulesDir, "modules.dep"),
}
for _, path := range files {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("mkdir %s: %v", filepath.Dir(path), err)
for _, path := range []string{got.RootfsPath, got.KernelPath, got.InitrdPath, got.ModulesDir} {
if !strings.HasPrefix(path, got.ArtifactDir) {
t.Fatalf("artifact path %q does not live under %q", path, got.ArtifactDir)
}
if err := os.WriteFile(path, []byte("test"), 0o644); err != nil {
t.Fatalf("write %s: %v", path, err)
if _, err := os.Stat(path); err != nil {
t.Fatalf("stat %s: %v", path, err)
}
}
return rootfs, kernel, initrd, modulesDir, packages
}
func modelPathsLayoutForTest(dir string) paths.Layout {
return paths.Layout{
ImagesDir: filepath.Join(dir, "images"),
}
}
func TestStartVMDNSFailsWhenAddressBusy(t *testing.T) {
t.Parallel()
packetConn, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
t.Fatalf("ListenPacket: %v", err)
}
defer packetConn.Close()
d := &Daemon{}
if err := d.startVMDNS(packetConn.LocalAddr().String()); err == nil {
t.Fatal("startVMDNS() succeeded on occupied address, want failure")
}
}
func TestSetDNSPublishesIntoDaemonServer(t *testing.T) {
t.Parallel()
d := &Daemon{}
if err := d.startVMDNS("127.0.0.1:0"); err != nil {
t.Fatalf("startVMDNS: %v", err)
}
defer d.stopVMDNS()
if err := d.setDNS(context.Background(), "devbox", "172.16.0.8"); err != nil {
t.Fatalf("setDNS: %v", err)
}
if _, ok := d.vmDNS.Lookup("devbox.vm"); !ok {
t.Fatal("devbox.vm missing after setDNS")
}
}
func TestDispatchUsesPassedContext(t *testing.T) {
t.Parallel()
db := openDefaultImageStore(t, t.TempDir())
d := &Daemon{store: db}
ctx, cancel := context.WithCancel(context.Background())
cancel()
resp := d.dispatch(ctx, rpc.Request{
Version: rpc.Version,
Method: "vm.list",
Params: mustJSON(t, api.Empty{}),
})
if resp.OK {
t.Fatal("dispatch() succeeded with canceled context")
}
if resp.Error == nil || !strings.Contains(resp.Error.Message, context.Canceled.Error()) {
t.Fatalf("dispatch() error = %+v, want context canceled", resp.Error)
}
}
func TestHandleConnCancelsRequestWhenClientDisconnects(t *testing.T) {
t.Parallel()
server, client := net.Pipe()
defer client.Close()
requestCanceled := make(chan struct{})
done := make(chan struct{})
d := &Daemon{
closing: make(chan struct{}),
requestHandler: func(ctx context.Context, req rpc.Request) rpc.Response {
if req.Method != "block" {
t.Errorf("request method = %q, want block", req.Method)
}
<-ctx.Done()
close(requestCanceled)
return rpc.NewError("operation_failed", ctx.Err().Error())
},
}
go func() {
d.handleConn(server)
close(done)
}()
if err := json.NewEncoder(client).Encode(rpc.Request{Version: rpc.Version, Method: "block"}); err != nil {
t.Fatalf("encode request: %v", err)
}
if err := client.Close(); err != nil {
t.Fatalf("close client: %v", err)
}
select {
case <-requestCanceled:
case <-time.After(2 * time.Second):
t.Fatal("request context was not canceled after client disconnect")
}
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("handleConn did not return after client disconnect")
}
}
func TestWatchRequestDisconnectCancelsContextOnEOF(t *testing.T) {
t.Parallel()
server, client := net.Pipe()
defer server.Close()
reader := bufio.NewReader(server)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
d := &Daemon{closing: make(chan struct{})}
stop := d.watchRequestDisconnect(server, reader, "block", cancel)
defer stop()
if err := client.Close(); err != nil {
t.Fatalf("close client: %v", err)
}
select {
case <-ctx.Done():
if !strings.Contains(ctx.Err().Error(), context.Canceled.Error()) {
t.Fatalf("ctx.Err() = %v, want canceled", ctx.Err())
}
case <-time.After(2 * time.Second):
t.Fatal("watchRequestDisconnect did not cancel context")
}
}
func mustJSON(t *testing.T, v any) []byte {
t.Helper()
data, err := json.Marshal(v)
if err != nil {
t.Fatalf("json.Marshal(%T): %v", v, err)
}
return data
}