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 }