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:
2026-10-12 20:19:58 +02:00
parent 95143dd998
commit df9b226c7e
32 changed files with 2329 additions and 322 deletions
+3 -2
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/warkanum/unitdore/config"
"github.com/warkanum/unitdore/initsys"
"github.com/warkanum/unitdore/sysuser"
)
@@ -16,8 +17,8 @@ func runtimeLabel(u config.Unit) string {
}
// lingerHint prints, once per user, how to make user units start at boot.
func lingerHint(seen map[string]bool, u config.Unit) {
if u.User == "" || seen[u.User] || sysuser.LingerEnabled(u.User) {
func lingerHint(ini initsys.Init, seen map[string]bool, u config.Unit) {
if !ini.SupportsLinger() || u.User == "" || seen[u.User] || sysuser.LingerEnabled(u.User) {
return
}
seen[u.User] = true
+14 -10
View File
@@ -5,7 +5,7 @@ import (
"github.com/spf13/cobra"
"github.com/warkanum/unitdore/config"
"github.com/warkanum/unitdore/systemd"
"github.com/warkanum/unitdore/initsys"
)
var dryRun bool
@@ -32,6 +32,10 @@ func runInstall(cmd *cobra.Command, args []string) error {
}
prefix, suffix := cfg.Prefix, cfg.Suffix
ini, err := initsys.New(cfg)
if err != nil {
return err
}
installed := 0
skipped := 0
removed := 0
@@ -40,14 +44,14 @@ func runInstall(cmd *cobra.Command, args []string) error {
for _, u := range cfg.Units {
if !u.Enabled {
// Remove unit file if it exists for disabled units
if systemd.IsInstalled(u, prefix, suffix) {
if ini.IsInstalled(u, prefix, suffix) {
if dryRun {
fmt.Printf(" ~ would remove: %s\n", systemd.ServiceName(u, prefix, suffix))
fmt.Printf(" ~ would remove: %s\n", ini.ServiceName(u, prefix, suffix))
} else {
if err := systemd.Uninstall(u, prefix, suffix); err != nil {
if err := ini.Uninstall(u, prefix, suffix); err != nil {
fmt.Printf(" ✗ failed to remove %s: %v\n", u.Name, err)
} else {
fmt.Printf(" - removed: %s (disabled)\n", systemd.ServiceName(u, prefix, suffix))
fmt.Printf(" - removed: %s (disabled)\n", ini.ServiceName(u, prefix, suffix))
removed++
}
}
@@ -58,23 +62,23 @@ func runInstall(cmd *cobra.Command, args []string) error {
}
if dryRun {
content, err := systemd.Generate(u, prefix, suffix)
content, err := ini.Generate(u, prefix, suffix)
if err != nil {
fmt.Printf(" ✗ %s: %v\n", u.Name, err)
continue
}
path, _ := systemd.UnitPath(u, prefix, suffix)
path, _ := ini.UnitPath(u, prefix, suffix)
fmt.Printf("\n--- %s ---\n%s\n", path, content)
installed++
continue
}
if err := systemd.Install(u, prefix, suffix); err != nil {
if err := ini.Install(u, prefix, suffix); err != nil {
fmt.Printf(" ✗ failed: %s: %v\n", u.Name, err)
} else {
path, _ := systemd.UnitPath(u, prefix, suffix)
path, _ := ini.UnitPath(u, prefix, suffix)
fmt.Printf(" ✓ installed: %s\n", path)
lingerHint(lingerSeen, u)
lingerHint(ini, lingerSeen, u)
installed++
}
}
+14 -10
View File
@@ -5,7 +5,7 @@ import (
"github.com/spf13/cobra"
"github.com/warkanum/unitdore/config"
"github.com/warkanum/unitdore/systemd"
"github.com/warkanum/unitdore/initsys"
)
var installallDryRun bool
@@ -32,6 +32,10 @@ func runInstallall(cmd *cobra.Command, args []string) error {
}
prefix, suffix := cfg.Prefix, cfg.Suffix
ini, err := initsys.New(cfg)
if err != nil {
return err
}
installed := 0
skipped := 0
removed := 0
@@ -39,14 +43,14 @@ func runInstallall(cmd *cobra.Command, args []string) error {
for _, u := range cfg.Units {
if !u.Enabled {
if systemd.IsInstalled(u, prefix, suffix) {
if ini.IsInstalled(u, prefix, suffix) {
if installallDryRun {
fmt.Printf(" ~ would remove: %s\n", systemd.ServiceName(u, prefix, suffix))
fmt.Printf(" ~ would remove: %s\n", ini.ServiceName(u, prefix, suffix))
} else {
if err := systemd.Uninstall(u, prefix, suffix); err != nil {
if err := ini.Uninstall(u, prefix, suffix); err != nil {
fmt.Printf(" ✗ failed to remove %s: %v\n", u.Name, err)
} else {
fmt.Printf(" - removed: %s (disabled)\n", systemd.ServiceName(u, prefix, suffix))
fmt.Printf(" - removed: %s (disabled)\n", ini.ServiceName(u, prefix, suffix))
removed++
}
}
@@ -57,23 +61,23 @@ func runInstallall(cmd *cobra.Command, args []string) error {
}
if installallDryRun {
content, err := systemd.Generate(u, prefix, suffix)
content, err := ini.Generate(u, prefix, suffix)
if err != nil {
fmt.Printf(" ✗ %s: %v\n", u.Name, err)
continue
}
path, _ := systemd.UnitPath(u, prefix, suffix)
path, _ := ini.UnitPath(u, prefix, suffix)
fmt.Printf("\n--- %s ---\n%s\n", path, content)
installed++
continue
}
if err := systemd.Install(u, prefix, suffix); err != nil {
if err := ini.Install(u, prefix, suffix); err != nil {
fmt.Printf(" ✗ failed: %s: %v\n", u.Name, err)
} else {
path, _ := systemd.UnitPath(u, prefix, suffix)
path, _ := ini.UnitPath(u, prefix, suffix)
fmt.Printf(" ✓ installed: %s\n", path)
lingerHint(lingerSeen, u)
lingerHint(ini, lingerSeen, u)
installed++
}
}
+7 -3
View File
@@ -7,7 +7,7 @@ import (
"github.com/spf13/cobra"
"github.com/warkanum/unitdore/config"
"github.com/warkanum/unitdore/systemd"
"github.com/warkanum/unitdore/initsys"
)
var listCmd = &cobra.Command{
@@ -41,6 +41,10 @@ func runList(cmd *cobra.Command, args []string) error {
})
prefix, suffix := cfg.Prefix, cfg.Suffix
ini, err := initsys.New(cfg)
if err != nil {
return err
}
type row struct{ name, runtime, enabled, installed, path string }
@@ -53,9 +57,9 @@ func runList(cmd *cobra.Command, args []string) error {
installed := "no"
unitPath := "—"
if p, err := systemd.UnitPath(u, prefix, suffix); err == nil {
if p, err := ini.UnitPath(u, prefix, suffix); err == nil {
unitPath = p
if systemd.IsInstalled(u, prefix, suffix) {
if ini.IsInstalled(u, prefix, suffix) {
installed = "yes"
}
}
+3 -2
View File
@@ -16,10 +16,11 @@ var configPath string
var rootCmd = &cobra.Command{
Use: "unitdore",
Short: "A door you open and close for container units",
Long: `Unitdore manages container units via systemd.
Long: `Unitdore manages container units via the system init (systemd, OpenRC or runit).
It discovers running containers and podman-compose stacks, stores them in a
config file, and generates + manages systemd .service units for each one.
config file, and generates + manages init service units for each one (systemd by default;
set 'init:' in the config for openrc or runit).
As root: system units, plus user units for users listed in 'users:'.
As a normal user: own config and user units only.`,
+8 -4
View File
@@ -5,7 +5,7 @@ import (
"github.com/spf13/cobra"
"github.com/warkanum/unitdore/config"
"github.com/warkanum/unitdore/systemd"
"github.com/warkanum/unitdore/initsys"
)
var startCmd = &cobra.Command{
@@ -32,13 +32,17 @@ func runStart(cmd *cobra.Command, args []string) error {
}
prefix, suffix := cfg.Prefix, cfg.Suffix
ini, err := initsys.New(cfg)
if err != nil {
return err
}
if !systemd.IsInstalled(*u, prefix, suffix) {
if !ini.IsInstalled(*u, prefix, suffix) {
return fmt.Errorf("%s is not installed — run 'unitdore install' first", name)
}
fmt.Printf(" ▶ starting %s...\n", systemd.ServiceName(*u, prefix, suffix))
if err := systemd.Start(*u, prefix, suffix); err != nil {
fmt.Printf(" ▶ starting %s...\n", ini.ServiceName(*u, prefix, suffix))
if err := ini.Start(*u, prefix, suffix); err != nil {
return err
}
fmt.Printf(" ✓ started: %s\n", name)
+8 -4
View File
@@ -6,7 +6,7 @@ import (
"github.com/spf13/cobra"
"github.com/warkanum/unitdore/config"
"github.com/warkanum/unitdore/systemd"
"github.com/warkanum/unitdore/initsys"
)
var startallCmd = &cobra.Command{
@@ -28,6 +28,10 @@ func runStartall(cmd *cobra.Command, args []string) error {
}
prefix, suffix := cfg.Prefix, cfg.Suffix
ini, err := initsys.New(cfg)
if err != nil {
return err
}
units := make([]config.Unit, len(cfg.Units))
copy(units, cfg.Units)
@@ -45,12 +49,12 @@ func runStartall(cmd *cobra.Command, args []string) error {
if !u.Enabled {
continue
}
if !systemd.IsInstalled(u, prefix, suffix) {
if !ini.IsInstalled(u, prefix, suffix) {
fmt.Printf(" ! %s: not installed — run 'unitdore install' first\n", u.Name)
continue
}
fmt.Printf(" ▶ starting: %s...\n", systemd.ServiceName(u, prefix, suffix))
if err := systemd.Start(u, prefix, suffix); err != nil {
fmt.Printf(" ▶ starting: %s...\n", ini.ServiceName(u, prefix, suffix))
if err := ini.Start(u, prefix, suffix); err != nil {
fmt.Printf(" ✗ failed: %s: %v\n", u.Name, err)
failed++
} else {
+8 -4
View File
@@ -7,7 +7,7 @@ import (
"github.com/spf13/cobra"
"github.com/warkanum/unitdore/config"
"github.com/warkanum/unitdore/systemd"
"github.com/warkanum/unitdore/initsys"
)
var statusCmd = &cobra.Command{
@@ -37,6 +37,10 @@ func runStatus(cmd *cobra.Command, args []string) error {
}
prefix, suffix := cfg.Prefix, cfg.Suffix
ini, err := initsys.New(cfg)
if err != nil {
return err
}
if len(cfg.Units) == 0 {
fmt.Println("No units configured. Run 'unitdore syncup' to discover containers.")
@@ -65,13 +69,13 @@ func runStatus(cmd *cobra.Command, args []string) error {
}
installed := "yes"
if !systemd.IsInstalled(u, prefix, suffix) {
if !ini.IsInstalled(u, prefix, suffix) {
installed = "no"
}
state := "—"
if systemd.IsInstalled(u, prefix, suffix) {
state = systemd.Status(u, prefix, suffix)
if ini.IsInstalled(u, prefix, suffix) {
state = ini.Status(u, prefix, suffix)
}
rows = append(rows, statusRow{
+8 -4
View File
@@ -5,7 +5,7 @@ import (
"github.com/spf13/cobra"
"github.com/warkanum/unitdore/config"
"github.com/warkanum/unitdore/systemd"
"github.com/warkanum/unitdore/initsys"
)
var stopCmd = &cobra.Command{
@@ -32,13 +32,17 @@ func runStop(cmd *cobra.Command, args []string) error {
}
prefix, suffix := cfg.Prefix, cfg.Suffix
ini, err := initsys.New(cfg)
if err != nil {
return err
}
if !systemd.IsInstalled(*u, prefix, suffix) {
if !ini.IsInstalled(*u, prefix, suffix) {
return fmt.Errorf("%s is not installed", name)
}
fmt.Printf(" ■ stopping %s...\n", systemd.ServiceName(*u, prefix, suffix))
if err := systemd.Stop(*u, prefix, suffix); err != nil {
fmt.Printf(" ■ stopping %s...\n", ini.ServiceName(*u, prefix, suffix))
if err := ini.Stop(*u, prefix, suffix); err != nil {
return err
}
fmt.Printf(" ✓ stopped: %s\n", name)
+8 -4
View File
@@ -6,7 +6,7 @@ import (
"github.com/spf13/cobra"
"github.com/warkanum/unitdore/config"
"github.com/warkanum/unitdore/systemd"
"github.com/warkanum/unitdore/initsys"
)
var stopallCmd = &cobra.Command{
@@ -27,6 +27,10 @@ func runStopall(cmd *cobra.Command, args []string) error {
}
prefix, suffix := cfg.Prefix, cfg.Suffix
ini, err := initsys.New(cfg)
if err != nil {
return err
}
// Reverse order for shutdown
units := make([]config.Unit, len(cfg.Units))
@@ -42,11 +46,11 @@ func runStopall(cmd *cobra.Command, args []string) error {
failed := 0
for _, u := range units {
if !systemd.IsInstalled(u, prefix, suffix) {
if !ini.IsInstalled(u, prefix, suffix) {
continue
}
fmt.Printf(" ■ stopping: %s...\n", systemd.ServiceName(u, prefix, suffix))
if err := systemd.Stop(u, prefix, suffix); err != nil {
fmt.Printf(" ■ stopping: %s...\n", ini.ServiceName(u, prefix, suffix))
if err := ini.Stop(u, prefix, suffix); err != nil {
fmt.Printf(" ✗ failed: %s: %v\n", u.Name, err)
failed++
} else {
+2 -2
View File
@@ -9,7 +9,7 @@ import (
"github.com/spf13/cobra"
"github.com/warkanum/unitdore/config"
"github.com/warkanum/unitdore/runtime"
"github.com/warkanum/unitdore/systemd"
"github.com/warkanum/unitdore/service"
"github.com/warkanum/unitdore/sysuser"
)
@@ -221,7 +221,7 @@ func mergeDiscovered(cfg *config.Config, containers []runtime.Container, compose
// unitExists checks whether a unit's container or stack still exists.
func unitExists(u config.Unit) (bool, error) {
if u.IsStack() {
if systemd.ComposeAvailable(u) {
if service.ComposeAvailable(u) {
return true, nil
}
return (&runtime.Podman{}).PodExists(u.User, runtime.PodName(u.Project))
+10 -6
View File
@@ -5,7 +5,7 @@ import (
"github.com/spf13/cobra"
"github.com/warkanum/unitdore/config"
"github.com/warkanum/unitdore/systemd"
"github.com/warkanum/unitdore/initsys"
)
var uninstallCmd = &cobra.Command{
@@ -32,21 +32,25 @@ func runUninstall(cmd *cobra.Command, args []string) error {
}
prefix, suffix := cfg.Prefix, cfg.Suffix
ini, err := initsys.New(cfg)
if err != nil {
return err
}
if !systemd.IsInstalled(*u, prefix, suffix) {
if !ini.IsInstalled(*u, prefix, suffix) {
return fmt.Errorf("%s is not installed", name)
}
svcName := systemd.ServiceName(*u, prefix, suffix)
svcName := ini.ServiceName(*u, prefix, suffix)
fmt.Printf(" ■ disabling %s...\n", svcName)
if err := systemd.Disable(*u, prefix, suffix); err != nil {
if err := ini.Disable(*u, prefix, suffix); err != nil {
return fmt.Errorf("disabling unit: %w", err)
}
path, _ := systemd.UnitPath(*u, prefix, suffix)
path, _ := ini.UnitPath(*u, prefix, suffix)
fmt.Printf(" ✗ removing %s...\n", path)
if err := systemd.Uninstall(*u, prefix, suffix); err != nil {
if err := ini.Uninstall(*u, prefix, suffix); err != nil {
return fmt.Errorf("removing service file: %w", err)
}
+10 -6
View File
@@ -6,7 +6,7 @@ import (
"github.com/spf13/cobra"
"github.com/warkanum/unitdore/config"
"github.com/warkanum/unitdore/systemd"
"github.com/warkanum/unitdore/initsys"
)
var uninstallallCmd = &cobra.Command{
@@ -27,6 +27,10 @@ func runUninstallall(cmd *cobra.Command, args []string) error {
}
prefix, suffix := cfg.Prefix, cfg.Suffix
ini, err := initsys.New(cfg)
if err != nil {
return err
}
units := make([]config.Unit, len(cfg.Units))
copy(units, cfg.Units)
@@ -41,19 +45,19 @@ func runUninstallall(cmd *cobra.Command, args []string) error {
failed := 0
for _, u := range units {
if !systemd.IsInstalled(u, prefix, suffix) {
if !ini.IsInstalled(u, prefix, suffix) {
continue
}
svcName := systemd.ServiceName(u, prefix, suffix)
svcName := ini.ServiceName(u, prefix, suffix)
fmt.Printf(" ■ disabling %s...\n", svcName)
if err := systemd.Disable(u, prefix, suffix); err != nil {
if err := ini.Disable(u, prefix, suffix); err != nil {
fmt.Printf(" ✗ failed to disable %s: %v\n", u.Name, err)
failed++
continue
}
path, _ := systemd.UnitPath(u, prefix, suffix)
path, _ := ini.UnitPath(u, prefix, suffix)
fmt.Printf(" ✗ removing %s...\n", path)
if err := systemd.Uninstall(u, prefix, suffix); err != nil {
if err := ini.Uninstall(u, prefix, suffix); err != nil {
fmt.Printf(" ✗ failed to remove %s: %v\n", u.Name, err)
failed++
continue
+8 -4
View File
@@ -6,7 +6,7 @@ import (
"github.com/spf13/cobra"
"github.com/warkanum/unitdore/config"
"github.com/warkanum/unitdore/systemd"
"github.com/warkanum/unitdore/initsys"
)
var (
@@ -64,12 +64,16 @@ func runUpdate(cmd *cobra.Command, args []string) error {
}
prefix, suffix := cfg.Prefix, cfg.Suffix
ini, err := initsys.New(cfg)
if err != nil {
return err
}
if systemd.IsInstalled(*u, prefix, suffix) {
if err := systemd.Install(*u, prefix, suffix); err != nil {
if ini.IsInstalled(*u, prefix, suffix) {
if err := ini.Install(*u, prefix, suffix); err != nil {
return fmt.Errorf("recreating service file: %w", err)
}
path, _ := systemd.UnitPath(*u, prefix, suffix)
path, _ := ini.UnitPath(*u, prefix, suffix)
fmt.Printf(" ✓ service file recreated: %s\n", path)
}