From 65b68d0bfd227fe151a26bd2d33923fda7783a69 Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 21 Apr 2026 05:29:08 -0300 Subject: [PATCH] test(metis): cover plan execution failures --- pkg/plan/coverage_more_test.go | 206 +++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) diff --git a/pkg/plan/coverage_more_test.go b/pkg/plan/coverage_more_test.go index 8b7da00..7b73ae6 100644 --- a/pkg/plan/coverage_more_test.go +++ b/pkg/plan/coverage_more_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "metis/pkg/config" "metis/pkg/inventory" ) @@ -133,4 +134,209 @@ func TestPlanMiscBranches(t *testing.T) { } } +func TestBuildImageFileErrorBranches(t *testing.T) { + dir := t.TempDir() + raw := filepath.Join(dir, "base.img") + if err := os.WriteFile(raw, []byte("image"), 0o644); err != nil { + t.Fatal(err) + } + sum := imageChecksum(t, raw) + inv := &inventory.Inventory{ + Classes: []inventory.NodeClass{{Name: "c1", Image: "file://" + raw, Checksum: sum}}, + Nodes: []inventory.NodeSpec{{Name: "n1", Class: "c1", Hostname: "n1", K3sRole: "agent"}}, + } + if err := BuildImageFile(context.Background(), inv, "missing", filepath.Join(dir, "cache"), filepath.Join(dir, "out.img")); err == nil { + t.Fatal("expected missing node to fail plan build") + } + + badChecksum := &inventory.Inventory{ + Classes: []inventory.NodeClass{{Name: "c1", Image: "file://" + raw, Checksum: "sha256:bad"}}, + Nodes: []inventory.NodeSpec{{Name: "n1", Class: "c1", Hostname: "n1", K3sRole: "agent"}}, + } + if err := BuildImageFile(context.Background(), badChecksum, "n1", filepath.Join(dir, "cache-bad"), filepath.Join(dir, "bad.img")); err == nil { + t.Fatal("expected image verification failure") + } + + blockedParent := filepath.Join(dir, "blocked-parent") + if err := os.WriteFile(blockedParent, []byte("not-a-dir"), 0o644); err != nil { + t.Fatal(err) + } + if err := BuildImageFile(context.Background(), inv, "n1", filepath.Join(dir, "cache-copy"), filepath.Join(blockedParent, "out.img")); err == nil { + t.Fatal("expected base-image copy failure") + } + + overlayMissing := &inventory.Inventory{ + Classes: []inventory.NodeClass{{Name: "c1", Image: "file://" + raw, Checksum: sum, RootOverlay: filepath.Join(dir, "missing-overlay")}}, + Nodes: []inventory.NodeSpec{{Name: "n1", Class: "c1", Hostname: "n1", K3sRole: "agent"}}, + } + if err := BuildImageFile(context.Background(), overlayMissing, "n1", filepath.Join(dir, "cache-overlay"), filepath.Join(dir, "overlay.img")); err == nil { + t.Fatal("expected overlay resolution failure") + } + + t.Setenv("PATH", t.TempDir()) + if err := BuildImageFile(context.Background(), inv, "n1", filepath.Join(dir, "cache-inject"), filepath.Join(dir, "inject.img")); err == nil { + t.Fatal("expected rootfs injection failure without helper tools") + } +} + +func TestExecuteAdditionalErrorBranches(t *testing.T) { + dir := t.TempDir() + raw := filepath.Join(dir, "base.img") + if err := os.WriteFile(raw, []byte("image"), 0o644); err != nil { + t.Fatal(err) + } + sum := imageChecksum(t, raw) + inv := &inventory.Inventory{ + Classes: []inventory.NodeClass{{Name: "c1", Image: "file://" + raw, Checksum: sum}}, + Nodes: []inventory.NodeSpec{{Name: "n1", Class: "c1", Hostname: "n1", K3sRole: "agent"}}, + } + if _, err := Execute(inv, "missing", filepath.Join(dir, "disk.img"), filepath.Join(dir, "cache-missing"), false); err == nil { + t.Fatal("expected missing node execute failure") + } + + badChecksum := &inventory.Inventory{ + Classes: []inventory.NodeClass{{Name: "c1", Image: "file://" + raw, Checksum: "sha256:bad"}}, + Nodes: []inventory.NodeSpec{{Name: "n1", Class: "c1", Hostname: "n1", K3sRole: "agent"}}, + } + if _, err := Execute(badChecksum, "n1", filepath.Join(dir, "disk-bad.img"), filepath.Join(dir, "cache-bad"), false); err == nil { + t.Fatal("expected execute download verification failure") + } + + blockedParent := filepath.Join(dir, "blocked-parent") + if err := os.WriteFile(blockedParent, []byte("not-a-dir"), 0o644); err != nil { + t.Fatal(err) + } + if _, err := Execute(inv, "n1", filepath.Join(blockedParent, "disk.img"), filepath.Join(dir, "cache-copy"), true); err == nil { + t.Fatal("expected execute write failure") + } + + blockedRoot := filepath.Join(dir, "blocked-root") + if err := os.WriteFile(blockedRoot, []byte("not-a-dir"), 0o644); err != nil { + t.Fatal(err) + } + t.Setenv("METIS_ROOT_PATH", blockedRoot) + if _, err := Execute(inv, "n1", filepath.Join(dir, "disk-inject.img"), filepath.Join(dir, "cache-inject"), true); err == nil { + t.Fatal("expected execute inject failure") + } + t.Setenv("METIS_ROOT_PATH", "") + t.Setenv("METIS_BOOT_PATH", "") + if got := maybeAutoMount("/dev/loop9"); got != nil { + t.Fatalf("expected automount disabled, got %#v", got) + } + + scripts := filepath.Join(dir, "bin") + if err := os.MkdirAll(scripts, 0o755); err != nil { + t.Fatal(err) + } + write := func(name, body string) { + path := filepath.Join(scripts, name) + if err := os.WriteFile(path, []byte("#!/usr/bin/env bash\nset -eu\n"+body+"\n"), 0o755); err != nil { + t.Fatalf("write %s: %v", name, err) + } + } + write("mount", `exit 2`) + write("umount", `exit 0`) + write("losetup", `exit 0`) + t.Setenv("PATH", scripts+string(os.PathListSeparator)+os.Getenv("PATH")) + t.Setenv("METIS_AUTO_MOUNT", "1") + if got := maybeAutoMount("/dev/loop9"); got != nil { + t.Fatalf("expected automount setup failure to be ignored, got %#v", got) + } +} + +func TestInjectAdditionalBranches(t *testing.T) { + dir := t.TempDir() + inv := &inventory.Inventory{ + Classes: []inventory.NodeClass{{Name: "c1", Image: "file:///tmp/base.img"}}, + Nodes: []inventory.NodeSpec{{ + Name: "n1", + Class: "c1", + Hostname: "n1", + IP: "10.0.0.1", + K3sRole: "agent", + SSHUser: "atlas", + SSHAuthorized: []string{"ssh-ed25519 AAA"}, + }}, + } + root := filepath.Join(dir, "root") + boot := filepath.Join(dir, "boot") + if err := os.MkdirAll(root, 0o755); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(boot, 0o755); err != nil { + t.Fatal(err) + } + + t.Setenv("METIS_ROOT_PATH", root) + if err := maybeInject(inv, "missing"); err == nil { + t.Fatal("expected maybeInject to report missing node") + } + + t.Setenv("METIS_BOOT_PATH", boot) + t.Setenv("METIS_ROOT_PATH", "") + if err := maybeInject(inv, "n1"); err != nil { + t.Fatalf("maybeInject boot-only: %v", err) + } + if _, err := os.Stat(filepath.Join(boot, "user-data")); err != nil { + t.Fatalf("expected boot-only injection to keep boot files: %v", err) + } + + t.Setenv("METIS_BOOT_PATH", "") + t.Setenv("METIS_ROOT_PATH", root) + if err := maybeInject(inv, "n1"); err != nil { + t.Fatalf("maybeInject root-only: %v", err) + } + if _, err := os.Stat(filepath.Join(root, "etc/metis/node.json")); err != nil { + t.Fatalf("expected root-only injection to keep root files: %v", err) + } + + oldBoot := filepath.Join(dir, "old-boot") + oldRoot := filepath.Join(dir, "old-root") + if err := os.Setenv("METIS_BOOT_PATH", oldBoot); err != nil { + t.Fatal(err) + } + if err := os.Setenv("METIS_ROOT_PATH", oldRoot); err != nil { + t.Fatal(err) + } + if err := Inject(inv, "n1", "", ""); err != nil { + t.Fatalf("Inject with existing env restoration: %v", err) + } + if os.Getenv("METIS_BOOT_PATH") != oldBoot || os.Getenv("METIS_ROOT_PATH") != oldRoot { + t.Fatalf("Inject did not restore env: boot=%q root=%q", os.Getenv("METIS_BOOT_PATH"), os.Getenv("METIS_ROOT_PATH")) + } +} + +func TestInjectHelperErrorBranches(t *testing.T) { + if got := fstabAppendContent(&config.NodeConfig{Fstab: []config.FstabEntry{ + {Label: "DATA", Mountpoint: "/mnt/data", FS: "ext4", Options: "defaults"}, + {Mountpoint: "/mnt/empty", FS: "none", Options: "defaults"}, + }}); !strings.Contains(got, "LABEL=DATA /mnt/data ext4 defaults 0 0") || !strings.Contains(got, "none /mnt/empty none defaults 0 0") { + t.Fatalf("fstab label/default branches missing: %s", got) + } + + t.Setenv("VAULT_ADDR", "http://127.0.0.1:1") + if got := fetchSecrets("n1"); got != nil { + t.Fatalf("expected Vault fetch failure to return nil, got %#v", got) + } + + files, err := collectOverlays(nil) + if err != nil || len(files) != 0 { + t.Fatalf("collectOverlays(nil) = %#v err=%v", files, err) + } + if _, err := collectOverlays(&inventory.NodeClass{BootOverlay: filepath.Join(t.TempDir(), "missing-boot")}); err == nil { + t.Fatal("expected missing boot overlay to fail") + } + if _, err := collectOverlays(&inventory.NodeClass{RootOverlay: filepath.Join(t.TempDir(), "missing-root")}); err == nil { + t.Fatal("expected missing root overlay to fail") + } + + dir := t.TempDir() + if err := os.Symlink(filepath.Join(dir, "missing-target"), filepath.Join(dir, "broken-link")); err != nil { + t.Fatal(err) + } + if _, err := overlayFiles(dir, true); err == nil { + t.Fatal("expected broken overlay symlink to fail") + } +} + func timeNow() time.Time { return time.Now() }