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.
222 lines
5.9 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|