metis/cmd/metis/main_test.go

292 lines
8.9 KiB
Go

package main
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
)
func TestUsageWritesSupportedCommands(t *testing.T) {
stdout, stderr := captureStreams(t, func() {
usage()
})
if stdout != "" {
t.Fatalf("usage wrote stdout: %q", stdout)
}
if !strings.Contains(stderr, "remote-flash") || !strings.Contains(stderr, "plan") {
t.Fatalf("usage output missing commands: %q", stderr)
}
}
func TestConfigFactsPlanBurnImageInjectAndServeCommands(t *testing.T) {
root := t.TempDir()
invPath, baseImage := writeTestInventory(t, root)
t.Setenv("METIS_INVENTORY_PATH", invPath)
t.Setenv("METIS_DATA_DIR", filepath.Join(root, "data"))
t.Setenv("METIS_BOOT_PATH", "")
t.Setenv("METIS_ROOT_PATH", "")
stdout, _ := captureStreams(t, func() {
configCmd([]string{"--inventory", invPath, "--node", "titan-15"})
})
if !strings.Contains(stdout, `"hostname": "titan-15"`) {
t.Fatalf("config output missing hostname: %s", stdout)
}
snapDir := filepath.Join(root, "snapshots")
if err := os.MkdirAll(snapDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(snapDir, "snap.json"), []byte(`{"hostname":"titan-15","kernel":"6.6.63","package_sample":{"containerd":"1.7"}}`), 0o644); err != nil {
t.Fatal(err)
}
stdout, _ = captureStreams(t, func() {
factsCmd([]string{"--inventory", invPath, "--snapshots", snapDir})
})
if !strings.Contains(stdout, `"class": "rpi4"`) {
t.Fatalf("facts output missing class summary: %s", stdout)
}
stdout, _ = captureStreams(t, func() {
planCmd([]string{"--inventory", invPath, "--node", "titan-15", "--device", "/dev/sdz", "--cache", filepath.Join(root, "cache")})
})
if !strings.Contains(stdout, `"node": "titan-15"`) || !strings.Contains(stdout, `"actions"`) {
t.Fatalf("plan output missing plan JSON: %s", stdout)
}
rootTools := fakeCommandDir(t, map[string]string{
"sfdisk": `cat <<'JSON'
{"partitiontable":{"sectorsize":512,"partitions":[{"start":3,"size":1,"type":"ef"},{"start":1,"size":2,"type":"83"}]}}
JSON`,
"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`,
})
t.Setenv("PATH", rootTools+string(os.PathListSeparator)+os.Getenv("PATH"))
stdout, _ = captureStreams(t, func() {
imageCmd([]string{"--inventory", invPath, "--node", "titan-15", "--output", filepath.Join(root, "out.img"), "--cache", filepath.Join(root, "cache")})
})
if !strings.Contains(stdout, "Wrote ") {
t.Fatalf("image output missing write confirmation: %s", stdout)
}
stdout, _ = captureStreams(t, func() {
burnCmd([]string{"--inventory", invPath, "--node", "titan-15", "--device", "/dev/sdz", "--cache", filepath.Join(root, "cache")})
})
if !strings.Contains(stdout, "Plan for titan-15 to /dev/sdz") {
t.Fatalf("burn output missing plan header: %s", stdout)
}
bootDir := filepath.Join(root, "boot")
rootDir := filepath.Join(root, "root")
if err := os.MkdirAll(bootDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(rootDir, 0o755); err != nil {
t.Fatal(err)
}
captureStreams(t, func() {
injectCmd([]string{"--inventory", invPath, "--node", "titan-15", "--boot", bootDir, "--root", rootDir})
})
if _, err := os.Stat(filepath.Join(rootDir, "etc/metis/node.json")); err != nil {
t.Fatalf("injectCmd did not write root file: %v", err)
}
listenAndServe = func(addr string, handler http.Handler) error {
if addr != ":0" {
t.Fatalf("unexpected bind addr: %s", addr)
}
return nil
}
t.Cleanup(func() { listenAndServe = http.ListenAndServe })
t.Setenv("METIS_BIND_ADDR", ":0")
t.Setenv("METIS_INVENTORY_PATH", invPath)
serveCmd([]string{"--bind", ":0"})
_ = baseImage
}
func TestMainDispatchesConfig(t *testing.T) {
root := t.TempDir()
invPath, _ := writeTestInventory(t, root)
oldArgs := os.Args
os.Args = []string{"metis", "config", "--inventory", invPath, "--node", "titan-15"}
t.Cleanup(func() { os.Args = oldArgs })
main()
}
func TestRemoteCommandsAndHelpers(t *testing.T) {
root := t.TempDir()
invPath, baseImage := writeTestInventory(t, root)
fakeTools := fakeCommandDir(t, map[string]string{
"lsblk": `cat <<'JSON'
{"blockdevices":[{"name":"sdz","path":"/dev/sdz","rm":true,"hotplug":true,"size":"32000000000","model":"Micro SD","tran":"usb","type":"disk"}]}
JSON`,
"sfdisk": `cat <<'JSON'
{"partitiontable":{"sectorsize":512,"partitions":[{"start":3,"size":1,"type":"ef"},{"start":1,"size":2,"type":"83"}]}}
JSON`,
"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`,
"xz": `dest="${@: -1}"; cp "$dest" "$dest.xz"`,
"oras": `case "${1:-}" in
login|tag) exit 0 ;;
push) exit 0 ;;
pull)
outdir="${@: -1}"
cp "` + baseImage + `" "${outdir}/titan-15.img"
exit 0
;;
esac
exit 0`,
})
t.Setenv("PATH", fakeTools+string(os.PathListSeparator)+os.Getenv("PATH"))
stdout, _ := captureStreams(t, func() {
remoteDevicesCmd([]string{"--max-device-bytes", "40000000000", "--host-tmp-dir", filepath.Join(root, "host-tmp")})
})
if !strings.Contains(stdout, `"path":"/dev/sdz"`) {
t.Fatalf("remoteDevicesCmd output missing device: %s", stdout)
}
stdout, _ = captureStreams(t, func() {
remoteBuildCmd([]string{"--inventory", invPath, "--node", "titan-15", "--artifact-ref", "registry.example/metis/titan-15", "--build-tag", "build-1", "--work-dir", filepath.Join(root, "build"), "--cache", filepath.Join(root, "cache"), "--harbor-registry", "registry.example", "--harbor-username", "admin", "--harbor-password", "pw"})
})
if !strings.Contains(stdout, `"build_tag":"build-1"`) {
t.Fatalf("remoteBuildCmd output missing build tag: %s", stdout)
}
stdout, _ = captureStreams(t, func() {
remoteFlashCmd([]string{"--node", "titan-15", "--device", "hosttmp:///tmp", "--artifact-ref", "registry.example/metis/titan-15", "--work-dir", filepath.Join(root, "flash"), "--host-tmp-dir", filepath.Join(root, "host-tmp"), "--harbor-registry", "registry.example", "--harbor-username", "admin", "--harbor-password", "pw"})
})
if !strings.Contains(stdout, `"dest_path"`) {
t.Fatalf("remoteFlashCmd output missing dest_path: %s", stdout)
}
}
func captureStreams(t *testing.T, fn func()) (string, string) {
t.Helper()
oldStdout := os.Stdout
oldStderr := os.Stderr
stdoutR, stdoutW, _ := os.Pipe()
stderrR, stderrW, _ := os.Pipe()
os.Stdout = stdoutW
os.Stderr = stderrW
defer func() {
os.Stdout = oldStdout
os.Stderr = oldStderr
}()
done := make(chan struct {
out string
err string
}, 1)
go func() {
var outBuf bytes.Buffer
var errBuf bytes.Buffer
_, _ = io.Copy(&outBuf, stdoutR)
_, _ = io.Copy(&errBuf, stderrR)
done <- struct {
out string
err string
}{out: outBuf.String(), err: errBuf.String()}
}()
fn()
_ = stdoutW.Close()
_ = stderrW.Close()
captured := <-done
return captured.out, captured.err
}
func writeTestInventory(t *testing.T, root string) (string, string) {
t.Helper()
baseImage := filepath.Join(root, "base.img")
if err := os.WriteFile(baseImage, make([]byte, 4096), 0o644); err != nil {
t.Fatal(err)
}
invPath := filepath.Join(root, "inventory.yaml")
inv := `classes:
- name: rpi4
arch: arm64
os: armbian
image: file://` + baseImage + `
checksum: sha256:` + sha256SumHex(t, make([]byte, 4096)) + `
k3s_version: v1.31.5+k3s1
nodes:
- name: titan-15
class: rpi4
hostname: titan-15
ip: 192.168.22.43
k3s_role: agent
k3s_url: https://192.168.22.7:6443
k3s_token: token
ssh_user: atlas
ssh_authorized_keys:
- ssh-ed25519 AAA
`
if err := os.WriteFile(invPath, []byte(inv), 0o644); err != nil {
t.Fatal(err)
}
return invPath, baseImage
}
func sha256SumHex(t *testing.T, data []byte) string {
t.Helper()
sum := sha256.Sum256(data)
return hex.EncodeToString(sum[:])
}
func fakeCommandDir(t *testing.T, scripts map[string]string) string {
t.Helper()
dir := t.TempDir()
for name, body := range scripts {
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)
}
}
return dir
}