package agent import ( "fmt" "maps" "net" "regexp" "slices" "strconv" "strings" "sultan/pkg/docker" "sultan/pkg/registry" ) const servicesPrefix = "traefik.http.services." var unsafeName = regexp.MustCompile(`[^A-Za-z0-9_-]`) // Workloads übersetzt Container in Workloads für service_state. // // Berücksichtigt werden nur Container mit traefik.enable=true. Weil der // Ingress auf einem anderen Host sitzt, wird loadbalancer.server.port (Port // im Container) auf den veröffentlichten Host-Port umgerechnet. Fehlt das // Port-Label, gilt wie bei Traefiks Docker-Provider der einzige // veröffentlichte Port; ohne Service-Labels entsteht ein Service mit dem // Containernamen. Container, deren Ports von außen nicht erreichbar sind // (nicht veröffentlicht, nur Loopback, nur UDP), werden mit Warnung // weggelassen. // // Gestoppte Container (State != "running", Ports aus PortBindings) bleiben // drin; ihre Services stehen in down, damit die Policy zur Wartungsseite // umleitet statt die Route zu entfernen. Überschneiden sich ihre Router- // oder Service-Namen mit einem laufenden Container, entfallen sie. func Workloads(containers []docker.Container, host string) (workloads []registry.Workload, down []string, warnings []string) { type candidate struct { workload registry.Workload running bool routers []string services []string } var candidates []candidate taken := map[string]bool{} // Namen laufender Container, "r:"/"s:" + klein for _, c := range containers { if !enabled(c.Labels) { continue } labels, warning := translatePorts(c) if warning != "" { warnings = append(warnings, warning) continue } cand := candidate{ workload: registry.Workload{Name: c.Name(), Host: host, Labels: labels}, running: c.State == "running", routers: labelNames(labels, "traefik.http.routers."), services: labelNames(labels, servicesPrefix), } if cand.running { for _, r := range cand.routers { taken["r:"+strings.ToLower(r)] = true } for _, s := range cand.services { taken["s:"+strings.ToLower(s)] = true } } candidates = append(candidates, cand) } collides := func(c candidate) bool { for _, r := range c.routers { if taken["r:"+strings.ToLower(r)] { return true } } for _, s := range c.services { if taken["s:"+strings.ToLower(s)] { return true } } return false } for _, c := range candidates { if !c.running { if collides(c) { continue } down = append(down, c.services...) } workloads = append(workloads, c.workload) } slices.SortFunc(workloads, func(a, b registry.Workload) int { return strings.Compare(a.Name, b.Name) }) slices.Sort(down) return workloads, slices.Compact(down), warnings } // labelNames liefert die Namen (Schreibweise aus dem Label) unter prefix, // z. B. die Router aus traefik.http.routers..… func labelNames(labels map[string]string, prefix string) []string { var out []string for k := range labels { if len(k) <= len(prefix) || !strings.EqualFold(k[:len(prefix)], prefix) { continue } if name, _, ok := strings.Cut(k[len(prefix):], "."); ok && !slices.Contains(out, name) { out = append(out, name) } } slices.Sort(out) return out } func enabled(labels map[string]string) bool { for k, v := range labels { if strings.EqualFold(k, "traefik.enable") { on, err := strconv.ParseBool(strings.TrimSpace(v)) return err == nil && on } } return false } // reachablePorts: Container-Port → von außen erreichbarer Host-Port (TCP). func reachablePorts(ports []docker.Port) map[uint16]uint16 { out := map[uint16]uint16{} for _, p := range ports { if p.Type != "tcp" || p.PublicPort == 0 { continue } if ip := net.ParseIP(p.IP); ip != nil && ip.IsLoopback() { continue } out[p.PrivatePort] = p.PublicPort } return out } type serviceLabels struct { name string // Schreibweise aus dem Label portKey string // Original-Schlüssel des Port-Labels, "" wenn keins } func translatePorts(c docker.Container) (map[string]string, string) { labels := maps.Clone(c.Labels) reachable := reachablePorts(c.Ports) single := func() (uint16, bool) { for _, pub := range reachable { return pub, len(reachable) == 1 } return 0, false } services := map[string]*serviceLabels{} hasRouters := false for k := range labels { lk := strings.ToLower(k) if strings.HasPrefix(lk, "traefik.http.routers.") { hasRouters = true } rest, ok := strings.CutPrefix(lk, servicesPrefix) if !ok { continue } svc, attr, ok := strings.Cut(rest, ".") if !ok { continue } s := services[svc] if s == nil { name, _, _ := strings.Cut(k[len(servicesPrefix):], ".") s = &serviceLabels{name: name} services[svc] = s } if attr == "loadbalancer.server.port" { s.portKey = k } } if len(services) == 0 { if !hasRouters { return labels, "" } pub, ok := single() if !ok { return nil, fmt.Sprintf("Container %s: %d erreichbare Ports – Label loadbalancer.server.port angeben", c.Name(), len(reachable)) } svc := unsafeName.ReplaceAllString(c.Name(), "-") labels[servicesPrefix+svc+".loadbalancer.server.port"] = strconv.Itoa(int(pub)) return labels, "" } for _, s := range services { if s.portKey == "" { pub, ok := single() if !ok { return nil, fmt.Sprintf("Container %s, Service %s: %d erreichbare Ports – Label loadbalancer.server.port angeben", c.Name(), s.name, len(reachable)) } labels[servicesPrefix+s.name+".loadbalancer.server.port"] = strconv.Itoa(int(pub)) continue } private, err := strconv.Atoi(strings.TrimSpace(labels[s.portKey])) if err != nil { return nil, fmt.Sprintf("Container %s: Port %q nicht lesbar", c.Name(), labels[s.portKey]) } pub, ok := reachable[uint16(private)] if !ok { return nil, fmt.Sprintf("Container %s: Port %d ist von außen nicht erreichbar (nicht veröffentlicht oder nur 127.0.0.1)", c.Name(), private) } labels[s.portKey] = strconv.Itoa(int(pub)) } return labels, "" }