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:
@@ -0,0 +1,77 @@
|
||||
// Package initsys selects the init system backend that installs and controls
|
||||
// unitdore services.
|
||||
package initsys
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/warkanum/unitdore/config"
|
||||
"github.com/warkanum/unitdore/openrc"
|
||||
"github.com/warkanum/unitdore/runit"
|
||||
"github.com/warkanum/unitdore/systemd"
|
||||
)
|
||||
|
||||
// Backend names.
|
||||
const (
|
||||
Systemd = "systemd"
|
||||
OpenRC = "openrc"
|
||||
Runit = "runit"
|
||||
)
|
||||
|
||||
// Init is implemented by every init system backend. u.User selects the scope;
|
||||
// backends that cannot do per-user managers run user units as system services.
|
||||
type Init interface {
|
||||
Name() string
|
||||
ServiceName(u config.Unit, prefix, suffix string) string
|
||||
UnitPath(u config.Unit, prefix, suffix string) (string, error)
|
||||
Generate(u config.Unit, prefix, suffix string) (string, error)
|
||||
Install(u config.Unit, prefix, suffix string) error
|
||||
Uninstall(u config.Unit, prefix, suffix string) error
|
||||
Enable(u config.Unit, prefix, suffix string) error
|
||||
Disable(u config.Unit, prefix, suffix string) error
|
||||
Start(u config.Unit, prefix, suffix string) error
|
||||
Stop(u config.Unit, prefix, suffix string) error
|
||||
// Status returns active | inactive | failed | activating | deactivating | unknown.
|
||||
Status(u config.Unit, prefix, suffix string) string
|
||||
IsInstalled(u config.Unit, prefix, suffix string) bool
|
||||
// SupportsLinger reports whether user units need systemd-style linger.
|
||||
SupportsLinger() bool
|
||||
}
|
||||
|
||||
// New returns the backend named in cfg.Init, or the detected one.
|
||||
func New(cfg *config.Config) (Init, error) {
|
||||
name := cfg.Init
|
||||
if name == "" {
|
||||
name = Detect("/")
|
||||
}
|
||||
switch name {
|
||||
case Systemd:
|
||||
return systemd.Backend{}, nil
|
||||
case OpenRC:
|
||||
return openrc.Backend{}, nil
|
||||
case Runit:
|
||||
return runit.Backend{ServiceDir: cfg.ServiceDir}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown init %q (want systemd, openrc or runit)", name)
|
||||
}
|
||||
}
|
||||
|
||||
// Detect guesses the running init system below root. Falls back to systemd.
|
||||
func Detect(root string) string {
|
||||
has := func(p string) bool {
|
||||
_, err := os.Stat(filepath.Join(root, p))
|
||||
return err == nil
|
||||
}
|
||||
switch {
|
||||
case has("run/systemd/system"):
|
||||
return Systemd
|
||||
case has("run/openrc"), has("sbin/openrc-run"):
|
||||
return OpenRC
|
||||
case has("run/runit"), has("etc/runit"):
|
||||
return Runit
|
||||
default:
|
||||
return Systemd
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package initsys
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/warkanum/unitdore/config"
|
||||
)
|
||||
|
||||
func TestDetect(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
paths []string
|
||||
want string
|
||||
}{
|
||||
{"systemd", []string{"run/systemd/system"}, Systemd},
|
||||
{"openrc", []string{"run/openrc"}, OpenRC},
|
||||
{"openrc binary", []string{"sbin/openrc-run"}, OpenRC},
|
||||
{"runit", []string{"etc/runit"}, Runit},
|
||||
{"systemd wins", []string{"run/systemd/system", "etc/runit"}, Systemd},
|
||||
{"fallback", nil, Systemd},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
for _, p := range tt.paths {
|
||||
if err := os.MkdirAll(filepath.Dir(filepath.Join(root, p)), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, p), nil, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if got := Detect(root); got != tt.want {
|
||||
t.Errorf("Detect() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
for _, name := range []string{Systemd, OpenRC, Runit} {
|
||||
ini, err := New(&config.Config{Init: name})
|
||||
if err != nil || ini.Name() != name {
|
||||
t.Errorf("New(%q) = %v, %v", name, ini, err)
|
||||
}
|
||||
}
|
||||
if _, err := New(&config.Config{Init: "sysv"}); err == nil {
|
||||
t.Error("New() should reject an unknown init")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user