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 }