- 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
135 lines
2.9 KiB
Go
135 lines
2.9 KiB
Go
package runtime
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"github.com/warkanum/unitdore/sysuser"
|
|
)
|
|
|
|
type Docker struct{}
|
|
|
|
func (d *Docker) Name() string { return "docker" }
|
|
|
|
type dockerContainer struct {
|
|
ID string `json:"ID"`
|
|
Names string `json:"Names"`
|
|
Image string `json:"Image"`
|
|
Command string `json:"Command"`
|
|
State string `json:"State"`
|
|
Labels string `json:"Labels"`
|
|
}
|
|
|
|
// dockerCmd builds a docker command for the given user scope.
|
|
// Returns nil if docker is not installed.
|
|
func dockerCmd(user string, args ...string) (*exec.Cmd, error) {
|
|
bin, err := exec.LookPath("docker")
|
|
if err != nil {
|
|
return nil, nil
|
|
}
|
|
return sysuser.Command(user, bin, args...)
|
|
}
|
|
|
|
func (d *Docker) ListRunning(user string) ([]Container, error) {
|
|
cmd, err := dockerCmd(user, "ps", "--format", "json")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if cmd == nil {
|
|
return nil, nil // docker not installed — not an error
|
|
}
|
|
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
// Docker installed but daemon not running — treat as no containers
|
|
return nil, nil
|
|
}
|
|
|
|
containers := parseDockerPS(out)
|
|
for i := range containers {
|
|
containers[i].User = user
|
|
}
|
|
return containers, nil
|
|
}
|
|
|
|
// parseDockerPS parses `docker ps --format json` output
|
|
// (one JSON object per line, not a JSON array).
|
|
func parseDockerPS(out []byte) []Container {
|
|
var containers []Container
|
|
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
var c dockerContainer
|
|
if err := json.Unmarshal([]byte(line), &c); err != nil {
|
|
continue
|
|
}
|
|
name := strings.TrimPrefix(c.Names, "/")
|
|
// Fall back to short container ID if no name assigned
|
|
if name == "" {
|
|
if len(c.ID) >= 12 {
|
|
name = c.ID[:12]
|
|
} else if c.ID != "" {
|
|
name = c.ID
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
containers = append(containers, Container{
|
|
Name: name,
|
|
Image: c.Image,
|
|
Command: c.Command,
|
|
Runtime: "docker",
|
|
Labels: parseDockerLabels(c.Labels),
|
|
})
|
|
}
|
|
return containers
|
|
}
|
|
|
|
// parseDockerLabels parses docker's "k=v,k=v" label string. A segment without
|
|
// "=" is a continuation of the previous value (values may contain commas,
|
|
// e.g. multiple compose config files).
|
|
func parseDockerLabels(s string) map[string]string {
|
|
labels := map[string]string{}
|
|
last := ""
|
|
for _, part := range strings.Split(s, ",") {
|
|
if part == "" {
|
|
continue
|
|
}
|
|
k, v, ok := strings.Cut(part, "=")
|
|
if !ok {
|
|
if last != "" {
|
|
labels[last] += "," + part
|
|
}
|
|
continue
|
|
}
|
|
labels[k] = v
|
|
last = k
|
|
}
|
|
return labels
|
|
}
|
|
|
|
func (d *Docker) Exists(user, name string) (bool, error) {
|
|
cmd, err := dockerCmd(user, "ps", "--format", "{{.Names}}")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if cmd == nil {
|
|
return false, nil
|
|
}
|
|
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return false, fmt.Errorf("docker ps: %w", err)
|
|
}
|
|
|
|
for _, line := range strings.Split(string(out), "\n") {
|
|
if strings.TrimSpace(line) == name {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|