test(metis): cover plan execution failures

This commit is contained in:
codex 2026-04-21 05:29:08 -03:00
parent 5ddff4f385
commit 65b68d0bfd

View File

@ -10,6 +10,7 @@ import (
"testing" "testing"
"time" "time"
"metis/pkg/config"
"metis/pkg/inventory" "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() } func timeNow() time.Time { return time.Now() }