// Package docker lists running containers through the Docker Engine API so agents // can offer them as targets. It speaks plain HTTP over the engine's socket, with // no dependency on the Docker SDK. package docker import ( "context" "encoding/json" "fmt" "net" "net/http" "net/url" "os" "runtime" "sort" "strings" "time" "git.veillax.com/Veillax/reef/model" ) func host() string { if h := os.Getenv("DOCKER_HOST"); h != "" { return h } if runtime.GOOS == "windows" { return "npipe:////./pipe/docker_engine" } return "unix:///var/run/docker.sock" } func dialer(h string) (func(ctx context.Context, network, addr string) (net.Conn, error), error) { u, err := url.Parse(h) if err != nil { return nil, err } switch u.Scheme { case "unix": path := u.Path return func(ctx context.Context, _, _ string) (net.Conn, error) { var d net.Dialer return d.DialContext(ctx, "unix", path) }, nil case "tcp", "http": addr := u.Host return func(ctx context.Context, _, _ string) (net.Conn, error) { var d net.Dialer return d.DialContext(ctx, "tcp", addr) }, nil case "npipe": return dialPipe(strings.ReplaceAll(u.Path, "/", `\`)), nil } return nil, fmt.Errorf("unsupported DOCKER_HOST %q", h) } // List returns the running containers, or an error if Docker isn't reachable. func List(ctx context.Context) ([]model.Container, error) { dial, err := dialer(host()) if err != nil { return nil, err } c := &http.Client{Timeout: 5 * time.Second, Transport: &http.Transport{DialContext: dial, DisableKeepAlives: true}} req, _ := http.NewRequestWithContext(ctx, "GET", "http://docker/containers/json", nil) resp, err := c.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != 200 { return nil, fmt.Errorf("docker returned %d", resp.StatusCode) } var raw []struct { ID string `json:"Id"` Names []string `json:"Names"` Image string State string Ports []struct { PrivatePort int PublicPort int Type string } NetworkSettings struct { Networks map[string]struct{ IPAddress string } } } if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil { return nil, err } out := []model.Container{} for _, r := range raw { c := model.Container{ID: r.ID, Image: r.Image, State: r.State, Ports: []model.ContainerPort{}, Networks: map[string]string{}} if len(c.ID) > 12 { c.ID = c.ID[:12] } if len(r.Names) > 0 { c.Name = strings.TrimPrefix(r.Names[0], "/") } seen := map[string]bool{} for _, p := range r.Ports { key := fmt.Sprintf("%d/%d/%s", p.PrivatePort, p.PublicPort, p.Type) if seen[key] { continue // docker lists IPv4 and IPv6 bindings separately } seen[key] = true c.Ports = append(c.Ports, model.ContainerPort{Private: p.PrivatePort, Public: p.PublicPort, Type: p.Type}) } for n, v := range r.NetworkSettings.Networks { if v.IPAddress != "" { c.Networks[n] = v.IPAddress } } out = append(out, c) } sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) return out, nil }