Phase 4 of the daemon god-struct refactor. VM lifecycle, create-op
registry, handle cache, disk provisioning, stats polling, ports
query, and the per-VM lock set all move off *Daemon onto *VMService.
Daemon keeps thin forwarders only for FindVM / TouchVM (dispatch
surface) and is otherwise out of VM lifecycle. Lazy-init via
d.vmSvc() mirrors the earlier services so test literals like
\`&Daemon{store: db, runner: r}\` still get a functional service
without spelling one out.
Three small cleanups along the way:
* preflight helpers (validateStartPrereqs / addBaseStartPrereqs
/ addBaseStartCommandPrereqs / validateWorkDiskResizePrereqs)
move with the VM methods that call them.
* cleanupRuntime / rebuildDNS move to *VMService, with
HostNetwork primitives (findFirecrackerPID, cleanupDMSnapshot,
killVMProcess, releaseTap, waitForExit, sendCtrlAltDel)
reached through s.net instead of the hostNet() facade.
* vsockAgentBinary becomes a package-level function so both
*Daemon (doctor) and *VMService (preflight) call one entry
point instead of each owning a forwarder method.
WorkspaceService's peer deps switch from eager method values to
closures — vmSvc() constructs VMService with WorkspaceService as a
peer, so resolving d.vmSvc().FindVM at construction time recursed
through workspaceSvc() → vmSvc(). Closures defer the lookup to call
time.
Pure code motion: build + unit tests green, lint clean. No RPC
surface or lock-ordering changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
146 lines
4.6 KiB
Go
146 lines
4.6 KiB
Go
package daemon
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"banger/internal/imagecat"
|
|
"banger/internal/model"
|
|
"banger/internal/paths"
|
|
"banger/internal/system"
|
|
)
|
|
|
|
func TestFindOrAutoPullImageReturnsLocalWithoutPulling(t *testing.T) {
|
|
d := &Daemon{
|
|
layout: paths.Layout{ImagesDir: t.TempDir()},
|
|
store: openDaemonStore(t),
|
|
runner: system.NewRunner(),
|
|
}
|
|
d.img = &ImageService{
|
|
layout: d.layout,
|
|
store: d.store,
|
|
runner: d.runner,
|
|
bundleFetch: func(context.Context, string, imagecat.CatEntry) (imagecat.Manifest, error) {
|
|
t.Fatal("bundleFetch should not be called when image is local")
|
|
return imagecat.Manifest{}, nil
|
|
},
|
|
}
|
|
id, _ := model.NewID()
|
|
if err := d.store.UpsertImage(context.Background(), model.Image{
|
|
ID: id,
|
|
Name: "my-local-image",
|
|
CreatedAt: model.Now(),
|
|
UpdatedAt: model.Now(),
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
image, err := d.vmSvc().findOrAutoPullImage(context.Background(), "my-local-image")
|
|
if err != nil {
|
|
t.Fatalf("findOrAutoPullImage: %v", err)
|
|
}
|
|
if image.Name != "my-local-image" {
|
|
t.Fatalf("Name = %q, want my-local-image", image.Name)
|
|
}
|
|
}
|
|
|
|
func TestFindOrAutoPullImagePullsFromCatalog(t *testing.T) {
|
|
imagesDir := t.TempDir()
|
|
kernelsDir := t.TempDir()
|
|
seedKernel(t, kernelsDir, "generic-6.12")
|
|
|
|
pullCalls := 0
|
|
d := &Daemon{
|
|
layout: paths.Layout{ImagesDir: imagesDir, KernelsDir: kernelsDir},
|
|
store: openDaemonStore(t),
|
|
runner: system.NewRunner(),
|
|
}
|
|
d.img = &ImageService{
|
|
layout: d.layout,
|
|
store: d.store,
|
|
runner: d.runner,
|
|
bundleFetch: func(ctx context.Context, destDir string, entry imagecat.CatEntry) (imagecat.Manifest, error) {
|
|
pullCalls++
|
|
return stubBundleFetch(imagecat.Manifest{KernelRef: "generic-6.12"})(ctx, destDir, entry)
|
|
},
|
|
}
|
|
// "debian-bookworm" is in the embedded imagecat catalog.
|
|
image, err := d.vmSvc().findOrAutoPullImage(context.Background(), "debian-bookworm")
|
|
if err != nil {
|
|
t.Fatalf("findOrAutoPullImage: %v", err)
|
|
}
|
|
if image.Name != "debian-bookworm" {
|
|
t.Fatalf("Name = %q, want debian-bookworm", image.Name)
|
|
}
|
|
if pullCalls != 1 {
|
|
t.Fatalf("bundleFetch calls = %d, want 1", pullCalls)
|
|
}
|
|
}
|
|
|
|
func TestFindOrAutoPullImageReturnsOriginalErrorWhenNotInCatalog(t *testing.T) {
|
|
d := &Daemon{
|
|
layout: paths.Layout{ImagesDir: t.TempDir()},
|
|
store: openDaemonStore(t),
|
|
runner: system.NewRunner(),
|
|
}
|
|
_, err := d.vmSvc().findOrAutoPullImage(context.Background(), "not-in-catalog-or-store")
|
|
if err == nil || !strings.Contains(err.Error(), "not found") {
|
|
t.Fatalf("err = %v, want not-found", err)
|
|
}
|
|
}
|
|
|
|
func TestReadOrAutoPullKernelReturnsLocalWithoutPulling(t *testing.T) {
|
|
kernelsDir := t.TempDir()
|
|
seedKernel(t, kernelsDir, "generic-6.12")
|
|
d := &Daemon{layout: paths.Layout{KernelsDir: kernelsDir}}
|
|
|
|
entry, err := d.imageSvc().readOrAutoPullKernel(context.Background(), "generic-6.12")
|
|
if err != nil {
|
|
t.Fatalf("readOrAutoPullKernel: %v", err)
|
|
}
|
|
if entry.Name != "generic-6.12" {
|
|
t.Fatalf("Name = %q", entry.Name)
|
|
}
|
|
}
|
|
|
|
func TestReadOrAutoPullKernelErrorsWhenNotInCatalog(t *testing.T) {
|
|
d := &Daemon{layout: paths.Layout{KernelsDir: t.TempDir()}}
|
|
_, err := d.imageSvc().readOrAutoPullKernel(context.Background(), "nonexistent-kernel")
|
|
if err == nil || !strings.Contains(err.Error(), "not found") {
|
|
t.Fatalf("err = %v, want not-found", err)
|
|
}
|
|
}
|
|
|
|
// TestReadOrAutoPullKernelSurfacesNonNotExistError covers the path where
|
|
// kernelcat.ReadLocal fails for a reason other than missing entry (e.g.
|
|
// corrupt manifest); the autopull logic should NOT try to fetch in that
|
|
// case since the entry clearly exists in some broken form.
|
|
func TestReadOrAutoPullKernelSurfacesNonNotExistError(t *testing.T) {
|
|
kernelsDir := t.TempDir()
|
|
// Seed a manifest that doesn't match the entry's own Name field —
|
|
// kernelcat.ReadLocal returns an error, not os.ErrNotExist.
|
|
dir := filepath.Join(kernelsDir, "broken-kernel")
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "manifest.json"), []byte(`{"name":"different-name"}`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d := &Daemon{layout: paths.Layout{KernelsDir: kernelsDir}}
|
|
_, err := d.imageSvc().readOrAutoPullKernel(context.Background(), "broken-kernel")
|
|
if err == nil {
|
|
t.Fatal("want error")
|
|
}
|
|
// Must not be wrapped in an "auto-pull" message — the corrupt-manifest
|
|
// failure should surface as the primary cause.
|
|
if strings.Contains(err.Error(), "not found in catalog") {
|
|
t.Fatalf("err = %v, should not claim 'not in catalog'", err)
|
|
}
|
|
// Sanity: ensure it's not os.ErrNotExist-compatible.
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("err = %v, should not be os.ErrNotExist", err)
|
|
}
|
|
}
|