banger/internal/daemon/daemon_test.go
Thales Maciel 3ed78fdcfc
Add experimental Void guest workflow and vsock agent
Make iterating on a Firecracker-friendly Void guest practical without replacing the Debian default image path.

Add local Void rootfs build/register/verify plumbing, a language-agnostic dev package baseline, and guest SSH/work-disk hardening so new images use the runtime bundle key, keep a normal root bash environment, and repair stale nested /root layouts on restart.

Replace the guest PING/PONG responder with an HTTP /healthz agent over vsock, rename the runtime bundle and config surface from ping helper to agent while still accepting the legacy keys, and route the post-SSH reminder through the new vm.health path.

Validated with GOCACHE=/tmp/banger-gocache go test ./..., make build, bash -n customize.sh make-rootfs-void.sh, and git diff --check.
2026-03-19 14:51:25 -03:00

542 lines
15 KiB
Go

package daemon
import (
"bufio"
"context"
"encoding/json"
"net"
"os"
"path/filepath"
"strings"
"testing"
"time"
"banger/internal/api"
"banger/internal/model"
"banger/internal/rpc"
"banger/internal/store"
)
func TestEnsureDefaultImageUsesConfiguredDefaultRootfs(t *testing.T) {
dir := t.TempDir()
rootfs, kernel, _, _, _ := writeDefaultImageArtifacts(t, dir)
db := openDefaultImageStore(t, dir)
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)
}
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")
}
}
func TestEnsureDefaultImageLeavesCurrentUnmanagedDefaultUntouched(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)
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,
}
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/rootfs-docker.ext4",
KernelPath: "/home/thales/projects/personal/banger/wtf/root/boot/vmlinux-6.8.0-94-generic",
InitrdPath: "/home/thales/projects/personal/banger/wtf/root/boot/initrd.img-6.8.0-94-generic",
ModulesDir: "/home/thales/projects/personal/banger/wtf/root/lib/modules/6.8.0-94-generic",
PackagesPath: "/home/thales/projects/personal/banger/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)
}
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 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)
}
if err := os.WriteFile(path, []byte("test"), 0o644); err != nil {
t.Fatalf("write %s: %v", path, err)
}
}
return rootfs, kernel, initrd, modulesDir, packages
}
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
}