621e8895c9
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
197 lines
5.9 KiB
Go
197 lines
5.9 KiB
Go
package infra
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// DockerContainerInfo holds the essential fields for a Docker container,
|
|
// mapped from the Engine API /containers/json response.
|
|
type DockerContainerInfo struct {
|
|
ID string // short id (12 chars)
|
|
Names []string // e.g. ["/my-container"]
|
|
Image string // image name
|
|
State string // running|exited|paused|...
|
|
Status string // human label, e.g. "Up 2 hours"
|
|
Ports []string // e.g. "0.0.0.0:8080->8080/tcp"
|
|
Networks []string // network names
|
|
Labels map[string]string // container labels
|
|
}
|
|
|
|
// DockerContainerListOpts controls which containers are returned and where
|
|
// the Docker daemon is reached.
|
|
type DockerContainerListOpts struct {
|
|
All bool // if true, include stopped/exited containers (default: false = only running)
|
|
Filters []string // Docker filter expressions, e.g. "label=app=agents_and_robots", "status=running"
|
|
DockerHost string // default "unix:///var/run/docker.sock". Use "tcp://host:port" for remote.
|
|
}
|
|
|
|
// DockerContainerList lists Docker containers on the local (or remote) host
|
|
// by calling the Docker Engine HTTP API directly. No docker CLI required.
|
|
//
|
|
// The DockerHost field selects the transport:
|
|
// - empty or "unix:///var/run/docker.sock" → unix socket
|
|
// - "tcp://host:port" → plain HTTP (no TLS)
|
|
func DockerContainerList(opts DockerContainerListOpts) ([]DockerContainerInfo, error) {
|
|
client, baseURL, err := dockerListHTTPClient(opts.DockerHost)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("docker_container_list: build client: %w", err)
|
|
}
|
|
|
|
// Build query string
|
|
q := url.Values{}
|
|
if opts.All {
|
|
q.Set("all", "1")
|
|
}
|
|
if len(opts.Filters) > 0 {
|
|
filters := buildDockerFilters(opts.Filters)
|
|
b, err := json.Marshal(filters)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("docker_container_list: marshal filters: %w", err)
|
|
}
|
|
q.Set("filters", string(b))
|
|
}
|
|
|
|
endpoint := baseURL + "/containers/json"
|
|
if len(q) > 0 {
|
|
endpoint += "?" + q.Encode()
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("docker_container_list: new request: %w", err)
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("docker_container_list: do request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("docker_container_list: read body: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("docker_container_list: daemon returned %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
return parseDockerContainerList(body)
|
|
}
|
|
|
|
// dockerListHTTPClient returns an *http.Client wired for unix socket or TCP,
|
|
// and a base URL ("http://localhost" for unix, "http://host:port" for TCP).
|
|
func dockerListHTTPClient(dockerHost string) (*http.Client, string, error) {
|
|
if dockerHost == "" {
|
|
dockerHost = "unix:///var/run/docker.sock"
|
|
}
|
|
|
|
if strings.HasPrefix(dockerHost, "unix://") {
|
|
sockPath := strings.TrimPrefix(dockerHost, "unix://")
|
|
transport := &http.Transport{
|
|
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
|
return (&net.Dialer{}).DialContext(ctx, "unix", sockPath)
|
|
},
|
|
}
|
|
return &http.Client{Transport: transport}, "http://localhost", nil
|
|
}
|
|
|
|
if strings.HasPrefix(dockerHost, "tcp://") {
|
|
host := strings.TrimPrefix(dockerHost, "tcp://")
|
|
return &http.Client{}, "http://" + host, nil
|
|
}
|
|
|
|
return nil, "", fmt.Errorf("unsupported docker host scheme: %q (use unix:// or tcp://)", dockerHost)
|
|
}
|
|
|
|
// buildDockerFilters converts []string{"label=k=v", "status=running"} into
|
|
// the map[string][]string format that the Docker Engine API expects.
|
|
func buildDockerFilters(filters []string) map[string][]string {
|
|
m := make(map[string][]string)
|
|
for _, f := range filters {
|
|
idx := strings.IndexByte(f, '=')
|
|
if idx < 0 {
|
|
continue
|
|
}
|
|
key := f[:idx]
|
|
val := f[idx+1:]
|
|
m[key] = append(m[key], val)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// parseDockerContainerList decodes the raw JSON from /containers/json.
|
|
func parseDockerContainerList(body []byte) ([]DockerContainerInfo, error) {
|
|
var raw []struct {
|
|
ID string `json:"Id"`
|
|
Names []string `json:"Names"`
|
|
Image string `json:"Image"`
|
|
State string `json:"State"`
|
|
Status string `json:"Status"`
|
|
Labels map[string]string `json:"Labels"`
|
|
NetworkSettings struct {
|
|
Networks map[string]json.RawMessage `json:"Networks"`
|
|
} `json:"NetworkSettings"`
|
|
Ports []struct {
|
|
IP string `json:"IP"`
|
|
PrivatePort uint16 `json:"PrivatePort"`
|
|
PublicPort uint16 `json:"PublicPort"`
|
|
Type string `json:"Type"`
|
|
} `json:"Ports"`
|
|
}
|
|
|
|
if err := json.Unmarshal(body, &raw); err != nil {
|
|
return nil, fmt.Errorf("docker_container_list: parse response: %w", err)
|
|
}
|
|
|
|
result := make([]DockerContainerInfo, 0, len(raw))
|
|
for _, c := range raw {
|
|
id := c.ID
|
|
if len(id) > 12 {
|
|
id = id[:12]
|
|
}
|
|
|
|
ports := make([]string, 0, len(c.Ports))
|
|
for _, p := range c.Ports {
|
|
if p.IP != "" && p.PublicPort != 0 {
|
|
ports = append(ports, fmt.Sprintf("%s:%d->%d/%s", p.IP, p.PublicPort, p.PrivatePort, p.Type))
|
|
} else if p.PrivatePort != 0 {
|
|
ports = append(ports, fmt.Sprintf("%d/%s", p.PrivatePort, p.Type))
|
|
}
|
|
}
|
|
|
|
networks := make([]string, 0, len(c.NetworkSettings.Networks))
|
|
for name := range c.NetworkSettings.Networks {
|
|
networks = append(networks, name)
|
|
}
|
|
|
|
labels := c.Labels
|
|
if labels == nil {
|
|
labels = map[string]string{}
|
|
}
|
|
|
|
result = append(result, DockerContainerInfo{
|
|
ID: id,
|
|
Names: c.Names,
|
|
Image: c.Image,
|
|
State: c.State,
|
|
Status: c.Status,
|
|
Ports: ports,
|
|
Networks: networks,
|
|
Labels: labels,
|
|
})
|
|
}
|
|
return result, nil
|
|
}
|