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:
+65
-12
@@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/warkanum/unitdore/sysuser"
|
||||
)
|
||||
|
||||
type Podman struct{}
|
||||
@@ -12,30 +14,56 @@ type Podman struct{}
|
||||
func (p *Podman) Name() string { return "podman" }
|
||||
|
||||
type podmanContainer struct {
|
||||
ID string `json:"Id"`
|
||||
Names []string `json:"Names"`
|
||||
Image string `json:"Image"`
|
||||
Command []string `json:"Command"`
|
||||
State string `json:"State"`
|
||||
ID string `json:"Id"`
|
||||
Names []string `json:"Names"`
|
||||
Image string `json:"Image"`
|
||||
Command []string `json:"Command"`
|
||||
State string `json:"State"`
|
||||
Labels map[string]string `json:"Labels"`
|
||||
}
|
||||
|
||||
func (p *Podman) ListRunning() ([]Container, error) {
|
||||
// podmanCmd builds a podman command for the given user scope.
|
||||
// Returns nil if podman is not installed.
|
||||
func podmanCmd(user string, args ...string) (*exec.Cmd, error) {
|
||||
bin, err := exec.LookPath("podman")
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return sysuser.Command(user, bin, args...)
|
||||
}
|
||||
|
||||
func (p *Podman) ListRunning(user string) ([]Container, error) {
|
||||
cmd, err := podmanCmd(user, "ps", "--format", "json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cmd == nil {
|
||||
return nil, nil // podman not installed — not an error
|
||||
}
|
||||
|
||||
out, err := exec.Command(bin, "ps", "--format", "json").Output()
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
// Podman installed but not usable — treat as no containers
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var raw []podmanContainer
|
||||
if err := json.Unmarshal(out, &raw); err != nil {
|
||||
containers, err := parsePodmanPS(out)
|
||||
if err != nil {
|
||||
// Podman installed but output is not valid JSON (e.g. OCI runtime misconfigured)
|
||||
return nil, nil
|
||||
}
|
||||
for i := range containers {
|
||||
containers[i].User = user
|
||||
}
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
// parsePodmanPS parses `podman ps --format json` output (a JSON array).
|
||||
func parsePodmanPS(out []byte) ([]Container, error) {
|
||||
var raw []podmanContainer
|
||||
if err := json.Unmarshal(out, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var containers []Container
|
||||
for _, c := range raw {
|
||||
@@ -58,18 +86,22 @@ func (p *Podman) ListRunning() ([]Container, error) {
|
||||
Image: c.Image,
|
||||
Command: strings.Join(c.Command, " "),
|
||||
Runtime: "podman",
|
||||
Labels: c.Labels,
|
||||
})
|
||||
}
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func (p *Podman) Exists(name string) (bool, error) {
|
||||
bin, err := exec.LookPath("podman")
|
||||
func (p *Podman) Exists(user, name string) (bool, error) {
|
||||
cmd, err := podmanCmd(user, "ps", "--format", "{{.Names}}")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if cmd == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
out, err := exec.Command(bin, "ps", "--format", "{{.Names}}").Output()
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("podman ps: %w", err)
|
||||
}
|
||||
@@ -81,3 +113,24 @@ func (p *Podman) Exists(name string) (bool, error) {
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// PodExists reports whether a pod with the given name exists (any state).
|
||||
func (p *Podman) PodExists(user, pod string) (bool, error) {
|
||||
cmd, err := podmanCmd(user, "pod", "exists", pod)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if cmd == nil {
|
||||
return false, nil
|
||||
}
|
||||
if err := cmd.Run(); err != nil {
|
||||
if _, ok := err.(*exec.ExitError); ok {
|
||||
return false, nil // exit 1 = pod does not exist
|
||||
}
|
||||
return false, fmt.Errorf("podman pod exists: %w", err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// PodName returns the pod name podman-compose uses for a project.
|
||||
func PodName(project string) string { return "pod_" + project }
|
||||
|
||||
Reference in New Issue
Block a user