package cli import ( "bytes" "os" "os/exec" "path/filepath" "strings" "testing" "time" "werkdock/internal/instance" "werkdock/internal/store" ) func TestParseSignal(t *testing.T) { for _, s := range []string{"KILL", "SIGKILL", "kill", "9"} { if sig, err := parseSignal(s); err != nil || int(sig) != 9 { t.Errorf("%q: got %v, %v", s, sig, err) } } if _, err := parseSignal("BOOM"); err == nil { t.Error("BOOM accepted") } } func TestPsRowFormatsLikeDocker(t *testing.T) { inst := &instance.Instance{ Config: instance.Config{ID: strings.Repeat("ab", 32), Name: "pg", Image: "postgres:16-alpine", Created: time.Now().Add(-90 * time.Second)}, State: instance.State{Status: instance.StatusRunning, StartedAt: time.Now().Add(-30 * time.Second)}, } inst.Config.Spec.Command = []string{"docker-entrypoint.sh", "postgres"} row := psRow{inst: inst} if row.ID() != "abababababab" || row.Names() != "pg" || row.RunningFor() != "About a minute ago" || row.Status() != "Up 30 seconds" { t.Errorf("row: %q %q %q %q", row.ID(), row.Names(), row.RunningFor(), row.Status()) } if row.Command() != `"docker-entrypoint.s…"` { t.Errorf("command: %q", row.Command()) } inst.State = instance.State{Status: instance.StatusExited, ExitCode: 3, FinishedAt: time.Now()} if got := row.Status(); got != "Exited (3) 0 seconds ago" { t.Errorf("exited status: %q", got) } var out bytes.Buffer if err := printFormatted(&out, "table", psColumns, []interface{}{row}); err != nil { t.Fatal(err) } if lines := strings.Split(strings.TrimSpace(out.String()), "\n"); len(lines) != 2 || !strings.HasPrefix(lines[0], "CONTAINER ID IMAGE ") || !strings.HasPrefix(lines[1], "abababababab postgres:16-alpine") { t.Errorf("table:\n%s", out.String()) } } // The whole lifecycle through Main, with a real bwrap over / as the // image: run -d, ps, logs, exec, stop, start, rm. func TestLifecycleVerbsInsideRealSandbox(t *testing.T) { if _, err := exec.LookPath("bwrap"); err != nil { t.Skip("bwrap not installed") } if err := exec.Command("bwrap", "--unshare-user", "--uid", "0", "--ro-bind", "/", "/", "true").Run(); err != nil { t.Skipf("unprivileged user namespaces not usable here: %v", err) } home := t.TempDir() t.Setenv("WERKDOCK_HOME", home) // an image whose rootfs is empty but for the mountpoints; the // host's toolchain directories are bound in read-only per run dir := filepath.Join(home, "images", "host", "latest") for _, d := range []string{"rootfs/etc", "rootfs/mnt"} { if err := os.MkdirAll(filepath.Join(dir, d), 0o755); err != nil { t.Fatal(err) } } var hostBinds []string for _, d := range []string{"/bin", "/sbin", "/lib", "/lib64", "/usr"} { if _, err := os.Stat(d); err == nil { hostBinds = append(hostBinds, "-v", d+":"+d+":ro") } } if err := os.WriteFile(filepath.Join(dir, "image.json"), []byte(`{"name":"host:latest","config":{"Volumes":{"/mnt":{}}}}`), 0o644); err != nil { t.Fatal(err) } st := store.Store{Root: home} if _, err := st.RootFS("host"); err != nil { t.Fatal(err) } // the supervisor is this binary: build it once bin := filepath.Join(t.TempDir(), "werkdock") if out, err := exec.Command("go", "build", "-o", bin, "werkdock").CombinedOutput(); err != nil { t.Fatalf("go build: %v\n%s", err, out) } run := func(args ...string) (string, int) { cmd := exec.Command(bin, args...) cmd.Env = append(os.Environ(), "WERKDOCK_HOME="+home) var out bytes.Buffer cmd.Stdout, cmd.Stderr = &out, &out err := cmd.Run() code := 0 if exit, ok := err.(*exec.ExitError); ok { code = exit.ExitCode() } else if err != nil { t.Fatalf("%v: %v", args, err) } return out.String(), code } out, code := run(append(append([]string{"run", "-d", "--name", "svc"}, hostBinds...), "host", "sh", "-c", `trap 'echo bye; exit 0' TERM; echo hello > /mnt/marker; echo up; while :; do sleep 0.1; done`)...) if code != 0 || len(strings.TrimSpace(out)) != 64 { t.Fatalf("run -d: %d %q", code, out) } deadline := time.Now().Add(5 * time.Second) for { if out, _ = run("logs", "svc"); strings.Contains(out, "up") { break } if time.Now().After(deadline) { t.Fatalf("service never printed: %q", out) } time.Sleep(100 * time.Millisecond) } if out, _ = run("ps", "--format", "{{.Names}} {{.State}}"); strings.TrimSpace(out) != "svc running" { t.Errorf("ps: %q", out) } // exec shares the anonymous volume, not the pid namespace if out, code = run("exec", "svc", "cat", "/mnt/marker"); code != 0 || strings.TrimSpace(out) != "hello" { t.Errorf("exec: %d %q", code, out) } if out, code = run("stop", "-t", "5", "svc"); code != 0 || strings.TrimSpace(out) != "svc" { t.Errorf("stop: %d %q", code, out) } if out, _ = run("ps", "-a", "--format", "{{.Names}} {{.State}} {{.Status}}"); !strings.HasPrefix(out, "svc exited Exited (0)") { t.Errorf("ps -a after stop: %q", out) } if out, _ = run("logs", "svc"); !strings.Contains(out, "bye") { t.Errorf("logs after graceful stop: %q", out) } if _, code = run("start", "svc"); code != 0 { t.Errorf("start: %d", code) } if out, code = run("exec", "svc", "cat", "/mnt/marker"); code != 0 || strings.TrimSpace(out) != "hello" { t.Errorf("volume kept across stop/start: %d %q", code, out) } if out, code = run("rm", "svc"); code == 0 || !strings.Contains(out, "is running") { t.Errorf("rm of a running instance: %d %q", code, out) } if out, code = run("rm", "-f", "svc"); code != 0 { t.Errorf("rm -f: %d %q", code, out) } if out, _ = run("ps", "-a", "-q"); strings.TrimSpace(out) != "" { t.Errorf("ps -a after rm: %q", out) } // a foreground --rm run leaves nothing behind either if out, code = run(append(append([]string{"run", "--rm"}, hostBinds...), "host", "echo", "once")...); code != 0 || strings.TrimSpace(out) != "once" { t.Errorf("run --rm: %d %q", code, out) } if entries, _ := os.ReadDir(filepath.Join(home, "instances")); len(entries) != 0 { t.Errorf("instances left: %v", entries) } }