Files
Reef/docker/docker.go
T
VeillaxandClaude Sonnet 5.5 2259580adf Add the shared packages for the sharks stack
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>
2026-10-26 02:41:44 -06:00

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
}