metis/pkg/plan/workflow_test.go

242 lines
6.6 KiB
Go

package plan
import (
"context"
"crypto/sha256"
"encoding/hex"
"os"
"path/filepath"
"strings"
"testing"
"metis/pkg/inventory"
)
func TestExecuteAndBuildImageFileWithFakes(t *testing.T) {
rootTools := fakeRootfsTools(t)
mountTools := fakeMountTools(t)
t.Setenv("PATH", rootTools+string(os.PathListSeparator)+mountTools+string(os.PathListSeparator)+os.Getenv("PATH"))
dir := t.TempDir()
rawImage := filepath.Join(dir, "base.img")
if err := os.WriteFile(rawImage, make([]byte, 4096), 0o644); err != nil {
t.Fatal(err)
}
sum := imageChecksum(t, rawImage)
inv := &inventory.Inventory{
Classes: []inventory.NodeClass{{
Name: "c1",
Arch: "arm64",
OS: "linux",
Image: "file://" + rawImage,
Checksum: sum,
}},
Nodes: []inventory.NodeSpec{{
Name: "n1",
Class: "c1",
Hostname: "n1",
IP: "10.0.0.1",
K3sRole: "agent",
SSHUser: "atlas",
SSHAuthorized: []string{"ssh-ed25519 AAA"},
}},
}
planDry, err := Execute(inv, "n1", filepath.Join(dir, "disk.img"), filepath.Join(dir, "cache"), false)
if err != nil {
t.Fatalf("Execute dry-run: %v", err)
}
if planDry.Node != "n1" || len(planDry.Actions) == 0 {
t.Fatalf("unexpected dry-run plan: %#v", planDry)
}
bootDir := filepath.Join(dir, "boot")
rootDir := filepath.Join(dir, "root")
if err := os.MkdirAll(bootDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(rootDir, 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("METIS_BOOT_PATH", bootDir)
t.Setenv("METIS_ROOT_PATH", rootDir)
t.Setenv("METIS_AUTO_MOUNT", "1")
written := filepath.Join(dir, "written.img")
planRun, err := Execute(inv, "n1", written, filepath.Join(dir, "cache2"), true)
if err != nil {
t.Fatalf("Execute confirm: %v", err)
}
if planRun.Image != "file://"+rawImage {
t.Fatalf("unexpected plan image: %#v", planRun)
}
if _, err := os.Stat(written); err != nil {
t.Fatalf("expected written image: %v", err)
}
if _, err := os.Stat(filepath.Join(rootDir, "etc/metis/firstboot.env")); err != nil {
t.Fatalf("expected injected rootfs file: %v", err)
}
}
func TestBuildImageFileMaterializesRootFS(t *testing.T) {
rootTools := fakeRootfsTools(t)
mountTools := fakeMountTools(t)
t.Setenv("PATH", rootTools+string(os.PathListSeparator)+mountTools+string(os.PathListSeparator)+os.Getenv("PATH"))
dir := t.TempDir()
rawImage := filepath.Join(dir, "base.img")
if err := os.WriteFile(rawImage, make([]byte, 4096), 0o644); err != nil {
t.Fatal(err)
}
sum := imageChecksum(t, rawImage)
inv := &inventory.Inventory{
Classes: []inventory.NodeClass{{
Name: "c1",
Arch: "arm64",
OS: "linux",
Image: "file://" + rawImage,
Checksum: sum,
}},
Nodes: []inventory.NodeSpec{{
Name: "n1",
Class: "c1",
Hostname: "n1",
IP: "10.0.0.1",
K3sRole: "agent",
SSHUser: "atlas",
SSHAuthorized: []string{"ssh-ed25519 AAA"},
}},
}
dumpDir := filepath.Join(dir, "dump")
t.Setenv("METIS_FAKE_UMOUNT_DUMP_DIR", dumpDir)
out := filepath.Join(dir, "output.img")
if err := BuildImageFile(context.Background(), inv, "n1", filepath.Join(dir, "cache"), out); err != nil {
t.Fatalf("BuildImageFile: %v", err)
}
if _, err := os.Stat(out); err != nil {
t.Fatalf("expected output image: %v", err)
}
userData := findDumpedFileContent(t, dumpDir, "user-data")
if !strings.Contains(userData, "metis-apply-node-identity.sh") {
t.Fatalf("expected boot cloud-init hook in user-data, got: %s", userData)
}
}
func TestMaybeInjectNoopsWhenEnvUnset(t *testing.T) {
if err := maybeInject(&inventory.Inventory{}, "n1"); err != nil {
t.Fatalf("maybeInject without env: %v", err)
}
}
func TestChecksumFromInventoryAndCacheName(t *testing.T) {
inv := &inventory.Inventory{
Classes: []inventory.NodeClass{{Name: "c1", Checksum: "sha256:deadbeef"}},
Nodes: []inventory.NodeSpec{{Name: "n1", Class: "c1"}},
}
if got := checksumFromInventory(inv, "n1"); got != "sha256:deadbeef" {
t.Fatalf("checksumFromInventory = %q", got)
}
if got := cacheName("/tmp/archive/base.img.xz"); got != "base.img" {
t.Fatalf("cacheName = %q", got)
}
}
func fakeRootfsTools(t *testing.T) string {
t.Helper()
dir := t.TempDir()
write := func(name, body string) {
path := filepath.Join(dir, 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("sfdisk", `cat <<'JSON'
{"partitiontable":{"sectorsize":512,"partitions":[{"start":3,"size":1,"type":"ef"},{"start":1,"size":2,"type":"83"}]}}
JSON`)
write("debugfs", `if [[ "${1:-}" == "-w" ]]; then
cp "${3:-}" "${4:-}.commands"
exit 0
fi
if [[ "${1:-}" == "-R" ]]; then
state="${3:-}.commands"
set -- $2
case "${1:-}" in
stat)
mode="$(awk -v path="${2:-}" '$1=="sif" && $2==path {print $4}' "${state}" | tail -n1)"
mode="${mode: -4}"
printf 'Mode: %s\n' "${mode}"
exit 0
;;
dump)
local_path="$(awk -v path="${2:-}" '$1=="write" && $3==path {print $2}' "${state}" | tail -n1)"
cat "${local_path}" > "${3:-}"
exit 0
;;
esac
fi
exit 0`)
return dir
}
func fakeMountTools(t *testing.T) string {
t.Helper()
dir := t.TempDir()
write := func(name, body string) {
path := filepath.Join(dir, 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("losetup", `if [[ "${1:-}" == "-Pf" && "${2:-}" == "--show" ]]; then
printf '/dev/loop9\n'
exit 0
fi
exit 0`)
write("mount", `mkdir -p "${2:-}"
exit 0`)
write("umount", `target="${1:-}"
dump="${METIS_FAKE_UMOUNT_DUMP_DIR:-}"
if [[ -n "$dump" && -d "$target" ]]; then
base="$(basename "$target")"
mkdir -p "$dump/$base"
cp -a "$target"/. "$dump/$base/" 2>/dev/null || true
fi
exit 0`)
return dir
}
func findDumpedFileContent(t *testing.T, dumpDir, fileName string) string {
t.Helper()
var content string
err := filepath.Walk(dumpDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() || filepath.Base(path) != fileName {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
content = string(data)
return filepath.SkipAll
})
if err != nil {
t.Fatalf("walk dumped files: %v", err)
}
if content == "" {
t.Fatalf("did not find dumped %s under %s", fileName, dumpDir)
}
return content
}
func imageChecksum(t *testing.T, path string) string {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
sum := sha256.Sum256(data)
return "sha256:" + hex.EncodeToString(sum[:])
}