The `image build` flow spun up a transient Firecracker VM, SSHed in, and ran a large bash provisioning script to derive a new managed image from an existing one. It overlapped heavily with the golden- image Dockerfile flow (same mise/docker/tmux/opencode install logic duplicated in Go as `imagemgr.BuildProvisionScript`) and had far more machinery: async op state, RPC begin/status/cancel, webui form + operation page, preflight checks, API types, tests. For custom images, writing a Dockerfile is simpler and more reproducible. Removed end-to-end: - CLI `image build` subcommand + `absolutizeImageBuildPaths`. - Daemon: BuildImage method, imagebuild.go (transient-VM orchestration), image_build_ops.go (async begin/status/cancel), imagemgr/build.go (the 247-line provisioning script generator and all its append* helpers), validateImageBuildPrereqs + addImageBuildPrereqs. - RPC dispatches for image.build / .begin / .status / .cancel. - opstate registry `imageBuildOps`, daemon seam `imageBuild`, background pruner call. - API types: ImageBuildParams, ImageBuildOperation, ImageBuildBeginResult, ImageBuildStatusParams, ImageBuildStatusResult; model type ImageBuildRequest. - Web UI: Backend interface methods, handlers, form, routes, template branches (images.html build form, operation.html build branch, dashboard.html Build button). - Tests that directly exercised BuildImage. Doctor polish (task C): - Drop the "image build" preflight section entirely (its raison d'être is gone). - Default-image check now accepts "not local but in imagecat" as OK: vm create auto-pulls on first use. Only flag when the image is neither locally registered nor in the catalog. Net: 24 files touched, 1,373 lines deleted, 25 added. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
118 lines
3.3 KiB
Go
118 lines
3.3 KiB
Go
package daemon
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"banger/internal/api"
|
|
"banger/internal/buildinfo"
|
|
"banger/internal/model"
|
|
"banger/internal/paths"
|
|
"banger/internal/rpc"
|
|
"banger/internal/system"
|
|
)
|
|
|
|
func TestRegisterImageRequiresKernel(t *testing.T) {
|
|
rootfs := filepath.Join(t.TempDir(), "rootfs.ext4")
|
|
if err := os.WriteFile(rootfs, []byte("rootfs"), 0o644); err != nil {
|
|
t.Fatalf("write rootfs: %v", err)
|
|
}
|
|
d := &Daemon{store: openDaemonStore(t)}
|
|
|
|
_, err := d.RegisterImage(context.Background(), api.ImageRegisterParams{
|
|
Name: "missing-kernel",
|
|
RootfsPath: rootfs,
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "kernel path is required") {
|
|
t.Fatalf("RegisterImage() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDispatchPingIncludesBuildInfo(t *testing.T) {
|
|
d := &Daemon{pid: 42, webURL: "http://127.0.0.1:7777"}
|
|
|
|
resp := d.dispatch(context.Background(), rpc.Request{Version: rpc.Version, Method: "ping"})
|
|
if !resp.OK {
|
|
t.Fatalf("dispatch(ping) = %+v, want ok", resp)
|
|
}
|
|
|
|
var got api.PingResult
|
|
if err := json.Unmarshal(resp.Result, &got); err != nil {
|
|
t.Fatalf("Unmarshal(PingResult): %v", err)
|
|
}
|
|
|
|
info := buildinfo.Current()
|
|
if got.Status != "ok" || got.PID != 42 || got.WebURL != "http://127.0.0.1:7777" {
|
|
t.Fatalf("PingResult = %+v, want status/pid/weburl populated", got)
|
|
}
|
|
if got.Version != info.Version || got.Commit != info.Commit || got.BuiltAt != info.BuiltAt {
|
|
t.Fatalf("PingResult build info = %+v, want %+v", got, info)
|
|
}
|
|
}
|
|
|
|
func TestPromoteImageCopiesBootArtifactsIntoArtifactDir(t *testing.T) {
|
|
dir := t.TempDir()
|
|
rootfs := filepath.Join(dir, "rootfs.ext4")
|
|
kernel := filepath.Join(dir, "vmlinux")
|
|
initrd := filepath.Join(dir, "initrd.img")
|
|
modulesDir := filepath.Join(dir, "modules")
|
|
if err := os.MkdirAll(modulesDir, 0o755); err != nil {
|
|
t.Fatalf("mkdir modules: %v", err)
|
|
}
|
|
for path, data := range map[string]string{
|
|
rootfs: "rootfs",
|
|
kernel: "kernel",
|
|
initrd: "initrd",
|
|
filepath.Join(modulesDir, "depmod"): "modules",
|
|
} {
|
|
if err := os.WriteFile(path, []byte(data), 0o644); err != nil {
|
|
t.Fatalf("write %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
db := openDaemonStore(t)
|
|
image := model.Image{
|
|
ID: "img-promote",
|
|
Name: "void",
|
|
Managed: false,
|
|
RootfsPath: rootfs,
|
|
KernelPath: kernel,
|
|
InitrdPath: initrd,
|
|
ModulesDir: modulesDir,
|
|
CreatedAt: model.Now(),
|
|
UpdatedAt: model.Now(),
|
|
}
|
|
if err := db.UpsertImage(context.Background(), image); err != nil {
|
|
t.Fatalf("UpsertImage: %v", err)
|
|
}
|
|
|
|
imagesDir := filepath.Join(dir, "images")
|
|
if err := os.MkdirAll(imagesDir, 0o755); err != nil {
|
|
t.Fatalf("mkdir images dir: %v", err)
|
|
}
|
|
|
|
d := &Daemon{
|
|
layout: paths.Layout{ImagesDir: imagesDir},
|
|
store: db,
|
|
runner: system.NewRunner(),
|
|
}
|
|
got, err := d.PromoteImage(context.Background(), image.Name)
|
|
if err != nil {
|
|
t.Fatalf("PromoteImage: %v", err)
|
|
}
|
|
if !got.Managed {
|
|
t.Fatal("promoted image should be managed")
|
|
}
|
|
for _, path := range []string{got.RootfsPath, got.KernelPath, got.InitrdPath, got.ModulesDir} {
|
|
if !strings.HasPrefix(path, got.ArtifactDir) {
|
|
t.Fatalf("artifact path %q does not live under %q", path, got.ArtifactDir)
|
|
}
|
|
if _, err := os.Stat(path); err != nil {
|
|
t.Fatalf("stat %s: %v", path, err)
|
|
}
|
|
}
|
|
}
|