package openrc import ( "os" "path/filepath" "strings" "testing" "time" "github.com/warkanum/unitdore/config" "github.com/warkanum/unitdore/service" ) // setup points the backend at a temp init dir with recorded commands. func setup(t *testing.T) *[]string { t.Helper() oldDir, oldRun, oldOut, oldRoot := initDir, runCmd, outCmd, service.IsRoot t.Cleanup(func() { initDir, runCmd, outCmd, service.IsRoot = oldDir, oldRun, oldOut, oldRoot }) initDir = t.TempDir() service.IsRoot = func() bool { return true } var calls []string runCmd = func(name string, args ...string) error { calls = append(calls, name+" "+strings.Join(args, " ")) return nil } return &calls } func mustContain(t *testing.T, content string, wants ...string) { t.Helper() for _, w := range wants { if !strings.Contains(content, w) { t.Errorf("missing %q in:\n%s", w, content) } } } func TestGenerate_Container(t *testing.T) { setup(t) u := config.Unit{Name: "nginx", Runtime: "podman", Enabled: true} got, err := Backend{}.Generate(u, "", "") if err != nil { t.Fatal(err) } mustContain(t, got, "#!/sbin/openrc-run", `description="Unitdore: nginx (podman)"`, "command_background=yes", `pidfile="/run/${RC_SVCNAME}.pid"`, `command="/usr/bin/podman"`, `command_args="start -a nginx"`, "need net", "stop_pre() {\n\t/usr/bin/podman stop nginx || true\n}", ) if strings.Contains(got, "supervise-daemon") || strings.Contains(got, "need docker") { t.Errorf("unexpected supervisor or docker dep:\n%s", got) } } func TestGenerate_DockerRestart(t *testing.T) { setup(t) u := config.Unit{Name: "redis", Runtime: "docker", Restart: true, RestartSec: 5, RestartRetries: 3} got, err := Backend{}.Generate(u, "", "") if err != nil { t.Fatal(err) } mustContain(t, got, "supervisor=supervise-daemon", "respawn_delay=5", "respawn_max=3", "need docker") if strings.Contains(got, "command_background") { t.Errorf("supervised unit must not background:\n%s", got) } } func TestGenerate_ScriptCustomShellAndDir(t *testing.T) { setup(t) u := config.Unit{ Name: "job", Runtime: config.RuntimeScript, Shell: "/bin/sh", Script: "/opt/my jobs/run.sh", WorkingDir: "/opt", } got, err := Backend{}.Generate(u, "", "") if err != nil { t.Fatal(err) } mustContain(t, got, "(sh script)", `command="/bin/sh"`, `command_args="'/opt/my jobs/run.sh'"`, `directory="/opt"`, ) } func TestGenerate_OneshotStack(t *testing.T) { setup(t) u := config.Unit{Name: "shop", Runtime: "podman", Kind: config.KindStack, Project: "shop"} got, err := Backend{}.Generate(u, "", "") if err != nil { t.Fatal(err) } mustContain(t, got, "start() {", "/usr/bin/podman pod start pod_shop", "stop() {", "/usr/bin/podman pod stop pod_shop", ) if strings.Contains(got, "command=") { t.Errorf("oneshot must not set command:\n%s", got) } } func TestGenerate_RootlessUser(t *testing.T) { setup(t) u := config.Unit{Name: "app", Runtime: "podman", User: "root"} got, err := Backend{}.Generate(u, "", "") if err != nil { t.Fatal(err) } mustContain(t, got, "export HOME=/root", "export XDG_RUNTIME_DIR=/run/user/0", `command_user="root"`, "checkpath -d -m 0700 -o root /run/user/0", "su -m -s /bin/sh -c '/usr/bin/podman stop app' root", ) } func TestGenerate_Invalid(t *testing.T) { setup(t) if _, err := (Backend{}).Generate(config.Unit{Name: "x", Runtime: config.RuntimeExec}, "", ""); err == nil { t.Error("expected validation error") } if _, err := (Backend{}).Generate(config.Unit{Name: "x", Runtime: "podman", Command: `/bin/x "open`}, "", ""); err == nil { t.Error("expected quote error") } } func TestInstallUninstall(t *testing.T) { calls := setup(t) b := Backend{} u := config.Unit{Name: "nginx", Runtime: "podman"} if b.IsInstalled(u, "", "") { t.Fatal("should not be installed yet") } if err := b.Install(u, "p-", ""); err != nil { t.Fatal(err) } path := filepath.Join(initDir, "unitdore-p-nginx") fi, err := os.Stat(path) if err != nil { t.Fatal(err) } if fi.Mode().Perm()&0111 == 0 { t.Errorf("init script must be executable, mode %v", fi.Mode()) } if !b.IsInstalled(u, "p-", "") { t.Error("should be installed") } if err := b.Uninstall(u, "p-", ""); err != nil { t.Fatal(err) } if b.IsInstalled(u, "p-", "") { t.Error("should be uninstalled") } if got := strings.Join(*calls, "|"); got != "rc-update del unitdore-p-nginx default" { t.Errorf("calls = %q", got) } } func TestControlCommands(t *testing.T) { calls := setup(t) b := Backend{} u := config.Unit{Name: "nginx", Runtime: "podman"} _ = b.Start(u, "", "") _ = b.Stop(u, "", "") _ = b.Enable(u, "", "") _ = b.Disable(u, "", "") want := []string{ "rc-service unitdore-nginx start", "rc-service unitdore-nginx stop", "rc-update add unitdore-nginx default", "rc-service unitdore-nginx start", "rc-service unitdore-nginx stop", "rc-update del unitdore-nginx default", } if got := strings.Join(*calls, "|"); got != strings.Join(want, "|") { t.Errorf("calls =\n%q\nwant\n%q", *calls, want) } } func TestRequiresRoot(t *testing.T) { setup(t) service.IsRoot = func() bool { return false } u := config.Unit{Name: "nginx", Runtime: "podman"} b := Backend{} for name, err := range map[string]error{ "install": b.Install(u, "", ""), "uninstall": b.Uninstall(u, "", ""), "start": b.Start(u, "", ""), "stop": b.Stop(u, "", ""), "enable": b.Enable(u, "", ""), "disable": b.Disable(u, "", ""), } { if err == nil { t.Errorf("%s should require root", name) } } } func TestParseStatus(t *testing.T) { tests := map[string]string{ " * status: started": "active", " * status: stopped": "inactive", " * status: crashed": "failed", " * status: starting": "activating", " * status: stopping": "deactivating", "": "unknown", "rc-service: not found": "unknown", } for in, want := range tests { if got := parseStatus(in); got != want { t.Errorf("parseStatus(%q) = %q, want %q", in, got, want) } } } func TestGenerate_OrderAndDelay(t *testing.T) { setup(t) u := config.Unit{Name: "web", Runtime: "podman", Enabled: true, After: []string{"db", "cache"}, StartDelay: 5 * time.Second} got, err := Backend{}.Generate(u, "p-", "") if err != nil { t.Fatal(err) } mustContain(t, got, "need net\n\tafter unitdore-p-db unitdore-p-cache\n}", "start_pre() {\n\tsleep 5\n}") } func TestGenerate_OneshotDelay(t *testing.T) { setup(t) u := config.Unit{Name: "job", Runtime: "exec", Executable: "/bin/true", ServiceType: "oneshot", Enabled: true, StartDelay: time.Second} got, err := Backend{}.Generate(u, "", "") if err != nil { t.Fatal(err) } mustContain(t, got, "start_pre() {\n\tsleep 1\n}") } func TestGenerate_NoOrderNoExtras(t *testing.T) { setup(t) got, err := Backend{}.Generate(config.Unit{Name: "a", Runtime: "podman", Enabled: true}, "", "") if err != nil { t.Fatal(err) } if strings.Contains(got, "after ") || strings.Contains(got, "start_pre") { t.Errorf("unexpected ordering output:\n%s", got) } }