metis/cmd/metis/gate_test.go

245 lines
8.4 KiB
Go

package main
import (
"net/http"
"os"
"path/filepath"
"strings"
"testing"
)
func TestMainDispatchAllCommands(t *testing.T) {
root := t.TempDir()
invPath, baseImage := writeTestInventory(t, root)
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)
}
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": `case "${1:-}" in
-T0) cp "${@: -1}" "${@: -1}.xz" ;;
-dc) cat "${2:-}" ;;
esac
exit 0`,
"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"))
t.Setenv("METIS_INVENTORY_PATH", invPath)
t.Setenv("METIS_DATA_DIR", filepath.Join(root, "data"))
listenAndServe = func(addr string, _ http.Handler) error { return nil }
t.Cleanup(func() { listenAndServe = httpListenAndServe })
callMain := func(args ...string) {
oldArgs := os.Args
os.Args = append([]string{"metis"}, args...)
defer func() { os.Args = oldArgs }()
main()
}
callMain("config", "--inventory", invPath, "--node", "titan-15")
callMain("facts", "--inventory", invPath, "--snapshots", snapDir)
callMain("plan", "--inventory", invPath, "--node", "titan-15")
callMain("burn", "--inventory", invPath, "--node", "titan-15", "--device", "/dev/sdz")
callMain("image", "--inventory", invPath, "--node", "titan-15", "--output", filepath.Join(root, "out.img"))
callMain("inject", "--inventory", invPath, "--node", "titan-15", "--boot", filepath.Join(root, "boot"), "--root", filepath.Join(root, "root"))
callMain("serve", "--bind", ":0")
callMain("remote-devices", "--host-tmp-dir", filepath.Join(root, "host-tmp"))
callMain("remote-build", "--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")
callMain("remote-flash", "--node", "titan-15", "--device", filepath.Join(root, "flash.img"), "--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")
}
func TestMainAndCommandFatalBranches(t *testing.T) {
trap := func() {
fatalf = func(format string, args ...any) {
panic("fatal: " + format)
}
exit = func(code int) {
panic("exit")
}
t.Cleanup(func() {
fatalf = httpLogFatalf
exit = httpExit
})
}
trap()
mustPanic := func(fn func()) {
t.Helper()
defer func() {
if r := recover(); r == nil {
t.Fatal("expected panic")
}
}()
fn()
}
mustPanic(func() {
oldArgs := os.Args
os.Args = []string{"metis"}
defer func() { os.Args = oldArgs }()
main()
})
mustPanic(func() {
oldArgs := os.Args
os.Args = []string{"metis", "bogus"}
defer func() { os.Args = oldArgs }()
main()
})
mustPanic(func() { configCmd(nil) })
mustPanic(func() { planCmd([]string{"--inventory", "/nope", "--node", "titan-15"}) })
mustPanic(func() { burnCmd([]string{"--inventory", "/nope", "--node", "titan-15", "--device", "/dev/sdz"}) })
mustPanic(func() { imageCmd(nil) })
mustPanic(func() { injectCmd(nil) })
mustPanic(func() { factsCmd([]string{"--inventory", "/nope", "--snapshots", "/nope"}) })
mustPanic(func() { serveCmd([]string{"--bind", ":0"}) })
mustPanic(func() { remoteBuildCmd([]string{"--node", "n1"}) })
mustPanic(func() { remoteFlashCmd([]string{"--node", "n1"}) })
}
func TestRemoteCommandHelpers(t *testing.T) {
if !hasMountedChildren([]struct {
Mountpoint string `json:"mountpoint"`
}{{Mountpoint: "/mnt"}}) {
t.Fatal("hasMountedChildren should detect a mount point")
}
if got := humanHostPath("/host-tmp/metis-flash"); got != "/tmp/metis-flash" {
t.Fatalf("humanHostPath = %q", got)
}
t.Setenv("METIS_REMOTE_SAMPLE", "value")
if got := getenvOr("METIS_REMOTE_SAMPLE", "fallback"); got != "value" {
t.Fatalf("getenvOr = %q", got)
}
dir := t.TempDir()
base := filepath.Join(dir, "base.img")
if err := os.WriteFile(base, []byte("artifact"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(base+".meta", []byte(`{"meta":true}`), 0o644); err != nil {
t.Fatal(err)
}
fakeTools := fakeCommandDir(t, map[string]string{
"oras": `case "${1:-}" in
login|tag) exit 0 ;;
push) exit 0 ;;
pull)
outdir="${@: -1}"
cp "` + base + `" "${outdir}/artifact.img"
exit 0
;;
esac
exit 0`,
"lsblk": `cat <<'JSON'
{"blockdevices":[{"name":"sdz","path":"/dev/sdz","rm":true,"hotplug":true,"size":"32000000000","model":"Micro SD","tran":"usb","type":"disk","children":[{"mountpoint":""}]},{"name":"sdy","path":"/dev/sdy","rm":true,"hotplug":true,"size":"64000000000","model":"SSD","tran":"usb","type":"disk","children":[{"mountpoint":"/mnt"}]}]}
JSON`,
})
t.Setenv("PATH", fakeTools+string(os.PathListSeparator)+os.Getenv("PATH"))
if err := orasLogin("registry.example", "", ""); err == nil {
t.Fatal("expected orasLogin to reject missing creds")
}
if err := orasLogin("registry.example", "u", "p"); err != nil {
t.Fatalf("orasLogin: %v", err)
}
if _, _, err := orasPushInvocation("r", filepath.Join(dir, "one", "a.img"), filepath.Join(dir, "two", "b.meta")); err == nil {
t.Fatal("expected orasPushInvocation mismatch error")
}
pushDir := filepath.Join(dir, "push")
if err := os.MkdirAll(pushDir, 0o755); err != nil {
t.Fatal(err)
}
img := filepath.Join(pushDir, "a.img")
meta := filepath.Join(pushDir, "a.meta")
if err := os.WriteFile(img, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(meta, []byte(`{}`), 0o644); err != nil {
t.Fatal(err)
}
if _, args, err := orasPushInvocation("ref", img, meta); err != nil || len(args) == 0 {
t.Fatalf("orasPushInvocation success = %#v %v", args, err)
}
if err := orasPush("ref", img, meta); err != nil {
t.Fatalf("orasPush: %v", err)
}
if err := orasTag("ref", "latest"); err != nil {
t.Fatalf("orasTag: %v", err)
}
pullDir := filepath.Join(dir, "pull")
if err := os.MkdirAll(pullDir, 0o755); err != nil {
t.Fatal(err)
}
if err := orasPull("ref", pullDir); err != nil {
t.Fatalf("orasPull: %v", err)
}
artifact, compressed, err := resolvePulledArtifact(pullDir)
if err != nil || compressed || !strings.HasSuffix(artifact, ".img") {
t.Fatalf("resolvePulledArtifact raw = %q compressed=%v err=%v", artifact, compressed, err)
}
if err := os.WriteFile(filepath.Join(pullDir, "artifact.img.xz"), []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
artifact, compressed, err = resolvePulledArtifact(pullDir)
if err != nil || !compressed || !strings.HasSuffix(artifact, ".img.xz") {
t.Fatalf("resolvePulledArtifact xz = %q compressed=%v err=%v", artifact, compressed, err)
}
if _, _, err := resolvePulledArtifact(filepath.Join(dir, "missing")); err == nil {
t.Fatal("expected resolvePulledArtifact error")
}
devices, err := localFlashDevices(40000000000, filepath.Join(dir, "host-tmp"))
if err != nil {
t.Fatalf("localFlashDevices: %v", err)
}
if len(devices) == 0 || devices[0].Path != "/dev/sdz" {
t.Fatalf("localFlashDevices = %#v", devices)
}
writeStructuredResult(map[string]any{"ok": true})
}
var (
httpLogFatalf = fatalf
httpExit = exit
httpListenAndServe = listenAndServe
)