package daemon import ( "context" "errors" "os" "os/exec" "path/filepath" "strings" "testing" "banger/internal/api" "banger/internal/imagepull" "banger/internal/model" "banger/internal/paths" "banger/internal/system" "github.com/google/go-containerregistry/pkg/name" ) func writeFakeKernelTriple(t *testing.T) (kernelPath, initrdPath, modulesDir string) { t.Helper() dir := t.TempDir() kernelPath = filepath.Join(dir, "vmlinux") if err := os.WriteFile(kernelPath, []byte("kernel"), 0o644); err != nil { t.Fatal(err) } initrdPath = filepath.Join(dir, "initrd.img") if err := os.WriteFile(initrdPath, []byte("initrd"), 0o644); err != nil { t.Fatal(err) } modulesDir = filepath.Join(dir, "modules") if err := os.MkdirAll(modulesDir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(modulesDir, "modules.dep"), []byte(""), 0o644); err != nil { t.Fatal(err) } return } // stubFinalizePulledRootfs is a no-op seam substitute that skips the real // debugfs + vsock-agent-binary injection machinery during daemon tests. func stubFinalizePulledRootfs(_ context.Context, _ string, _ imagepull.Metadata) error { return nil } // stubPullAndFlatten writes a fixed file tree into destDir, simulating a // successful OCI pull without the network or tarball machinery. func stubPullAndFlatten(_ context.Context, _ string, _ string, destDir string) (imagepull.Metadata, error) { if err := os.MkdirAll(filepath.Join(destDir, "etc"), 0o755); err != nil { return imagepull.Metadata{}, err } if err := os.WriteFile(filepath.Join(destDir, "etc", "hello"), []byte("world"), 0o644); err != nil { return imagepull.Metadata{}, err } if err := os.WriteFile(filepath.Join(destDir, "marker"), []byte("ok"), 0o644); err != nil { return imagepull.Metadata{}, err } // Tiny synthetic metadata — daemon-level tests exercise the seam // plumbing, not the ownership pass itself. return imagepull.Metadata{Entries: map[string]imagepull.FileMeta{}}, nil } func TestPullImageHappyPath(t *testing.T) { if _, err := exec.LookPath("mkfs.ext4"); err != nil { t.Skip("mkfs.ext4 not available; skipping") } imagesDir := t.TempDir() cacheDir := t.TempDir() kernel, initrd, modules := writeFakeKernelTriple(t) d := &Daemon{ layout: paths.Layout{ImagesDir: imagesDir, OCICacheDir: cacheDir}, store: openDaemonStore(t), runner: system.NewRunner(), } d.img = &ImageService{ layout: d.layout, store: d.store, runner: d.runner, pullAndFlatten: stubPullAndFlatten, finalizePulledRootfs: stubFinalizePulledRootfs, } wireServices(d) image, err := d.img.PullImage(context.Background(), api.ImagePullParams{ Ref: "docker.io/library/debian:bookworm", KernelPath: kernel, InitrdPath: initrd, ModulesDir: modules, }) if err != nil { t.Fatalf("PullImage: %v", err) } if image.Name != "debian-bookworm" { t.Errorf("Name = %q, want debian-bookworm", image.Name) } if !image.Managed { t.Errorf("expected Managed=true") } if image.ArtifactDir == "" || !strings.HasPrefix(image.ArtifactDir, imagesDir) { t.Errorf("ArtifactDir = %q, want under %q", image.ArtifactDir, imagesDir) } for _, rel := range []string{"rootfs.ext4", "kernel", "initrd.img", "modules"} { if _, err := os.Stat(filepath.Join(image.ArtifactDir, rel)); err != nil { t.Errorf("missing artifact %s: %v", rel, err) } } // Staging dir should be gone after publish. stagings, _ := filepath.Glob(filepath.Join(imagesDir, "*.staging")) if len(stagings) != 0 { t.Errorf("staging dirs left behind: %v", stagings) } } func TestPullImageRejectsExistingName(t *testing.T) { imagesDir := t.TempDir() kernel, _, _ := writeFakeKernelTriple(t) d := &Daemon{ layout: paths.Layout{ImagesDir: imagesDir, OCICacheDir: t.TempDir()}, store: openDaemonStore(t), runner: system.NewRunner(), } d.img = &ImageService{ layout: d.layout, store: d.store, runner: d.runner, pullAndFlatten: stubPullAndFlatten, finalizePulledRootfs: stubFinalizePulledRootfs, } wireServices(d) // Seed a preexisting image with the would-be derived name. id, _ := model.NewID() if err := d.store.UpsertImage(context.Background(), model.Image{ ID: id, Name: "debian-bookworm", CreatedAt: model.Now(), UpdatedAt: model.Now(), }); err != nil { t.Fatal(err) } _, err := d.img.PullImage(context.Background(), api.ImagePullParams{ Ref: "docker.io/library/debian:bookworm", KernelPath: kernel, }) if err == nil || !strings.Contains(err.Error(), "already exists") { t.Fatalf("expected already-exists error, got %v", err) } } func TestPullImageRequiresKernel(t *testing.T) { d := &Daemon{ layout: paths.Layout{ImagesDir: t.TempDir(), OCICacheDir: t.TempDir()}, store: openDaemonStore(t), runner: system.NewRunner(), } d.img = &ImageService{ layout: d.layout, store: d.store, runner: d.runner, pullAndFlatten: stubPullAndFlatten, finalizePulledRootfs: stubFinalizePulledRootfs, } wireServices(d) _, err := d.img.PullImage(context.Background(), api.ImagePullParams{ Ref: "docker.io/library/debian:bookworm", }) if err == nil || !strings.Contains(err.Error(), "kernel") { t.Fatalf("expected kernel-required error, got %v", err) } } func TestPullImageCleansStagingOnFailure(t *testing.T) { imagesDir := t.TempDir() kernel, _, _ := writeFakeKernelTriple(t) failureSeam := func(_ context.Context, _ string, _ string, _ string) (imagepull.Metadata, error) { return imagepull.Metadata{}, errors.New("network borked") } d := &Daemon{ layout: paths.Layout{ImagesDir: imagesDir, OCICacheDir: t.TempDir()}, store: openDaemonStore(t), runner: system.NewRunner(), } d.img = &ImageService{ layout: d.layout, store: d.store, runner: d.runner, pullAndFlatten: failureSeam, finalizePulledRootfs: stubFinalizePulledRootfs, } wireServices(d) _, err := d.img.PullImage(context.Background(), api.ImagePullParams{ Ref: "docker.io/library/debian:bookworm", KernelPath: kernel, }) if err == nil || !strings.Contains(err.Error(), "network borked") { t.Fatalf("expected propagated pull error, got %v", err) } stagings, _ := filepath.Glob(filepath.Join(imagesDir, "*.staging")) if len(stagings) != 0 { t.Errorf("staging dir left behind on failure: %v", stagings) } } func TestDefaultImageNameFromRef(t *testing.T) { cases := []struct { in string want string }{ {"docker.io/library/debian:bookworm", "debian-bookworm"}, {"alpine:3.20", "alpine-3-20"}, {"docker.io/library/debian", "debian"}, {"ghcr.io/some/org/my-image:v2.1", "my-image-v2-1"}, } for _, tc := range cases { ref, err := name.ParseReference(tc.in) if err != nil { t.Fatalf("parse %s: %v", tc.in, err) } if got := defaultImageNameFromRef(ref); got != tc.want { t.Errorf("defaultImageNameFromRef(%s) = %q, want %q", tc.in, got, tc.want) } } }