Files
RustProxy/docker-agent/docker.go
T
2026-10-04 11:15:58 +02:00

168 lines
4.7 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"sort"
"strconv"
"strings"
"time"
)
type unixRoundTripper struct{ sock string }
func (u unixRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
transport := &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, "unix", u.sock)
},
DisableKeepAlives: false,
}
clone := req.Clone(req.Context())
clone.URL.Scheme = "http"
clone.URL.Host = "docker"
return transport.RoundTrip(clone)
}
func dockerContainers(ctx context.Context, sock string) ([]dockerContainer, error) {
client := &http.Client{Transport: unixRoundTripper{sock: sock}, Timeout: 4 * time.Second}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://docker/containers/json?all=0", nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("docker API: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return nil, fmt.Errorf("docker API returned %s: %s", resp.Status, strings.TrimSpace(string(body)))
}
var items []dockerContainer
dec := json.NewDecoder(io.LimitReader(resp.Body, 4<<20))
if err := dec.Decode(&items); err != nil {
return nil, fmt.Errorf("decode Docker response: %w", err)
}
return items, nil
}
func cleanName(names []string) string {
if len(names) == 0 {
return "container"
}
name := strings.TrimPrefix(names[0], "/")
if name == "" {
return "container"
}
return name
}
func dnsLabel(value string) string {
value = strings.ToLower(value)
var b strings.Builder
dash := false
for _, r := range value {
ok := (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9')
if ok {
b.WriteRune(r)
dash = false
} else if b.Len() > 0 && !dash {
b.WriteByte('-')
dash = true
}
}
out := strings.Trim(b.String(), "-")
if out == "" {
out = "app"
}
if len(out) > 48 {
out = strings.Trim(out[:48], "-")
}
return out
}
func serviceIdentity(c dockerContainer, p dockerPort) (key, technical string) {
name := cleanName(c.Names)
project := c.Labels["com.docker.compose.project"]
composeService := c.Labels["com.docker.compose.service"]
replica := c.Labels["com.docker.compose.container-number"]
suffix := strconv.Itoa(int(p.PrivatePort))
if project != "" && composeService != "" {
if replica == "" {
replica = name
}
key = fmt.Sprintf("compose:%s/%s/%s:%d/%s", project, composeService, replica, p.PrivatePort, p.Type)
technicalBase := project + "-" + composeService
if replica != "1" {
technicalBase += "-" + replica
}
technical = dnsLabel(technicalBase + "-" + suffix)
} else {
key = fmt.Sprintf("container:%s:%d/%s", name, p.PrivatePort, p.Type)
technical = dnsLabel(name + "-" + suffix)
}
return key, technical
}
func servicesFrom(containers []dockerContainer) []service {
byKey := make(map[string]*service)
bindingSeen := make(map[string]map[string]struct{})
for _, c := range containers {
name := cleanName(c.Names)
for _, p := range c.Ports {
if p.PublicPort == 0 || p.Type != "tcp" {
continue
}
if bind := net.ParseIP(strings.TrimSpace(p.IP)); bind != nil && bind.IsLoopback() {
continue
}
key, technical := serviceIdentity(c, p)
item := byKey[key]
if item == nil {
id := c.ID
if len(id) > 12 {
id = id[:12]
}
item = &service{
Key: key, TechnicalName: technical, ContainerID: id, ContainerName: name,
Image: c.Image, PrivatePort: p.PrivatePort, Protocol: p.Type,
}
byKey[key] = item
bindingSeen[key] = make(map[string]struct{})
}
bindIP := strings.TrimSpace(p.IP)
dedupe := bindIP + "|" + strconv.Itoa(int(p.PublicPort))
if _, ok := bindingSeen[key][dedupe]; ok {
continue
}
bindingSeen[key][dedupe] = struct{}{}
item.Bindings = append(item.Bindings, binding{BindIP: bindIP, PublicPort: p.PublicPort})
}
}
out := make([]service, 0, len(byKey))
for _, item := range byKey {
sort.Slice(item.Bindings, func(i, j int) bool {
left := net.ParseIP(strings.TrimSpace(item.Bindings[i].BindIP))
right := net.ParseIP(strings.TrimSpace(item.Bindings[j].BindIP))
leftWildcard := left == nil || left.IsUnspecified()
rightWildcard := right == nil || right.IsUnspecified()
if leftWildcard != rightWildcard {
return !leftWildcard
}
if item.Bindings[i].BindIP != item.Bindings[j].BindIP {
return item.Bindings[i].BindIP < item.Bindings[j].BindIP
}
return item.Bindings[i].PublicPort < item.Bindings[j].PublicPort
})
out = append(out, *item)
}
sort.Slice(out, func(i, j int) bool { return out[i].Key < out[j].Key })
return out
}