Files

259 lines
7.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package agent
import (
"fmt"
"maps"
"net"
"regexp"
"slices"
"strconv"
"strings"
"sultan/pkg/docker"
"sultan/pkg/registry"
)
// protocols sind die Label-Ebenen mit Routern und Services (HTTP und TCP
// für TLS-Passthrough). Router- und Service-Namen sind je Ebene getrennt.
var protocols = []string{"http", "tcp"}
func routersPrefix(proto string) string { return "traefik." + proto + ".routers." }
func servicesPrefix(proto string) string { return "traefik." + proto + ".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. Die Regeln
// gelten für traefik.http.* und traefik.tcp.* (TLS-Passthrough) getrennt. 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 // "<proto>:<name>"
services []string // "<proto>:<name>"
}
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, Meta: meta(c)},
running: c.State == "running",
}
for _, proto := range protocols {
for _, r := range labelNames(labels, routersPrefix(proto)) {
cand.routers = append(cand.routers, proto+":"+r)
}
for _, s := range labelNames(labels, servicesPrefix(proto)) {
cand.services = append(cand.services, proto+":"+s)
}
}
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
}
for _, s := range c.services {
_, name, _ := strings.Cut(s, ":")
down = append(down, name)
}
}
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
}
// Labels, die Docker Compose an jeden Container setzt.
const (
labelComposeProject = "com.docker.compose.project"
labelComposeService = "com.docker.compose.service"
labelComposeDir = "com.docker.compose.project.working_dir"
)
// meta liefert die Zusatzdaten eines Containers (Spec 5.3) – nur aus dem,
// was Docker ohnehin kennt, ohne zusätzliche Labels.
func meta(c docker.Container) *registry.WorkloadMeta {
return &registry.WorkloadMeta{
ComposeProject: c.Labels[labelComposeProject],
ComposeService: c.Labels[labelComposeService],
ComposeDir: c.Labels[labelComposeDir],
State: c.State,
StartedAt: c.StartedAt,
Health: c.Health,
}
}
// labelNames liefert die Namen (Schreibweise aus dem Label) unter prefix,
// z. B. die Router aus traefik.http.routers.<name>.…
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)
for _, proto := range protocols {
if warning := translateProtoPorts(c, labels, reachable, proto); warning != "" {
return nil, warning
}
}
return labels, ""
}
// translateProtoPorts rechnet die Service-Ports einer Ebene (http/tcp) in
// labels auf veröffentlichte Host-Ports um.
func translateProtoPorts(c docker.Container, labels map[string]string, reachable map[uint16]uint16, proto string) string {
svcPrefix := servicesPrefix(proto)
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, routersPrefix(proto)) {
hasRouters = true
}
rest, ok := strings.CutPrefix(lk, svcPrefix)
if !ok {
continue
}
svc, attr, ok := strings.Cut(rest, ".")
if !ok {
continue
}
s := services[svc]
if s == nil {
name, _, _ := strings.Cut(k[len(svcPrefix):], ".")
s = &serviceLabels{name: name}
services[svc] = s
}
if attr == "loadbalancer.server.port" {
s.portKey = k
}
}
if len(services) == 0 {
if !hasRouters {
return ""
}
pub, ok := single()
if !ok {
return fmt.Sprintf("Container %s: %d erreichbare Ports – Label loadbalancer.server.port angeben", c.Name(), len(reachable))
}
svc := unsafeName.ReplaceAllString(c.Name(), "-")
labels[svcPrefix+svc+".loadbalancer.server.port"] = strconv.Itoa(int(pub))
return ""
}
for _, s := range services {
if s.portKey == "" {
pub, ok := single()
if !ok {
return fmt.Sprintf("Container %s, Service %s: %d erreichbare Ports – Label loadbalancer.server.port angeben", c.Name(), s.name, len(reachable))
}
labels[svcPrefix+s.name+".loadbalancer.server.port"] = strconv.Itoa(int(pub))
continue
}
private, err := strconv.Atoi(strings.TrimSpace(labels[s.portKey]))
if err != nil {
return fmt.Sprintf("Container %s: Port %q nicht lesbar", c.Name(), labels[s.portKey])
}
pub, ok := reachable[uint16(private)]
if !ok {
return 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 ""
}