package plan import ( "context" "crypto/sha256" "encoding/hex" "os" "path/filepath" "strings" "testing" "time" "metis/pkg/inventory" ) func TestPlanBuildFilesAndExecuteBranches(t *testing.T) { dir := t.TempDir() base := filepath.Join(dir, "base.img") baseContent := []byte("image") if err := os.WriteFile(base, baseContent, 0o644); err != nil { t.Fatal(err) } sum := sha256.Sum256(baseContent) inv := &inventory.Inventory{ Classes: []inventory.NodeClass{{ Name: "rpi4", Arch: "arm64", OS: "armbian", Image: "file://" + base, Checksum: "sha256:" + hex.EncodeToString(sum[:]), DefaultLabels: map[string]string{ "node-role.kubernetes.io/worker": "true", }, BootOverlay: filepath.Join(dir, "boot-overlay"), RootOverlay: filepath.Join(dir, "root-overlay"), }}, Nodes: []inventory.NodeSpec{{ Name: "titan-15", Class: "rpi4", Hostname: "titan-15", IP: "192.168.22.43", K3sRole: "agent", K3sURL: "https://192.168.22.7:6443", K3sToken: "token", SSHUser: "atlas", SSHAuthorized: []string{"ssh-ed25519 AAA"}, LonghornDisks: []inventory.LonghornDisk{{Mountpoint: "/var/lib/longhorn", UUID: "u1"}}, }}, } if err := os.MkdirAll(inv.Classes[0].BootOverlay, 0o755); err != nil { t.Fatal(err) } if err := os.MkdirAll(inv.Classes[0].RootOverlay, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(inv.Classes[0].BootOverlay, "boot.txt"), []byte("boot"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(inv.Classes[0].RootOverlay, "root.txt"), []byte("root"), 0o600); err != nil { t.Fatal(err) } if _, err := Build(inv, "missing", "", dir); err == nil { t.Fatal("expected Build to fail for missing node") } p, err := Build(inv, "titan-15", "", dir) if err != nil { t.Fatalf("Build: %v", err) } if p.Device != "/dev/sdX" || !strings.Contains(strings.Join(actionDetails(p.Actions), " "), "Inject hostname/network/k3s config") { t.Fatalf("unexpected plan: %#v", p) } if got := cacheName("foo.img.xz"); got != "foo.img" { t.Fatalf("cacheName = %q", got) } files, err := Files(inv, "titan-15") if err != nil { t.Fatalf("Files: %v", err) } if len(files) == 0 { t.Fatal("expected files") } if got := cloudInitUserData(nil, nil); got != "" { t.Fatalf("cloudInitUserData nil = %q", got) } if got := allowK3sNodeLabel("agent", "node-role.kubernetes.io/master"); got { t.Fatal("agent should reject node-role labels") } // Inject is a thin wrapper around maybeInject; exercise both the no-op and // path-setting branches. if err := Inject(inv, "titan-15", "", ""); err != nil { t.Fatalf("Inject noop: %v", err) } boot := filepath.Join(dir, "boot") root := filepath.Join(dir, "root") if err := os.MkdirAll(boot, 0o755); err != nil { t.Fatal(err) } if err := os.MkdirAll(root, 0o755); err != nil { t.Fatal(err) } if err := Inject(inv, "titan-15", boot, root); err != nil { t.Fatalf("Inject with paths: %v", err) } cacheDir := filepath.Join(dir, "cache") output := filepath.Join(dir, "output.img") t.Setenv("PATH", dir) if err := BuildImageFile(context.Background(), inv, "titan-15", cacheDir, output); err == nil { t.Fatal("expected BuildImageFile to fail without xz/debugfs setup") } if _, err := Execute(inv, "titan-15", "/dev/sdX", cacheDir, true); err == nil { t.Fatal("expected Execute to reject placeholder device") } if _, err := Execute(inv, "titan-15", "/dev/sdz", cacheDir, false); err != nil { t.Fatalf("Execute dry-run: %v", err) } } func actionDetails(actions []Action) []string { out := make([]string, 0, len(actions)) for _, action := range actions { out = append(out, action.Detail) } return out } func TestPlanMiscBranches(t *testing.T) { if !NextRunStale(timeNow().Add(-time.Hour), time.Minute) { t.Fatal("expected NextRunStale") } } func timeNow() time.Time { return time.Now() }