Separate tracked source from generated artifacts so the repo root stops accumulating helper scripts, manifests, and local runtime outputs. Move manual shell entrypoints under scripts/, manifests under config/, and the Firecracker API reference under docs/reference/. Make build and runtimebundle now target build/bin, build/runtime, and build/dist as the canonical source-checkout paths. Update runtime discovery, helper scripts, tests, and docs to follow the new layout while keeping legacy source-checkout runtime fallbacks for existing local bundles during migration. Validated with bash -n on the moved scripts, make build, and GOCACHE=/tmp/banger-gocache go test ./....
722 lines
21 KiB
Go
722 lines
21 KiB
Go
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"
|
|
)
|
|
|
|
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/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)
|
|
}
|
|
|
|
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),
|
|
store: db,
|
|
}
|
|
|
|
image, err := d.PromoteImage(context.Background(), "default")
|
|
if err != nil {
|
|
t.Fatalf("PromoteImage: %v", err)
|
|
}
|
|
if !image.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)
|
|
}
|
|
if err := os.WriteFile(path, []byte("test"), 0o644); err != nil {
|
|
t.Fatalf("write %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
|
|
}
|