Manage image artifacts and show VM create progress
Stop relying on ad hoc rootfs handling by adding image promotion, managed work-seed fingerprint metadata, and lazy self-healing for older managed images after the first create. Rebuild guest images with baked SSH access, a guest NIC bootstrap, and default opencode services, and add the staged Void kernel/initramfs/modules workflow so void-exp uses a matching Void boot stack. Replace the opaque blocking vm.create RPC with a begin/status flow that prints live stages in the CLI while still waiting for vsock health and opencode on guest port 4096. Validate with GOCACHE=/tmp/banger-gocache go test ./... and live void-exp create/delete smoke runs.
This commit is contained in:
parent
9f09b0d25c
commit
30f0c0b54a
37 changed files with 2334 additions and 99 deletions
|
|
@ -2,6 +2,7 @@ package daemon
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net"
|
||||
|
|
@ -13,6 +14,7 @@ import (
|
|||
|
||||
"banger/internal/api"
|
||||
"banger/internal/model"
|
||||
"banger/internal/paths"
|
||||
"banger/internal/rpc"
|
||||
"banger/internal/store"
|
||||
)
|
||||
|
|
@ -368,6 +370,178 @@ func TestRegisterImageRejectsManagedOverwrite(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
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"))
|
||||
|
|
@ -405,6 +579,12 @@ func writeDefaultImageArtifacts(t *testing.T, dir string) (rootfs, kernel, initr
|
|||
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()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue