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.
216 lines
5.6 KiB
Go
216 lines
5.6 KiB
Go
package service
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/warkanum/unitdore/config"
|
|
)
|
|
|
|
func TestBuildExecCommands(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
unit config.Unit
|
|
wantStart string
|
|
wantStop string
|
|
}{
|
|
{
|
|
name: "podman",
|
|
unit: config.Unit{Name: "app", Runtime: "podman"},
|
|
wantStart: "/usr/bin/podman start -a app",
|
|
wantStop: "/usr/bin/podman stop app",
|
|
},
|
|
{
|
|
name: "docker",
|
|
unit: config.Unit{Name: "app", Runtime: "docker"},
|
|
wantStart: "/usr/bin/docker start -a app",
|
|
wantStop: "/usr/bin/docker stop app",
|
|
},
|
|
{
|
|
name: "custom command",
|
|
unit: config.Unit{Name: "app", Runtime: "podman", Command: "/bin/custom"},
|
|
wantStart: "/bin/custom",
|
|
wantStop: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
start, stop := buildExecCommands(tt.unit)
|
|
if start != tt.wantStart {
|
|
t.Errorf("start = %q, want %q", start, tt.wantStart)
|
|
}
|
|
if stop != tt.wantStop {
|
|
t.Errorf("stop = %q, want %q", stop, tt.wantStop)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStackMode(t *testing.T) {
|
|
u := newComposeStack(t)
|
|
if StackMode(u) != StackModeCompose {
|
|
t.Errorf("StackMode() = %q, want compose", StackMode(u))
|
|
}
|
|
|
|
missingFile := u
|
|
missingFile.ComposeFiles = []string{"gone.yaml"}
|
|
if StackMode(missingFile) != StackModePod {
|
|
t.Error("missing compose file should fall back to pod mode")
|
|
}
|
|
|
|
missingBin := u
|
|
missingBin.ComposeBin = ""
|
|
if StackMode(missingBin) != StackModePod {
|
|
t.Error("missing compose binary should fall back to pod mode")
|
|
}
|
|
|
|
missingDir := u
|
|
missingDir.WorkingDir = "/nonexistent/unitdore-test"
|
|
if StackMode(missingDir) != StackModePod {
|
|
t.Error("missing working dir should fall back to pod mode")
|
|
}
|
|
}
|
|
|
|
func TestBuildComposeCommands_Quoting(t *testing.T) {
|
|
u := config.Unit{
|
|
Project: "shop",
|
|
ComposeBin: "/usr/bin/podman-compose",
|
|
ComposeFiles: []string{"compose.yaml", "my override.yaml"},
|
|
}
|
|
start, stop := buildComposeCommands(u)
|
|
wantBase := `/usr/bin/podman-compose -p shop -f compose.yaml -f "my override.yaml"`
|
|
if start != wantBase+" up" {
|
|
t.Errorf("start = %q", start)
|
|
}
|
|
if stop != wantBase+" stop" {
|
|
t.Errorf("stop = %q", stop)
|
|
}
|
|
}
|
|
|
|
// newComposeStack creates a stack unit whose compose binary, working dir and
|
|
// compose file exist on disk (compose mode).
|
|
func newComposeStack(t *testing.T) config.Unit {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
bin := filepath.Join(dir, "podman-compose")
|
|
if err := os.WriteFile(bin, []byte("#!/bin/sh\n"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "compose.yaml"), []byte("services: {}\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return config.Unit{
|
|
Name: "shop",
|
|
Runtime: "podman",
|
|
Kind: config.KindStack,
|
|
Project: "shop",
|
|
WorkingDir: dir,
|
|
ComposeFiles: []string{"compose.yaml"},
|
|
ComposeBin: bin,
|
|
Enabled: true,
|
|
}
|
|
}
|
|
|
|
func TestSplitCommand(t *testing.T) {
|
|
tests := []struct {
|
|
in string
|
|
want []string
|
|
wantErr bool
|
|
}{
|
|
{in: "/usr/bin/podman start -a app", want: []string{"/usr/bin/podman", "start", "-a", "app"}},
|
|
{in: `/bin/x "my file.txt" 'a b'`, want: []string{"/bin/x", "my file.txt", "a b"}},
|
|
{in: `/bin/x "say \"hi\""`, want: []string{"/bin/x", `say "hi"`}},
|
|
{in: `/bin/x ""`, want: []string{"/bin/x", ""}},
|
|
{in: " ", want: nil},
|
|
{in: `/bin/x "open`, wantErr: true},
|
|
{in: `/bin/x \`, wantErr: true},
|
|
}
|
|
for _, tt := range tests {
|
|
got, err := SplitCommand(tt.in)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("SplitCommand(%q) err = %v, wantErr %v", tt.in, err, tt.wantErr)
|
|
continue
|
|
}
|
|
if len(got) != len(tt.want) {
|
|
t.Errorf("SplitCommand(%q) = %q, want %q", tt.in, got, tt.want)
|
|
continue
|
|
}
|
|
for i := range got {
|
|
if got[i] != tt.want[i] {
|
|
t.Errorf("SplitCommand(%q)[%d] = %q, want %q", tt.in, i, got[i], tt.want[i])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestQuoteArg_SplitCommand_RoundTrip(t *testing.T) {
|
|
for _, arg := range []string{"plain", "with space", `a"b`, `back\slash`, "it's"} {
|
|
got, err := SplitCommand("x " + QuoteArg(arg))
|
|
if err != nil || len(got) != 2 || got[1] != arg {
|
|
t.Errorf("round trip %q = %q, %v", arg, got, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestShellQuote(t *testing.T) {
|
|
tests := map[string]string{
|
|
"plain-1.0/x": "plain-1.0/x",
|
|
"": "''",
|
|
"a b": "'a b'",
|
|
"it's": `'it'\''s'`,
|
|
"$HOME": "'$HOME'",
|
|
}
|
|
for in, want := range tests {
|
|
if got := ShellQuote(in); got != want {
|
|
t.Errorf("ShellQuote(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestShellCommand(t *testing.T) {
|
|
got, err := ShellCommand(`/opt/agent --config "/etc/my agent.toml"`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if want := `/opt/agent --config '/etc/my agent.toml'`; got != want {
|
|
t.Errorf("ShellCommand() = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestValidate(t *testing.T) {
|
|
bad := []config.Unit{
|
|
{Name: "a", Runtime: config.RuntimeScript},
|
|
{Name: "b", Runtime: config.RuntimeExec},
|
|
}
|
|
for _, u := range bad {
|
|
if Validate(u) == nil {
|
|
t.Errorf("Validate(%s) should fail", u.Name)
|
|
}
|
|
}
|
|
ok := []config.Unit{
|
|
{Name: "a", Runtime: config.RuntimeScript, Command: "/x"},
|
|
{Name: "b", Runtime: "podman"},
|
|
}
|
|
for _, u := range ok {
|
|
if err := Validate(u); err != nil {
|
|
t.Errorf("Validate(%s) = %v", u.Name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBaseName(t *testing.T) {
|
|
if got := BaseName(config.Unit{Name: "x"}, "p-", "-s"); got != "unitdore-p-x-s" {
|
|
t.Errorf("BaseName() = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestResolve_PodFallbackIsOneshot(t *testing.T) {
|
|
u := config.Unit{Name: "shop", Runtime: "podman", Kind: config.KindStack, Project: "shop"}
|
|
spec := Resolve(u)
|
|
if spec.Type != TypeOneshot || spec.Start != "/usr/bin/podman pod start pod_shop" {
|
|
t.Errorf("Resolve() = %+v", spec)
|
|
}
|
|
}
|