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.
This commit is contained in:
+364
@@ -0,0 +1,364 @@
|
||||
// Package runit generates and manages runit service directories.
|
||||
//
|
||||
// Each unit is a directory /etc/sv/<name> (run, finish) linked into the
|
||||
// supervised service dir. Services run at system level (root only). Units with
|
||||
// a user run as that user through chpst, with HOME and XDG_RUNTIME_DIR set.
|
||||
//
|
||||
// Install links the service with a `down` file: it is supervised but does not
|
||||
// start until started or enabled. Enable removes `down`, Disable restores it.
|
||||
package runit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/warkanum/unitdore/config"
|
||||
"github.com/warkanum/unitdore/service"
|
||||
)
|
||||
|
||||
// Overridable for tests.
|
||||
var (
|
||||
svDir = "/etc/sv"
|
||||
|
||||
// serviceDirCandidates are probed in order when no service_dir is set.
|
||||
serviceDirCandidates = []string{"/var/service", "/etc/service"}
|
||||
|
||||
// superviseWait is how long Start waits for runsvdir to pick up a new link.
|
||||
superviseWait = 10 * time.Second
|
||||
|
||||
runCmd = func(name string, args ...string) error {
|
||||
cmd := exec.Command(name, args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
outCmd = func(name string, args ...string) ([]byte, error) {
|
||||
return exec.Command(name, args...).CombinedOutput()
|
||||
}
|
||||
)
|
||||
|
||||
// Backend is the runit implementation of the init interface.
|
||||
type Backend struct {
|
||||
ServiceDir string // supervised dir; empty = autodetect
|
||||
}
|
||||
|
||||
func (Backend) Name() string { return "runit" }
|
||||
|
||||
// SupportsLinger is false: runit services need no linger.
|
||||
func (Backend) SupportsLinger() bool { return false }
|
||||
|
||||
func (Backend) ServiceName(u config.Unit, prefix, suffix string) string {
|
||||
return service.BaseName(u, prefix, suffix)
|
||||
}
|
||||
|
||||
// UnitPath is the service definition directory under /etc/sv.
|
||||
func (b Backend) UnitPath(u config.Unit, prefix, suffix string) (string, error) {
|
||||
return filepath.Join(svDir, b.ServiceName(u, prefix, suffix)), nil
|
||||
}
|
||||
|
||||
func (b Backend) IsInstalled(u config.Unit, prefix, suffix string) bool {
|
||||
dir, _ := b.UnitPath(u, prefix, suffix)
|
||||
_, err := os.Stat(filepath.Join(dir, "run"))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// serviceDir returns the supervised dir that gets the symlinks.
|
||||
func (b Backend) serviceDir() string {
|
||||
if b.ServiceDir != "" {
|
||||
return b.ServiceDir
|
||||
}
|
||||
for _, d := range serviceDirCandidates {
|
||||
if fi, err := os.Stat(d); err == nil && fi.IsDir() {
|
||||
return d
|
||||
}
|
||||
}
|
||||
return serviceDirCandidates[0]
|
||||
}
|
||||
|
||||
func (b Backend) link(u config.Unit, prefix, suffix string) string {
|
||||
return filepath.Join(b.serviceDir(), b.ServiceName(u, prefix, suffix))
|
||||
}
|
||||
|
||||
type file struct {
|
||||
name string
|
||||
content string
|
||||
}
|
||||
|
||||
// Generate produces the run and finish scripts, separated by marker lines.
|
||||
func (b Backend) Generate(u config.Unit, prefix, suffix string) (string, error) {
|
||||
files, err := b.files(u, prefix, suffix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
dir, _ := b.UnitPath(u, prefix, suffix)
|
||||
var w strings.Builder
|
||||
for _, f := range files {
|
||||
fmt.Fprintf(&w, "# ===== %s =====\n%s\n", filepath.Join(dir, f.name), f.content)
|
||||
}
|
||||
return w.String(), nil
|
||||
}
|
||||
|
||||
func (b Backend) files(u config.Unit, prefix, suffix string) ([]file, error) {
|
||||
if err := service.Validate(u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := service.LookupUser(u.User)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
spec := service.Resolve(u)
|
||||
|
||||
start, err := service.ShellCommand(spec.Start)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unit %s: %w", u.Name, err)
|
||||
}
|
||||
stop := ""
|
||||
if spec.Stop != "" {
|
||||
if stop, err = service.ShellCommand(spec.Stop); err != nil {
|
||||
return nil, fmt.Errorf("unit %s: %w", u.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
header := "#!/bin/sh\n# " + b.ServiceName(u, prefix, suffix) + " — generated by unitdore, do not edit manually\n"
|
||||
|
||||
run := header + "exec 2>&1\n" + preamble(user, spec.WorkingDir)
|
||||
if spec.Type == service.TypeOneshot {
|
||||
// Run once, then stay up (like RemainAfterExit) until stopped
|
||||
run += asUser(user, start) + " || exit 1\nexec tail -f /dev/null\n"
|
||||
} else {
|
||||
run += "exec " + asUser(user, start) + "\n"
|
||||
}
|
||||
|
||||
files := []file{{"run", run}}
|
||||
if fin := finishScript(header, u, user, spec.WorkingDir, stop); fin != "" {
|
||||
files = append(files, file{"finish", fin})
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// preamble prepares the environment: working dir and rootless user runtime.
|
||||
func preamble(user *service.UserEnv, workDir string) string {
|
||||
var s string
|
||||
if workDir != "" {
|
||||
s += "cd " + service.ShellQuote(workDir) + " || exit 1\n"
|
||||
}
|
||||
if user != nil {
|
||||
s += fmt.Sprintf("[ -d %[1]s ] || install -d -m 0700 -o %[2]s %[1]s\n",
|
||||
service.ShellQuote(user.RuntimeDir), service.ShellQuote(user.Name))
|
||||
s += "export HOME=" + service.ShellQuote(user.Home) + "\n"
|
||||
s += "export XDG_RUNTIME_DIR=" + service.ShellQuote(user.RuntimeDir) + "\n"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func asUser(user *service.UserEnv, cmd string) string {
|
||||
if user == nil {
|
||||
return cmd
|
||||
}
|
||||
return "chpst -u " + service.ShellQuote(user.Name) + " " + cmd
|
||||
}
|
||||
|
||||
// finishScript runs after the service exits: best-effort stop command, then
|
||||
// the restart policy. runit restarts by default, so no-restart stops it.
|
||||
func finishScript(header string, u config.Unit, user *service.UserEnv, workDir, stop string) string {
|
||||
var body string
|
||||
if workDir != "" {
|
||||
body += "cd " + service.ShellQuote(workDir) + " 2>/dev/null\n"
|
||||
}
|
||||
if stop != "" {
|
||||
body += asUser(user, stop) + " || true\n"
|
||||
}
|
||||
switch {
|
||||
case !u.Restart:
|
||||
body += "exec sv down .\n"
|
||||
default:
|
||||
// Restart=on-failure: a clean exit stays down
|
||||
body += "[ \"$1\" = 0 ] && exec sv down .\n"
|
||||
if u.RestartSec > 0 {
|
||||
body += fmt.Sprintf("sleep %d\n", u.RestartSec)
|
||||
}
|
||||
}
|
||||
return header + body
|
||||
}
|
||||
|
||||
func (b Backend) Install(u config.Unit, prefix, suffix string) error {
|
||||
if err := service.RequireRoot("runit"); err != nil {
|
||||
return err
|
||||
}
|
||||
files, err := b.files(u, prefix, suffix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir, _ := b.UnitPath(u, prefix, suffix)
|
||||
link := b.link(u, prefix, suffix)
|
||||
|
||||
// A re-install keeps an already enabled service enabled
|
||||
wasEnabled := isLinked(link, dir) && !exists(filepath.Join(dir, "down"))
|
||||
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("creating service dir: %w", err)
|
||||
}
|
||||
for _, f := range files {
|
||||
if err := os.WriteFile(filepath.Join(dir, f.name), []byte(f.content), 0755); err != nil {
|
||||
return fmt.Errorf("writing %s: %w", f.name, err)
|
||||
}
|
||||
}
|
||||
if !hasFile(files, "finish") {
|
||||
_ = os.Remove(filepath.Join(dir, "finish"))
|
||||
}
|
||||
if !wasEnabled {
|
||||
if err := setDown(dir, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return ensureLink(dir, link)
|
||||
}
|
||||
|
||||
func (b Backend) Uninstall(u config.Unit, prefix, suffix string) error {
|
||||
if err := service.RequireRoot("runit"); err != nil {
|
||||
return err
|
||||
}
|
||||
dir, _ := b.UnitPath(u, prefix, suffix)
|
||||
link := b.link(u, prefix, suffix)
|
||||
|
||||
_ = runCmd("sv", "stop", link) // not running is fine
|
||||
if err := os.Remove(link); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("removing service link: %w", err)
|
||||
}
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
return fmt.Errorf("removing service dir: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enable makes the service start with runsvdir and starts it now.
|
||||
func (b Backend) Enable(u config.Unit, prefix, suffix string) error {
|
||||
if err := service.RequireRoot("runit"); err != nil {
|
||||
return err
|
||||
}
|
||||
dir, _ := b.UnitPath(u, prefix, suffix)
|
||||
link := b.link(u, prefix, suffix)
|
||||
if err := setDown(dir, false); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureLink(dir, link); err != nil {
|
||||
return err
|
||||
}
|
||||
return startLink(link)
|
||||
}
|
||||
|
||||
// Disable stops the service and keeps it from starting with runsvdir.
|
||||
func (b Backend) Disable(u config.Unit, prefix, suffix string) error {
|
||||
if err := service.RequireRoot("runit"); err != nil {
|
||||
return err
|
||||
}
|
||||
dir, _ := b.UnitPath(u, prefix, suffix)
|
||||
if err := setDown(dir, true); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = runCmd("sv", "stop", b.link(u, prefix, suffix)) // not running is fine
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b Backend) Start(u config.Unit, prefix, suffix string) error {
|
||||
if err := service.RequireRoot("runit"); err != nil {
|
||||
return err
|
||||
}
|
||||
dir, _ := b.UnitPath(u, prefix, suffix)
|
||||
link := b.link(u, prefix, suffix)
|
||||
if err := ensureLink(dir, link); err != nil {
|
||||
return err
|
||||
}
|
||||
return startLink(link)
|
||||
}
|
||||
|
||||
func (b Backend) Stop(u config.Unit, prefix, suffix string) error {
|
||||
if err := service.RequireRoot("runit"); err != nil {
|
||||
return err
|
||||
}
|
||||
return runCmd("sv", "stop", b.link(u, prefix, suffix))
|
||||
}
|
||||
|
||||
func (b Backend) Status(u config.Unit, prefix, suffix string) string {
|
||||
out, _ := outCmd("sv", "status", b.link(u, prefix, suffix))
|
||||
return parseStatus(string(out))
|
||||
}
|
||||
|
||||
// parseStatus maps `sv status` output to a systemd-like state.
|
||||
func parseStatus(out string) string {
|
||||
out = strings.TrimSpace(out)
|
||||
switch {
|
||||
case strings.HasPrefix(out, "run:"):
|
||||
return "active"
|
||||
case strings.HasPrefix(out, "down:"):
|
||||
return "inactive"
|
||||
case strings.HasPrefix(out, "finish:"):
|
||||
return "deactivating"
|
||||
case strings.Contains(out, "runsv not running"):
|
||||
return "inactive"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// startLink waits until runsvdir supervises the link, then starts it.
|
||||
func startLink(link string) error {
|
||||
deadline := time.Now().Add(superviseWait)
|
||||
for !exists(filepath.Join(link, "supervise", "ok")) {
|
||||
if time.Now().After(deadline) {
|
||||
return fmt.Errorf("%s is not supervised yet (is runsvdir running?)", link)
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
return runCmd("sv", "start", link)
|
||||
}
|
||||
|
||||
func ensureLink(dir, link string) error {
|
||||
if isLinked(link, dir) {
|
||||
return nil
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(link), 0755); err != nil {
|
||||
return fmt.Errorf("creating service dir: %w", err)
|
||||
}
|
||||
if err := os.Symlink(dir, link); err != nil {
|
||||
return fmt.Errorf("linking service: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isLinked(link, dir string) bool {
|
||||
target, err := os.Readlink(link)
|
||||
return err == nil && target == dir
|
||||
}
|
||||
|
||||
func setDown(dir string, down bool) error {
|
||||
path := filepath.Join(dir, "down")
|
||||
if !down {
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("removing down file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := os.WriteFile(path, nil, 0644); err != nil {
|
||||
return fmt.Errorf("writing down file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func exists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func hasFile(files []file, name string) bool {
|
||||
for _, f := range files {
|
||||
if f.name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
package runit
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/warkanum/unitdore/config"
|
||||
"github.com/warkanum/unitdore/service"
|
||||
)
|
||||
|
||||
// setup points the backend at temp dirs with recorded commands.
|
||||
func setup(t *testing.T) (b Backend, calls *[]string) {
|
||||
t.Helper()
|
||||
oldSv, oldRun, oldOut, oldRoot, oldWait := svDir, runCmd, outCmd, service.IsRoot, superviseWait
|
||||
t.Cleanup(func() { svDir, runCmd, outCmd, service.IsRoot, superviseWait = oldSv, oldRun, oldOut, oldRoot, oldWait })
|
||||
svDir = t.TempDir()
|
||||
superviseWait = 300 * time.Millisecond
|
||||
service.IsRoot = func() bool { return true }
|
||||
var recorded []string
|
||||
runCmd = func(name string, args ...string) error {
|
||||
recorded = append(recorded, name+" "+strings.Join(args, " "))
|
||||
return nil
|
||||
}
|
||||
return Backend{ServiceDir: t.TempDir()}, &recorded
|
||||
}
|
||||
|
||||
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 read(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func TestGenerate_Container(t *testing.T) {
|
||||
b, _ := setup(t)
|
||||
u := config.Unit{Name: "nginx", Runtime: "podman"}
|
||||
got, err := b.Generate(u, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mustContain(t, got,
|
||||
"===== "+filepath.Join(svDir, "unitdore-nginx", "run")+" =====",
|
||||
"exec 2>&1",
|
||||
"exec /usr/bin/podman start -a nginx",
|
||||
"===== "+filepath.Join(svDir, "unitdore-nginx", "finish")+" =====",
|
||||
"/usr/bin/podman stop nginx || true",
|
||||
"exec sv down .",
|
||||
)
|
||||
}
|
||||
|
||||
func TestGenerate_RestartPolicy(t *testing.T) {
|
||||
b, _ := setup(t)
|
||||
u := config.Unit{Name: "app", Runtime: "podman", Restart: true, RestartSec: 7}
|
||||
got, err := b.Generate(u, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mustContain(t, got, `[ "$1" = 0 ] && exec sv down .`, "sleep 7")
|
||||
if strings.Contains(got, "\nexec sv down .\n") {
|
||||
t.Errorf("restarting unit must not always go down:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerate_ScriptExecAndWorkingDir(t *testing.T) {
|
||||
b, _ := setup(t)
|
||||
u := config.Unit{
|
||||
Name: "agent", Runtime: config.RuntimeExec, Executable: "/opt/agent",
|
||||
Args: []string{"--config", "/etc/my agent.toml"}, WorkingDir: "/opt",
|
||||
}
|
||||
got, err := b.Generate(u, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mustContain(t, got, "cd /opt || exit 1", `exec /opt/agent --config '/etc/my agent.toml'`)
|
||||
}
|
||||
|
||||
func TestGenerate_Oneshot(t *testing.T) {
|
||||
b, _ := setup(t)
|
||||
u := config.Unit{Name: "shop", Runtime: "podman", Kind: config.KindStack, Project: "shop"}
|
||||
got, err := b.Generate(u, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mustContain(t, got,
|
||||
"/usr/bin/podman pod start pod_shop || exit 1\nexec tail -f /dev/null",
|
||||
"/usr/bin/podman pod stop pod_shop || true",
|
||||
)
|
||||
}
|
||||
|
||||
func TestGenerate_RootlessUser(t *testing.T) {
|
||||
b, _ := setup(t)
|
||||
u := config.Unit{Name: "app", Runtime: "podman", User: "root"}
|
||||
got, err := b.Generate(u, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mustContain(t, got,
|
||||
"[ -d /run/user/0 ] || install -d -m 0700 -o root /run/user/0",
|
||||
"export HOME=/root",
|
||||
"export XDG_RUNTIME_DIR=/run/user/0",
|
||||
"exec chpst -u root /usr/bin/podman start -a app",
|
||||
"chpst -u root /usr/bin/podman stop app || true",
|
||||
)
|
||||
}
|
||||
|
||||
func TestGenerate_Invalid(t *testing.T) {
|
||||
b, _ := setup(t)
|
||||
if _, err := b.Generate(config.Unit{Name: "x", Runtime: config.RuntimeScript}, "", ""); err == nil {
|
||||
t.Error("expected validation error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstall_WritesDirLinkAndDown(t *testing.T) {
|
||||
b, _ := setup(t)
|
||||
u := config.Unit{Name: "nginx", Runtime: "podman"}
|
||||
dir := filepath.Join(svDir, "unitdore-nginx")
|
||||
link := filepath.Join(b.ServiceDir, "unitdore-nginx")
|
||||
|
||||
if b.IsInstalled(u, "", "") {
|
||||
t.Fatal("should not be installed yet")
|
||||
}
|
||||
if err := b.Install(u, "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, name := range []string{"run", "finish"} {
|
||||
fi, err := os.Stat(filepath.Join(dir, name))
|
||||
if err != nil || fi.Mode().Perm()&0111 == 0 {
|
||||
t.Errorf("%s missing or not executable: %v", name, err)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "down")); err != nil {
|
||||
t.Error("fresh install must write a down file")
|
||||
}
|
||||
if target, err := os.Readlink(link); err != nil || target != dir {
|
||||
t.Errorf("link = %q, %v", target, err)
|
||||
}
|
||||
if !b.IsInstalled(u, "", "") {
|
||||
t.Error("should be installed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstall_ReinstallKeepsEnabled(t *testing.T) {
|
||||
b, _ := setup(t)
|
||||
u := config.Unit{Name: "nginx", Runtime: "podman"}
|
||||
dir := filepath.Join(svDir, "unitdore-nginx")
|
||||
|
||||
if err := b.Install(u, "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := b.Enable(u, "", ""); err == nil {
|
||||
t.Log("enable ok") // may time out waiting for supervise; down file still removed
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "down")); !os.IsNotExist(err) {
|
||||
t.Fatal("enable must remove the down file")
|
||||
}
|
||||
|
||||
u.Restart = true
|
||||
if err := b.Install(u, "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "down")); !os.IsNotExist(err) {
|
||||
t.Error("re-install must keep an enabled service enabled")
|
||||
}
|
||||
if !strings.Contains(read(t, filepath.Join(dir, "finish")), `"$1" = 0`) {
|
||||
t.Error("re-install must rewrite scripts")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisable_WritesDownAndStops(t *testing.T) {
|
||||
b, calls := setup(t)
|
||||
u := config.Unit{Name: "nginx", Runtime: "podman"}
|
||||
dir := filepath.Join(svDir, "unitdore-nginx")
|
||||
if err := b.Install(u, "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
*calls = nil
|
||||
if err := b.Disable(u, "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "down")); err != nil {
|
||||
t.Error("disable must write a down file")
|
||||
}
|
||||
if got := strings.Join(*calls, "|"); got != "sv stop "+filepath.Join(b.ServiceDir, "unitdore-nginx") {
|
||||
t.Errorf("calls = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartStop_UseSv(t *testing.T) {
|
||||
b, calls := setup(t)
|
||||
u := config.Unit{Name: "nginx", Runtime: "podman"}
|
||||
link := filepath.Join(b.ServiceDir, "unitdore-nginx")
|
||||
if err := b.Install(u, "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Not supervised yet: Start must give up with a clear error
|
||||
if err := b.Start(u, "", ""); err == nil || !strings.Contains(err.Error(), "not supervised") {
|
||||
t.Errorf("Start() err = %v", err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(link, "supervise"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(link, "supervise", "ok"), nil, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
*calls = nil
|
||||
if err := b.Start(u, "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := b.Stop(u, "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := strings.Join(*calls, "|"); got != "sv start "+link+"|sv stop "+link {
|
||||
t.Errorf("calls = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUninstall_RemovesDirAndLink(t *testing.T) {
|
||||
b, calls := setup(t)
|
||||
u := config.Unit{Name: "nginx", Runtime: "podman"}
|
||||
if err := b.Install(u, "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
*calls = nil
|
||||
if err := b.Uninstall(u, "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b.IsInstalled(u, "", "") {
|
||||
t.Error("should be uninstalled")
|
||||
}
|
||||
if _, err := os.Lstat(filepath.Join(b.ServiceDir, "unitdore-nginx")); !os.IsNotExist(err) {
|
||||
t.Error("link should be gone")
|
||||
}
|
||||
if len(*calls) != 1 || !strings.HasPrefix((*calls)[0], "sv stop ") {
|
||||
t.Errorf("calls = %q", *calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceDirAutodetect(t *testing.T) {
|
||||
old := serviceDirCandidates
|
||||
t.Cleanup(func() { serviceDirCandidates = old })
|
||||
existing := t.TempDir()
|
||||
serviceDirCandidates = []string{filepath.Join(existing, "nope"), existing}
|
||||
if got := (Backend{}).serviceDir(); got != existing {
|
||||
t.Errorf("serviceDir() = %q, want %q", got, existing)
|
||||
}
|
||||
if got := (Backend{ServiceDir: "/custom"}).serviceDir(); got != "/custom" {
|
||||
t.Errorf("explicit serviceDir() = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequiresRoot(t *testing.T) {
|
||||
b, _ := setup(t)
|
||||
service.IsRoot = func() bool { return false }
|
||||
u := config.Unit{Name: "nginx", Runtime: "podman"}
|
||||
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{
|
||||
"run: /var/service/x: (pid 123) 5s": "active",
|
||||
"down: /var/service/x: 3s, normally up": "inactive",
|
||||
"finish: /var/service/x: (pid 9) 1s": "deactivating",
|
||||
"fail: /var/service/x: runsv not running": "inactive",
|
||||
"warning: weird": "unknown",
|
||||
}
|
||||
for in, want := range tests {
|
||||
if got := parseStatus(in); got != want {
|
||||
t.Errorf("parseStatus(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user