package imagepull import ( "os" "os/exec" "path/filepath" "strings" "testing" ) // runFirstBootPlan executes first-boot.sh in planning mode (RUN_PLAN=1) // against a synthetic /etc/os-release. Returns the planned install // command or an error. func runFirstBootPlan(t *testing.T, osReleaseContent string) string { t.Helper() if _, err := exec.LookPath("sh"); err != nil { t.Skip("sh not available") } dir := t.TempDir() osRelease := filepath.Join(dir, "os-release") if err := os.WriteFile(osRelease, []byte(osReleaseContent), 0o644); err != nil { t.Fatal(err) } scriptPath := filepath.Join(dir, "banger-first-boot") if err := os.WriteFile(scriptPath, []byte(FirstBootScript()), 0o755); err != nil { t.Fatal(err) } marker := filepath.Join(dir, "first-boot-pending") if err := os.WriteFile(marker, nil, 0o644); err != nil { t.Fatal(err) } cmd := exec.Command("sh", scriptPath) cmd.Env = append(os.Environ(), "RUN_PLAN=1", "OS_RELEASE_FILE="+osRelease, "BANGER_FIRST_BOOT_MARKER="+marker, ) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("first-boot script: %v\noutput:\n%s", err, out) } // Planned command is printed to stdout (no [banger-first-boot] prefix); // log output goes to stderr. CombinedOutput merges them, so pick the // last non-log line. lines := strings.Split(strings.TrimRight(string(out), "\n"), "\n") for i := len(lines) - 1; i >= 0; i-- { l := lines[i] if strings.TrimSpace(l) == "" { continue } if strings.HasPrefix(l, "[banger-first-boot]") { continue } return l } t.Fatalf("no planned command in output:\n%s", out) return "" } func TestFirstBootScriptDispatchesByDistro(t *testing.T) { cases := []struct { name string osRel string wantRe string }{ {"debian", `ID=debian` + "\n" + `ID_LIKE=""`, "systemd-sysv openssh-server"}, {"ubuntu", `ID=ubuntu`, "systemd-sysv openssh-server"}, {"alpine", `ID=alpine`, "apk add"}, {"fedora", `ID=fedora`, "dnf install -y systemd openssh-server"}, {"arch", `ID=arch`, "pacman -Sy --noconfirm openssh"}, {"opensuse-leap", `ID="opensuse-leap"`, "zypper --non-interactive install"}, {"unknown-with-debian-like", `ID=someweirddistro` + "\n" + `ID_LIKE=debian`, "systemd-sysv openssh-server"}, {"unknown-with-rhel-like", `ID=something` + "\n" + `ID_LIKE="rhel fedora"`, "dnf install -y systemd openssh-server"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := runFirstBootPlan(t, tc.osRel) if !strings.Contains(got, tc.wantRe) { t.Errorf("got=%q, want contains %q", got, tc.wantRe) } }) } } func TestFirstBootScriptContainsDistroCases(t *testing.T) { s := FirstBootScript() for _, snippet := range []string{ "debian|ubuntu|kali|raspbian", "apt-get", "systemd-sysv", "openssh-server", "alpine)", "apk add", "fedora|rhel|centos|rocky|almalinux", "dnf install", "arch|archlinux|manjaro", "pacman -Sy", "opensuse*|suse", "zypper", `ID_LIKE`, "RUN_PLAN", "/usr/lib/systemd/systemd", "mount -t proc", } { if !strings.Contains(s, snippet) { t.Errorf("script missing %q", snippet) } } } func TestFirstBootScriptIsShSyntaxValid(t *testing.T) { if _, err := exec.LookPath("sh"); err != nil { t.Skip("sh not available") } dir := t.TempDir() path := filepath.Join(dir, "first-boot") if err := os.WriteFile(path, []byte(FirstBootScript()), 0o755); err != nil { t.Fatal(err) } out, err := exec.Command("sh", "-n", path).CombinedOutput() if err != nil { t.Fatalf("sh -n first-boot: %v: %s", err, out) } } func TestFirstBootUnitReferencesScript(t *testing.T) { u := FirstBootUnit() for _, want := range []string{ FirstBootScriptPath, "ConditionPathExists=" + FirstBootMarkerPath, "After=network-online.target", "Before=sshd.service", } { if !strings.Contains(u, want) { t.Errorf("unit missing %q", want) } } }