Files
fn_registry/functions/infra/docker_container_list.go
T
egutierrez 621e8895c9 feat(infra): auto-commit con 86 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:38:15 +02:00

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
}