feat: podman-compose stack units and root/user execution scopes
- syncup: podman first, docker only as fallback (podman wins on name clash;
existing docker units found in podman switch to podman)
- syncup: group podman-compose containers into one `kind: stack` unit per
project (project, working_dir, compose_files, compose_bin from labels);
disable existing member units ("part of stack <project>")
- generator: compose mode (podman-compose up/stop, WorkingDirectory) with
pod start/stop fallback when compose binary/dir/files are missing
- reconcile: stacks disabled with "stack not found" when neither compose
files nor pod exist
- root: scan root + rootless users listed in `users:` (podman only)
- non-root: config ~/.config/unitdore/units.yaml, own user units only
- sysuser package: runuser with XDG_RUNTIME_DIR/DBUS env, home/linger lookup
- install: warn when linger is not enabled for user units
- testdata/stack: podman-compose test stack and captured podman ps fixture
- docs: README, generated-units, man page, AGENTS, PLAN
This commit is contained in:
+80
-2
@@ -1,6 +1,8 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -53,7 +55,7 @@ func TestAvailable_ReturnsRuntimes(t *testing.T) {
|
||||
func TestPodman_ListRunning_NoBinary(t *testing.T) {
|
||||
// This test is meaningful if podman isn't installed; if it is, we just check no error occurs
|
||||
p := &Podman{}
|
||||
containers, err := p.ListRunning()
|
||||
containers, err := p.ListRunning("")
|
||||
if err != nil {
|
||||
t.Errorf("ListRunning() should not error when podman is absent: %v", err)
|
||||
}
|
||||
@@ -63,9 +65,85 @@ func TestPodman_ListRunning_NoBinary(t *testing.T) {
|
||||
|
||||
func TestDocker_ListRunning_NoBinary(t *testing.T) {
|
||||
d := &Docker{}
|
||||
containers, err := d.ListRunning()
|
||||
containers, err := d.ListRunning("")
|
||||
if err != nil {
|
||||
t.Errorf("ListRunning() should not error when docker is absent: %v", err)
|
||||
}
|
||||
_ = containers
|
||||
}
|
||||
|
||||
func TestAvailable_PodmanFirst(t *testing.T) {
|
||||
runtimes := Available()
|
||||
if len(runtimes) < 2 || runtimes[0].Name() != "podman" || runtimes[1].Name() != "docker" {
|
||||
t.Errorf("Available() order must be podman, docker; got %v", runtimes)
|
||||
}
|
||||
}
|
||||
|
||||
// Fixture captured from `podman-compose up -d` of testdata/stack/compose.yaml.
|
||||
func TestParsePodmanPS_ComposeStack(t *testing.T) {
|
||||
data, err := os.ReadFile("../testdata/stack/podman_ps.json")
|
||||
if err != nil {
|
||||
t.Fatalf("reading fixture: %v", err)
|
||||
}
|
||||
containers, err := parsePodmanPS(data)
|
||||
if err != nil {
|
||||
t.Fatalf("parsePodmanPS() error: %v", err)
|
||||
}
|
||||
if len(containers) != 2 {
|
||||
t.Fatalf("expected 2 containers, got %d", len(containers))
|
||||
}
|
||||
for _, c := range containers {
|
||||
if c.Project() != "unitdore-test" {
|
||||
t.Errorf("%s: Project() = %q, want unitdore-test", c.Name, c.Project())
|
||||
}
|
||||
if c.WorkingDir() == "" {
|
||||
t.Errorf("%s: WorkingDir() empty", c.Name)
|
||||
}
|
||||
if !reflect.DeepEqual(c.ComposeFiles(), []string{"compose.yaml"}) {
|
||||
t.Errorf("%s: ComposeFiles() = %v", c.Name, c.ComposeFiles())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainer_NoProject(t *testing.T) {
|
||||
c := Container{Name: "plain"}
|
||||
if c.Project() != "" || c.WorkingDir() != "" || c.ComposeFiles() != nil {
|
||||
t.Errorf("container without labels should have no stack info: %+v", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainer_ComposeFilesMultiple(t *testing.T) {
|
||||
c := Container{Labels: map[string]string{LabelComposeConfigFiles: "compose.yaml, ./override.yaml"}}
|
||||
want := []string{"compose.yaml", "override.yaml"}
|
||||
if got := c.ComposeFiles(); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("ComposeFiles() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDockerPS_Labels(t *testing.T) {
|
||||
out := []byte(`{"ID":"abc","Names":"web","Image":"nginx","Command":"nginx","State":"running","Labels":"com.docker.compose.project=shop,com.docker.compose.project.config_files=/s/a.yml,/s/b.yml,com.docker.compose.service=web"}`)
|
||||
cs := parseDockerPS(out)
|
||||
if len(cs) != 1 {
|
||||
t.Fatalf("expected 1 container, got %d", len(cs))
|
||||
}
|
||||
l := cs[0].Labels
|
||||
if l["com.docker.compose.project"] != "shop" {
|
||||
t.Errorf("project label = %q", l["com.docker.compose.project"])
|
||||
}
|
||||
if l["com.docker.compose.project.config_files"] != "/s/a.yml,/s/b.yml" {
|
||||
t.Errorf("config_files label = %q", l["com.docker.compose.project.config_files"])
|
||||
}
|
||||
if l["com.docker.compose.service"] != "web" {
|
||||
t.Errorf("service label = %q", l["com.docker.compose.service"])
|
||||
}
|
||||
// Docker compose stacks are not grouped yet (podman-compose only)
|
||||
if cs[0].Project() != "" {
|
||||
t.Errorf("docker compose container should not report a podman-compose project")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPodName(t *testing.T) {
|
||||
if PodName("shop") != "pod_shop" {
|
||||
t.Errorf("PodName() = %q", PodName("shop"))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user