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.
130 lines
3.0 KiB
Go
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 ""
|
|
}
|