Files
unitdore/systemd/manager.go
T
warkanum f0827fcdd7 feat: podman-compose stack units and root/user execution scopes
- syncup: podman first, docker only as fallback (podman wins on name clash;
  existing docker units found in podman switch to podman)
- syncup: group podman-compose containers into one `kind: stack` unit per
  project (project, working_dir, compose_files, compose_bin from labels);
  disable existing member units ("part of stack <project>")
- generator: compose mode (podman-compose up/stop, WorkingDirectory) with
  pod start/stop fallback when compose binary/dir/files are missing
- reconcile: stacks disabled with "stack not found" when neither compose
  files nor pod exist
- root: scan root + rootless users listed in `users:` (podman only)
- non-root: config ~/.config/unitdore/units.yaml, own user units only
- sysuser package: runuser with XDG_RUNTIME_DIR/DBUS env, home/linger lookup
- install: warn when linger is not enabled for user units
- testdata/stack: podman-compose test stack and captured podman ps fixture
- docs: README, generated-units, man page, AGENTS, PLAN
2026-10-12 19:05:51 +02:00

150 lines
3.8 KiB
Go

package systemd
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/warkanum/unitdore/config"
"github.com/warkanum/unitdore/sysuser"
)
const systemUnitDir = "/etc/systemd/system"
// UnitPath returns the filesystem path where the .service file should be written.
func UnitPath(u config.Unit, prefix, suffix string) (string, error) {
svcName := ServiceName(u, prefix, suffix)
if u.User == "" {
return filepath.Join(systemUnitDir, svcName), nil
}
// Rootless: write to the user's systemd config dir
home, err := sysuser.HomeDir(u.User)
if err != nil {
return "", err
}
return filepath.Join(home, ".config", "systemd", "user", svcName), nil
}
// Install writes the .service file for a unit and reloads systemd.
func Install(u config.Unit, prefix, suffix string) error {
content, err := Generate(u, prefix, suffix)
if err != nil {
return err
}
path, err := UnitPath(u, prefix, suffix)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return fmt.Errorf("creating unit dir: %w", err)
}
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
return fmt.Errorf("writing unit file: %w", err)
}
return daemonReload(u.User)
}
// Uninstall removes the .service file for a unit and reloads systemd.
func Uninstall(u config.Unit, prefix, suffix string) error {
path, err := UnitPath(u, prefix, suffix)
if err != nil {
return err
}
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("removing unit file: %w", err)
}
return daemonReload(u.User)
}
// Enable enables and starts a unit.
func Enable(u config.Unit, prefix, suffix string) error {
return systemctl(u.User, "enable", "--now", ServiceName(u, prefix, suffix))
}
// Disable disables and stops a unit.
func Disable(u config.Unit, prefix, suffix string) error {
return systemctl(u.User, "disable", "--now", ServiceName(u, prefix, suffix))
}
// Start starts a unit without enabling it.
func Start(u config.Unit, prefix, suffix string) error {
return systemctl(u.User, "start", ServiceName(u, prefix, suffix))
}
// Stop stops a unit without disabling it.
func Stop(u config.Unit, prefix, suffix string) error {
return systemctl(u.User, "stop", ServiceName(u, prefix, suffix))
}
// Status returns the ActiveState of a unit ("active", "inactive", "failed", "unknown").
func Status(u config.Unit, prefix, suffix string) string {
args := []string{"show", "-p", "ActiveState", "--value", ServiceName(u, prefix, suffix)}
var out []byte
var err error
if u.User == "" {
out, err = exec.Command("systemctl", args...).Output()
} else {
out, err = runAsUser(u.User, "systemctl", append([]string{"--user"}, args...)...)
}
if err != nil {
return "unknown"
}
return strings.TrimSpace(string(out))
}
// IsInstalled returns true if the unit file exists on disk.
func IsInstalled(u config.Unit, prefix, suffix string) bool {
path, err := UnitPath(u, prefix, suffix)
if err != nil {
return false
}
_, err = os.Stat(path)
return err == nil
}
// --- helpers ---
func daemonReload(user string) error {
if user == "" {
return systemctl("", "daemon-reload")
}
return systemctl(user, "daemon-reload")
}
func systemctl(user string, args ...string) error {
if user == "" {
cmd := exec.Command("systemctl", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
return runAsUserCmd(user, "systemctl", append([]string{"--user"}, args...)...)
}
func runAsUser(user string, bin string, args ...string) ([]byte, error) {
cmd, err := sysuser.Command(user, bin, args...)
if err != nil {
return nil, err
}
return cmd.Output()
}
func runAsUserCmd(user string, bin string, args ...string) error {
cmd, err := sysuser.Command(user, bin, args...)
if err != nil {
return err
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}