- install/uninstall/start/stop/enable take <name> or --all; *all kept as shortcuts - add enable (enable at boot + start); startall no longer enables - order/delay resolved by config.ResolveOrder and written into systemd, OpenRC and runit service files (After=/sleep), so they apply at boot - install --all stops and disables units before removing their files - update regenerates all installed, enabled units and removes disabled ones - edit creates the config dir; docs and man page updated - tests: fake Init for cmd, ResolveOrder, backend ordering output
150 lines
3.8 KiB
Go
150 lines
3.8 KiB
Go
package systemd
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"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}} {{.}}{{end}}{{range .After}} {{.}}{{end}}{{with .RuntimeService}}
|
|
Requires={{.}}{{end}}
|
|
|
|
[Service]
|
|
Type={{.ServiceType}}
|
|
{{- if eq .ServiceType "oneshot"}}
|
|
RemainAfterExit=yes
|
|
{{- end}}
|
|
{{- with .WorkingDir}}
|
|
WorkingDirectory={{.}}
|
|
{{- end}}
|
|
{{- with .DelaySec}}
|
|
ExecStartPre=/usr/bin/env sleep {{.}}
|
|
TimeoutStartSec={{$.StartTimeout}}
|
|
{{- 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{{range .After}} {{.}}{{end}}
|
|
|
|
[Service]
|
|
Type={{.ServiceType}}
|
|
{{- if eq .ServiceType "oneshot"}}
|
|
RemainAfterExit=yes
|
|
{{- end}}
|
|
{{- with .WorkingDir}}
|
|
WorkingDirectory={{.}}
|
|
{{- end}}
|
|
{{- with .DelaySec}}
|
|
ExecStartPre=/usr/bin/env sleep {{.}}
|
|
TimeoutStartSec={{$.StartTimeout}}
|
|
{{- 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
|
|
After []string // service names of the previous order group
|
|
DelaySec int // ExecStartPre sleep, from unit delay
|
|
StartTimeout int // TimeoutStartSec when a delay is set
|
|
}
|
|
|
|
// 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,
|
|
}
|
|
|
|
for _, n := range u.After {
|
|
data.After = append(data.After, ServiceName(config.Unit{Name: n}, prefix, suffix))
|
|
}
|
|
if u.StartDelay > 0 {
|
|
data.DelaySec = int((u.StartDelay + time.Second - 1) / time.Second)
|
|
data.StartTimeout = data.DelaySec + 90
|
|
}
|
|
|
|
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 ""
|
|
}
|