feat(infra): auto-commit con 86 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user