From df9b226c7ed5c1660483cc8560e5152ca2add1ec Mon Sep 17 00:00:00 2001 From: Hein Date: Mon, 12 Oct 2026 20:19:58 +0200 Subject: [PATCH] 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. --- AGENTS.md | 17 +- README.md | 25 ++- cmd/helpers.go | 5 +- cmd/install.go | 24 ++- cmd/installall.go | 24 ++- cmd/list.go | 10 +- cmd/root.go | 5 +- cmd/start.go | 12 +- cmd/startall.go | 12 +- cmd/status.go | 12 +- cmd/stop.go | 12 +- cmd/stopall.go | 12 +- cmd/syncup.go | 4 +- cmd/uninstall.go | 16 +- cmd/uninstallall.go | 16 +- cmd/update.go | 12 +- config/config.go | 3 + docs/generated-units.md | 324 ++++++++++++++++++++++++++++++++- docs/unitdore.1 | 11 +- initsys/initsys.go | 77 ++++++++ initsys/initsys_test.go | 52 ++++++ openrc/openrc.go | 279 ++++++++++++++++++++++++++++ openrc/openrc_test.go | 221 ++++++++++++++++++++++ runit/runit.go | 364 +++++++++++++++++++++++++++++++++++++ runit/runit_test.go | 295 ++++++++++++++++++++++++++++++ service/spec.go | 294 ++++++++++++++++++++++++++++++ service/spec_test.go | 215 ++++++++++++++++++++++ systemd/backend.go | 37 ++++ systemd/generator.go | 155 +--------------- systemd/generator_test.go | 81 --------- systemd/start_stop_test.go | 25 --- typescript | 0 32 files changed, 2329 insertions(+), 322 deletions(-) create mode 100644 initsys/initsys.go create mode 100644 initsys/initsys_test.go create mode 100644 openrc/openrc.go create mode 100644 openrc/openrc_test.go create mode 100644 runit/runit.go create mode 100644 runit/runit_test.go create mode 100644 service/spec.go create mode 100644 service/spec_test.go create mode 100644 systemd/backend.go create mode 100644 typescript diff --git a/AGENTS.md b/AGENTS.md index 4a2b89a..9277441 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ ## Project Overview -**Unitdore** is a Go CLI tool that bridges container runtimes (Podman, Docker) and systemd. It discovers running containers, stores them in a YAML config, and generates + manages systemd `.service` unit files for each one. +**Unitdore** is a Go CLI tool that bridges container runtimes (Podman, Docker) and the init system (systemd, OpenRC, runit). It discovers running containers, stores them in a YAML config, and generates + manages service files for each one. # Agent Rules @@ -33,7 +33,11 @@ config/ # Config struct + YAML load/save (units.yaml) runtime/ # Docker and Podman runtime abstraction, compose labels sysuser/ # run commands as root/user (runuser), home/linger lookup testdata/stack # podman-compose test stack + captured `podman ps` fixture -systemd/ # .service file generator and systemd manager +service/ # init-neutral unit resolution: commands, quoting, user env +initsys/ # Init interface, backend selection (config init: / autodetect) +systemd/ # .service generator + systemctl manager (Backend) +openrc/ # /etc/init.d script generator + rc-service manager (Backend) +runit/ # /etc/sv run/finish generator + sv manager (Backend) docs/ # generated-units.md (unit file examples), unitdore.1 (man page) ``` @@ -41,13 +45,18 @@ docs/ # generated-units.md (unit file examples), unitdore.1 (man page) - **`config`** — `Unit` and `Config` structs; `Load`, `Save`, `FindUnit`, `AddUnit` - **`runtime`** — `ContainerRuntime` interface; `docker.go`, `podman.go` implementations +- **`service`** — `Resolve(unit)` → start/stop commands, type, workdir; shared by all backends +- **`initsys`** — `Init` interface; `New(cfg)` picks backend; cmd/ only talks to `Init` - **`systemd`** — `generator.go` builds `.service` file content; `manager.go` calls `systemctl` +- **`openrc`**, **`runit`** — same role; root only; tests stub command execution via package vars ### Config file **Location:** `/etc/unitdore/units.yaml` ```yaml +init: "" # systemd | openrc | runit (empty = autodetect) +service_dir: "" # runit: supervised dir (empty = autodetect) prefix: "" # prepended to service file name (e.g. "prod-") suffix: "" # appended to service file name (e.g. "-svc") users: [] # root only: rootless users scanned by syncup @@ -101,6 +110,10 @@ Tests exist in: - `runtime/runtime_test.go` - `cmd/syncup_test.go` - `sysuser/sysuser_test.go` +- `service/spec_test.go` +- `initsys/initsys_test.go` +- `openrc/openrc_test.go` +- `runit/runit_test.go` - `systemd/generator_test.go` Always run `make test` before marking a task complete. Add tests for new behaviour in the same package as the code under test. diff --git a/README.md b/README.md index b390c13..3e6e054 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > *A door you open and close for container units.* -Unitdore bridges your container runtime (Podman, Docker) and systemd. It discovers running containers and podman-compose stacks, stores them in a config file, and generates + manages systemd `.service` units for each one. +Unitdore bridges your container runtime (Podman, Docker) and your init system (systemd, OpenRC, runit). It discovers running containers and podman-compose stacks, stores them in a config file, and generates + manages a service for each one. ## Install @@ -107,6 +107,8 @@ oldthing podman hein no no — container **Location:** `/etc/unitdore/units.yaml` ```yaml +init: "" # optional: systemd | openrc | runit (empty = autodetect) +service_dir: "" # optional: runit supervised dir (empty = /var/service or /etc/service) prefix: "" # optional: prepended to all service names, e.g. "prod-" suffix: "" # optional: appended to all service names, e.g. "-svc" users: [hein] # root only: rootless users scanned by syncup @@ -158,6 +160,27 @@ units: enabled: true ``` +### Init systems + +Set `init:` in the config, or leave empty to autodetect (`/run/systemd/system` → systemd, `/run/openrc` → openrc, `/etc/runit` → runit, else systemd). + +| | systemd | openrc | runit | +|---|---|---|---| +| Service file | `/etc/systemd/system/unitdore-.service` | `/etc/init.d/unitdore-` | `/etc/sv/unitdore-/{run,finish}` + symlink in `service_dir` | +| Start / stop | `systemctl` | `rc-service` | `sv` | +| Enable at boot | `systemctl enable` | `rc-update add … default` | remove `down` file | +| Installed state | disabled until enabled | not in runlevel | symlinked with `down` file (no autostart) | +| Rootless `user:` | `systemctl --user` unit (needs linger) | system service, `command_user` | system service, `chpst -u` | +| Non-root use | yes (own user units) | **root only** | **root only** | +| `restart*` | `Restart=on-failure`, `StartLimitBurst` | `supervise-daemon` `respawn_*` (always respawns) | `finish`: on-failure, `RestartSec`; **no retry limit** | +| `docker` dependency | `Requires=docker.service` | `need docker` | none | +| `oneshot` | `RemainAfterExit` | `start()`/`stop()` functions | run once, then `tail -f /dev/null` | + +- Rootless units on openrc/runit set `HOME` and `XDG_RUNTIME_DIR=/run/user/` and create that dir if missing. +- `command` / `args` use systemd quoting; they are re-quoted for sh on openrc/runit. +- No log redirection is generated on openrc; use the runtime's own logs. +- `docs/generated-units.md` shows systemd output only. + ### Script / executable units Added manually (`unitdore edit`); syncup never creates or disables them. diff --git a/cmd/helpers.go b/cmd/helpers.go index 3cfcbd9..e3237d5 100644 --- a/cmd/helpers.go +++ b/cmd/helpers.go @@ -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 diff --git a/cmd/install.go b/cmd/install.go index 7c8e528..38b08fa 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -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++ } } diff --git a/cmd/installall.go b/cmd/installall.go index 202dd8e..a690772 100644 --- a/cmd/installall.go +++ b/cmd/installall.go @@ -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++ } } diff --git a/cmd/list.go b/cmd/list.go index 90508a3..b1824df 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -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" } } diff --git a/cmd/root.go b/cmd/root.go index f8552a7..6b35419 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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.`, diff --git a/cmd/start.go b/cmd/start.go index 033436a..15fc541 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -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) diff --git a/cmd/startall.go b/cmd/startall.go index fffbda8..9b4c44f 100644 --- a/cmd/startall.go +++ b/cmd/startall.go @@ -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 { diff --git a/cmd/status.go b/cmd/status.go index f66cf09..641699e 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -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{ diff --git a/cmd/stop.go b/cmd/stop.go index a95f23c..8ad4d8b 100644 --- a/cmd/stop.go +++ b/cmd/stop.go @@ -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) diff --git a/cmd/stopall.go b/cmd/stopall.go index f3515bb..c91ba56 100644 --- a/cmd/stopall.go +++ b/cmd/stopall.go @@ -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 { diff --git a/cmd/syncup.go b/cmd/syncup.go index 0e52539..4cd54e2 100644 --- a/cmd/syncup.go +++ b/cmd/syncup.go @@ -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)) diff --git a/cmd/uninstall.go b/cmd/uninstall.go index 1456372..1a2be9e 100644 --- a/cmd/uninstall.go +++ b/cmd/uninstall.go @@ -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) } diff --git a/cmd/uninstallall.go b/cmd/uninstallall.go index 0989224..164614a 100644 --- a/cmd/uninstallall.go +++ b/cmd/uninstallall.go @@ -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 diff --git a/cmd/update.go b/cmd/update.go index 19f48f3..fc43b5c 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -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) } diff --git a/config/config.go b/config/config.go index 3991f7b..1d39678 100644 --- a/config/config.go +++ b/config/config.go @@ -87,6 +87,9 @@ type Config struct { Prefix string `yaml:"prefix,omitempty"` // prepended to generated service name, e.g. "prod-" Suffix string `yaml:"suffix,omitempty"` // appended to generated service name, e.g. "-svc" Users []string `yaml:"users,omitempty"` // rootless users scanned by syncup when run as root + + Init string `yaml:"init,omitempty"` // systemd | openrc | runit (empty = autodetect) + ServiceDir string `yaml:"service_dir,omitempty"` // runit: supervised service dir (empty = autodetect) } // Load reads and parses the config file at path. diff --git a/docs/generated-units.md b/docs/generated-units.md index 0f33548..a1af1bb 100644 --- a/docs/generated-units.md +++ b/docs/generated-units.md @@ -1,4 +1,4 @@ -# Generated Systemd Unit Files +# Generated Unit Files (systemd, OpenRC, runit) Unitdore generates `.service` files from your `units.yaml` config. This document shows every combination with annotated examples. @@ -330,6 +330,326 @@ ExecStop=-/usr/bin/podman pod stop pod_shop --- +## Config Behind the Init System Examples + +```yaml +init: systemd # systemd | openrc | runit +units: + - {name: nginx, runtime: podman, order: 1, enabled: true} + - {name: redis, runtime: docker, order: 2, enabled: true, restart: true, restart_sec: 5, restart_retries: 3} + - {name: myapp, runtime: podman, user: alice, order: 3, enabled: true} + - {name: backup, runtime: script, script: /opt/scripts/backup.sh, stop_script: /opt/scripts/stop.sh, shell: /bin/sh, working_dir: /opt/scripts, order: 4, enabled: true} + - {name: agent, runtime: exec, executable: /opt/agent/bin/agent, args: [--config, /etc/agent.toml], service_type: oneshot, order: 5, enabled: true} + - {name: shop, runtime: podman, kind: stack, project: shop, order: 6, enabled: true} +``` + +--- + +## Script and Executable Units (systemd) + +`script` runs `