feat: script and exec runtimes with configurable shell

Add runtime: script (shell file, default /bin/bash, optional stop_script) and runtime: exec (executable with args, optional stop_executable). Both support service_type simple|oneshot and working_dir.
This commit is contained in:
2026-10-12 19:45:54 +02:00
parent 173b4b2523
commit 95143dd998
5 changed files with 251 additions and 1 deletions
+127
View File
@@ -391,3 +391,130 @@ func TestBuildComposeCommands_Quoting(t *testing.T) {
t.Errorf("stop = %q", stop)
}
}
func TestGenerate_Script(t *testing.T) {
u := config.Unit{
Name: "backup",
Runtime: config.RuntimeScript,
Script: "/opt/scripts/backup.sh",
StopScript: "/opt/scripts/backup-stop.sh",
WorkingDir: "/opt/scripts",
Enabled: true,
}
content, err := Generate(u, "", "")
if err != nil {
t.Fatalf("Generate() error: %v", err)
}
checks := []string{
"Description=Unitdore: backup (bash script)",
"Type=simple",
"WorkingDirectory=/opt/scripts",
"ExecStart=/bin/bash /opt/scripts/backup.sh",
"ExecStop=-/bin/bash /opt/scripts/backup-stop.sh",
}
for _, check := range checks {
if !strings.Contains(content, check) {
t.Errorf("Generate() script missing %q in output:\n%s", check, content)
}
}
if strings.Contains(content, "Requires=") {
t.Errorf("Generate() script unit should not contain Requires=:\n%s", content)
}
}
func TestGenerate_ScriptOneshotUserQuoted(t *testing.T) {
u := config.Unit{
Name: "setup",
Runtime: config.RuntimeScript,
User: "alice",
Script: "/home/alice/my scripts/setup.sh",
ServiceType: "oneshot",
Enabled: true,
}
content, err := Generate(u, "", "")
if err != nil {
t.Fatalf("Generate() error: %v", err)
}
checks := []string{
"Type=oneshot",
"RemainAfterExit=yes",
`ExecStart=/bin/bash "/home/alice/my scripts/setup.sh"`,
"WantedBy=default.target",
}
for _, check := range checks {
if !strings.Contains(content, check) {
t.Errorf("Generate() missing %q in output:\n%s", check, content)
}
}
if strings.Contains(content, "ExecStop=-") {
t.Errorf("Generate() should have no stop command without stop_script:\n%s", content)
}
}
func TestGenerate_ScriptMissingPath(t *testing.T) {
u := config.Unit{Name: "bad", Runtime: config.RuntimeScript, Enabled: true}
if _, err := Generate(u, "", ""); err == nil {
t.Error("Generate() should error when script path is empty")
}
}
func TestGenerate_ScriptCustomShell(t *testing.T) {
u := config.Unit{
Name: "fishy",
Runtime: config.RuntimeScript,
Shell: "/usr/bin/zsh",
Script: "/opt/run.zsh",
Enabled: true,
}
content, err := Generate(u, "", "")
if err != nil {
t.Fatalf("Generate() error: %v", err)
}
for _, check := range []string{"Description=Unitdore: fishy (zsh script)", "ExecStart=/usr/bin/zsh /opt/run.zsh"} {
if !strings.Contains(content, check) {
t.Errorf("Generate() missing %q in output:\n%s", check, content)
}
}
}
func TestGenerate_Executable(t *testing.T) {
u := config.Unit{
Name: "agent",
Runtime: config.RuntimeExec,
Executable: "/opt/agent/bin/agent",
Args: []string{"--config", "/etc/my agent.toml"},
StopExecutable: "/opt/agent/bin/agent-stop",
WorkingDir: "/opt/agent",
ServiceType: "oneshot",
Enabled: true,
}
content, err := Generate(u, "", "")
if err != nil {
t.Fatalf("Generate() error: %v", err)
}
checks := []string{
"Description=Unitdore: agent (executable)",
"Type=oneshot",
"WorkingDirectory=/opt/agent",
`ExecStart=/opt/agent/bin/agent --config "/etc/my agent.toml"`,
"ExecStop=-/opt/agent/bin/agent-stop",
}
for _, check := range checks {
if !strings.Contains(content, check) {
t.Errorf("Generate() missing %q in output:\n%s", check, content)
}
}
}
func TestGenerate_ExecutableMissingPath(t *testing.T) {
u := config.Unit{Name: "bad", Runtime: config.RuntimeExec, Enabled: true}
if _, err := Generate(u, "", ""); err == nil {
t.Error("Generate() should error when executable is empty")
}
}