- config: add Prefix/Suffix fields to Config struct - systemd: ServiceName/Generate/UnitPath/Install/Uninstall/Enable/Disable/Status all accept prefix+suffix - runtime: fall back to short container ID (12 chars) when container has no name - cmd: active, status, install all thread prefix/suffix from config - systemd/generator_test.go: updated all calls + added TestGenerate_WithPrefixSuffix - docs/generated-units.md: full examples of every unit type + ordering + naming - README: updated config docs, prefix/suffix section, link to docs/
147 lines
3.8 KiB
Go
147 lines
3.8 KiB
Go
package systemd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/warkanum/unitdore/config"
|
|
)
|
|
|
|
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 := userHomeDir(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))
|
|
}
|
|
|
|
// 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) {
|
|
full := append([]string{"-u", user, "--", bin}, args...)
|
|
return exec.Command("runuser", full...).Output()
|
|
}
|
|
|
|
func runAsUserCmd(user string, bin string, args ...string) error {
|
|
full := append([]string{"-u", user, "--", bin}, args...)
|
|
cmd := exec.Command("runuser", full...)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
return cmd.Run()
|
|
}
|
|
|
|
func userHomeDir(user string) (string, error) {
|
|
out, err := exec.Command("getent", "passwd", user).Output()
|
|
if err != nil {
|
|
return "", fmt.Errorf("looking up user %s: %w", user, err)
|
|
}
|
|
// passwd format: name:pass:uid:gid:gecos:home:shell
|
|
parts := strings.Split(strings.TrimSpace(string(out)), ":")
|
|
if len(parts) < 6 {
|
|
return "", fmt.Errorf("unexpected passwd entry for %s", user)
|
|
}
|
|
return parts[5], nil
|
|
}
|