Files
unitdore/openrc/openrc_test.go
T
warkanum df9b226c7e feat: OpenRC and runit init backends behind an Init interface
Extract init-neutral unit resolution into service/, add initsys.Init with systemd, openrc and runit backends, selected by 'init:' config or autodetect. OpenRC and runit are root only; rootless units run as system services as that user. Add docs and generated examples for all three.
2026-10-12 20:19:58 +02:00

222 lines
5.9 KiB
Go

package openrc
import (
"os"
"path/filepath"
"strings"
"testing"
"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)
}
}
}