model, tunnel, wg, authtoken, netguard, docker and dotenv, split out of the original single repository so Hammerhead, Blacktip and Bamboo can live in their own repos and import them. README extended; LICENSE unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
117 lines
3.0 KiB
Go
117 lines
3.0 KiB
Go
// 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
|
|
}
|