v0.25.1
This commit is contained in:
1 parent
87e571c170
commit
fa7abc9e53
308 files changed
+49746
-50237
No files matched your search
@@ -0,0 +1,167 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user