Files
unitdore/systemd/generator.go
T
warkanum df9b226c7e 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.
2026-10-12 20:19:58 +02:00

130 lines
3.0 KiB
Go

package systemd
import (
"fmt"
"strings"
"text/template"
"github.com/warkanum/unitdore/config"
"github.com/warkanum/unitdore/service"
)
const systemUnitTemplate = `# /etc/systemd/system/{{.ServiceName}}
# Generated by unitdore — do not edit manually
[Unit]
Description=Unitdore: {{.Unit.Name}} ({{.Label}})
After=network.target{{with .RuntimeService}} {{.}}
Requires={{.}}{{end}}
[Service]
Type={{.ServiceType}}
{{- if eq .ServiceType "oneshot"}}
RemainAfterExit=yes
{{- end}}
{{- with .WorkingDir}}
WorkingDirectory={{.}}
{{- end}}
ExecStart={{.ExecStart}}
ExecStop={{if .ExecStop}}-{{.ExecStop}}{{end}}
{{- if .Unit.Restart}}
Restart=on-failure
RestartSec={{.Unit.RestartSec}}
{{- if gt .Unit.RestartRetries 0}}
StartLimitBurst={{.Unit.RestartRetries}}
{{- end}}
{{- end}}
[Install]
WantedBy=multi-user.target
`
const userUnitTemplate = `# ~/.config/systemd/user/{{.ServiceName}}
# Generated by unitdore — do not edit manually
[Unit]
Description=Unitdore: {{.Unit.Name}} ({{.Label}})
After=default.target
[Service]
Type={{.ServiceType}}
{{- if eq .ServiceType "oneshot"}}
RemainAfterExit=yes
{{- end}}
{{- with .WorkingDir}}
WorkingDirectory={{.}}
{{- end}}
ExecStart={{.ExecStart}}
ExecStop={{if .ExecStop}}-{{.ExecStop}}{{end}}
{{- if .Unit.Restart}}
Restart=on-failure
RestartSec={{.Unit.RestartSec}}
{{- if gt .Unit.RestartRetries 0}}
StartLimitBurst={{.Unit.RestartRetries}}
{{- end}}
{{- end}}
[Install]
WantedBy=default.target
`
type templateData struct {
ServiceName string
Unit config.Unit
ExecStart string
ExecStop string
RuntimeService string
Label string // shown in Description
ServiceType string // simple | oneshot
WorkingDir string
}
// ServiceName returns the systemd service name for a unit, with optional prefix/suffix.
func ServiceName(u config.Unit, prefix, suffix string) string {
return service.BaseName(u, prefix, suffix) + ".service"
}
// Generate produces the .service file content for a unit.
func Generate(u config.Unit, prefix, suffix string) (string, error) {
if err := service.Validate(u); err != nil {
return "", err
}
spec := service.Resolve(u)
data := templateData{
ServiceName: ServiceName(u, prefix, suffix),
Unit: u,
ExecStart: spec.Start,
ExecStop: spec.Stop,
RuntimeService: runtimeService(u.Runtime),
Label: spec.Label,
ServiceType: spec.Type,
WorkingDir: spec.WorkingDir,
}
tmplStr := systemUnitTemplate
if u.User != "" {
tmplStr = userUnitTemplate
}
tmpl, err := template.New("unit").Parse(tmplStr)
if err != nil {
return "", fmt.Errorf("parsing template: %w", err)
}
var buf strings.Builder
if err := tmpl.Execute(&buf, data); err != nil {
return "", fmt.Errorf("rendering template: %w", err)
}
return buf.String(), nil
}
// runtimeService returns the systemd service name that must be running for the
// given container runtime, or "" if none is required (e.g. daemonless podman).
func runtimeService(runtime string) string {
if runtime == "docker" {
return "docker.service"
}
return ""
}