erste module drin.
zwischenstand: Architektur aktualisiert, TOFU implementiert,
This commit is contained in:
1 parent
9d5789c5d7
commit
c8df9d7651
41 files changed
+6455
-36
No files matched your search
@@ -0,0 +1,303 @@
|
||||
package traefik
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrUnsupportedLabel meldet ein Traefik-Label, das Sultan (noch) nicht übersetzt.
|
||||
ErrUnsupportedLabel = errors.New("nicht unterstütztes Label")
|
||||
// ErrInvalidLabel meldet ein Label mit ungültigem Wert.
|
||||
ErrInvalidLabel = errors.New("ungültiges Label")
|
||||
// ErrIncomplete meldet eine Konfiguration, der Pflichtangaben fehlen.
|
||||
ErrIncomplete = errors.New("unvollständige Konfiguration")
|
||||
)
|
||||
|
||||
const labelPrefix = "traefik."
|
||||
|
||||
// FromLabels übersetzt die flachen Traefik-Labels eines Containers (bzw.
|
||||
// einer /etc/sultan.d-Datei) in eine verschachtelte Konfiguration.
|
||||
//
|
||||
// host ist die Adresse, unter der der Ingress das Backend erreicht (LAN- oder
|
||||
// NetBird-IP); sie wird zusammen mit loadbalancer.server.port in die
|
||||
// Server-URLs eingesetzt.
|
||||
//
|
||||
// Wie bei Traefik mit exposedByDefault=false wird nur übersetzt, wenn
|
||||
// traefik.enable=true gesetzt ist; sonst ist das Ergebnis leer. Label-Schlüssel
|
||||
// werden ohne Beachtung der Groß-/Kleinschreibung erkannt, Router-, Service-
|
||||
// und Middleware-Namen behalten ihre Schreibweise. traefik.docker.* wird
|
||||
// ignoriert. Alle gefundenen Fehler werden gesammelt per errors.Join
|
||||
// zurückgegeben.
|
||||
func FromLabels(host string, labels map[string]string) (*Configuration, error) {
|
||||
enabled, err := isEnabled(labels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !enabled {
|
||||
return &Configuration{}, nil
|
||||
}
|
||||
|
||||
b := builder{
|
||||
routers: map[string]*Router{},
|
||||
services: map[string]*serviceSpec{},
|
||||
middlewares: map[string]Middleware{},
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(labels))
|
||||
for k := range labels {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, key := range keys {
|
||||
if !strings.HasPrefix(strings.ToLower(key), labelPrefix) {
|
||||
continue
|
||||
}
|
||||
if err := b.apply(key, labels[key]); err != nil {
|
||||
b.errs = append(b.errs, fmt.Errorf("%s: %w", key, err))
|
||||
}
|
||||
}
|
||||
|
||||
cfg := b.build(host)
|
||||
if len(b.errs) > 0 {
|
||||
return nil, errors.Join(b.errs...)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func isEnabled(labels map[string]string) (bool, error) {
|
||||
for k, v := range labels {
|
||||
if strings.EqualFold(k, labelPrefix+"enable") {
|
||||
enabled, err := strconv.ParseBool(strings.TrimSpace(v))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("%s: %w: %q ist kein Bool", k, ErrInvalidLabel, v)
|
||||
}
|
||||
return enabled, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
type serviceSpec struct {
|
||||
port string
|
||||
scheme string
|
||||
passHostHeader *bool
|
||||
}
|
||||
|
||||
type builder struct {
|
||||
routers map[string]*Router
|
||||
services map[string]*serviceSpec
|
||||
middlewares map[string]Middleware
|
||||
errs []error
|
||||
}
|
||||
|
||||
func (b *builder) apply(key, value string) error {
|
||||
parts := strings.Split(key[len(labelPrefix):], ".")
|
||||
switch strings.ToLower(parts[0]) {
|
||||
case "enable", "docker":
|
||||
return nil
|
||||
case "http":
|
||||
default:
|
||||
return ErrUnsupportedLabel
|
||||
}
|
||||
if len(parts) < 4 {
|
||||
return ErrUnsupportedLabel
|
||||
}
|
||||
kind, name, attr := strings.ToLower(parts[1]), parts[2], parts[3:]
|
||||
if name == "" {
|
||||
return fmt.Errorf("%w: leerer Name", ErrInvalidLabel)
|
||||
}
|
||||
switch kind {
|
||||
case "routers":
|
||||
r, ok := b.routers[name]
|
||||
if !ok {
|
||||
r = &Router{}
|
||||
b.routers[name] = r
|
||||
}
|
||||
return setRouterAttr(r, strings.ToLower(strings.Join(attr, ".")), value)
|
||||
case "services":
|
||||
s, ok := b.services[name]
|
||||
if !ok {
|
||||
s = &serviceSpec{}
|
||||
b.services[name] = s
|
||||
}
|
||||
return setServiceAttr(s, strings.ToLower(strings.Join(attr, ".")), value)
|
||||
case "middlewares":
|
||||
m, ok := b.middlewares[name]
|
||||
if !ok {
|
||||
m = Middleware{}
|
||||
b.middlewares[name] = m
|
||||
}
|
||||
return setMiddlewareAttr(m, attr, value)
|
||||
default:
|
||||
return ErrUnsupportedLabel
|
||||
}
|
||||
}
|
||||
|
||||
func setRouterAttr(r *Router, attr, value string) error {
|
||||
switch attr {
|
||||
case "rule":
|
||||
r.Rule = strings.TrimSpace(value)
|
||||
case "entrypoints":
|
||||
r.EntryPoints = splitList(value)
|
||||
case "middlewares":
|
||||
r.Middlewares = splitList(value)
|
||||
case "service":
|
||||
r.Service = strings.TrimSpace(value)
|
||||
case "priority":
|
||||
p, err := strconv.Atoi(strings.TrimSpace(value))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %q ist keine Zahl", ErrInvalidLabel, value)
|
||||
}
|
||||
r.Priority = p
|
||||
case "tls":
|
||||
on, err := strconv.ParseBool(strings.TrimSpace(value))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %q ist kein Bool", ErrInvalidLabel, value)
|
||||
}
|
||||
if on && r.TLS == nil {
|
||||
r.TLS = &RouterTLS{}
|
||||
}
|
||||
case "tls.certresolver":
|
||||
ensureTLS(r).CertResolver = strings.TrimSpace(value)
|
||||
case "tls.options":
|
||||
ensureTLS(r).Options = strings.TrimSpace(value)
|
||||
default:
|
||||
return ErrUnsupportedLabel
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureTLS(r *Router) *RouterTLS {
|
||||
if r.TLS == nil {
|
||||
r.TLS = &RouterTLS{}
|
||||
}
|
||||
return r.TLS
|
||||
}
|
||||
|
||||
func setServiceAttr(s *serviceSpec, attr, value string) error {
|
||||
value = strings.TrimSpace(value)
|
||||
switch attr {
|
||||
case "loadbalancer.server.port":
|
||||
p, err := strconv.Atoi(value)
|
||||
if err != nil || p < 1 || p > 65535 {
|
||||
return fmt.Errorf("%w: %q ist kein gültiger Port", ErrInvalidLabel, value)
|
||||
}
|
||||
s.port = strconv.Itoa(p)
|
||||
case "loadbalancer.server.scheme":
|
||||
scheme := strings.ToLower(value)
|
||||
switch scheme {
|
||||
case "http", "https", "h2c":
|
||||
s.scheme = scheme
|
||||
default:
|
||||
return fmt.Errorf("%w: Schema %q nicht erlaubt (http, https, h2c)", ErrInvalidLabel, value)
|
||||
}
|
||||
case "loadbalancer.passhostheader":
|
||||
v, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %q ist kein Bool", ErrInvalidLabel, value)
|
||||
}
|
||||
s.passHostHeader = &v
|
||||
default:
|
||||
return ErrUnsupportedLabel
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setMiddlewareAttr legt value unter dem Pfad attr in m ab. attr[0] ist der
|
||||
// Middleware-Typ (z. B. redirectregex), der Rest die Optionen.
|
||||
func setMiddlewareAttr(m Middleware, attr []string, value string) error {
|
||||
node := map[string]any(m)
|
||||
for i, p := range attr {
|
||||
if p == "" {
|
||||
return fmt.Errorf("%w: leeres Pfadsegment", ErrInvalidLabel)
|
||||
}
|
||||
if i == len(attr)-1 {
|
||||
if _, isMap := node[p].(map[string]any); isMap {
|
||||
return fmt.Errorf("%w: %q ist bereits ein Objekt", ErrInvalidLabel, p)
|
||||
}
|
||||
node[p] = value
|
||||
return nil
|
||||
}
|
||||
switch child := node[p].(type) {
|
||||
case nil:
|
||||
next := map[string]any{}
|
||||
node[p] = next
|
||||
node = next
|
||||
case map[string]any:
|
||||
node = child
|
||||
default:
|
||||
return fmt.Errorf("%w: %q ist bereits ein Wert", ErrInvalidLabel, p)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *builder) build(host string) *Configuration {
|
||||
if len(b.routers) == 0 && len(b.services) == 0 && len(b.middlewares) == 0 {
|
||||
return &Configuration{}
|
||||
}
|
||||
http := &HTTPConfiguration{}
|
||||
|
||||
if len(b.services) > 0 {
|
||||
if strings.TrimSpace(host) == "" {
|
||||
b.errs = append(b.errs, fmt.Errorf("%w: keine Backend-Adresse (host) angegeben", ErrIncomplete))
|
||||
}
|
||||
http.Services = map[string]*Service{}
|
||||
for name, s := range b.services {
|
||||
if s.port == "" {
|
||||
b.errs = append(b.errs, fmt.Errorf("service %q: %w: loadbalancer.server.port fehlt", name, ErrIncomplete))
|
||||
continue
|
||||
}
|
||||
scheme := s.scheme
|
||||
if scheme == "" {
|
||||
scheme = "http"
|
||||
}
|
||||
http.Services[name] = &Service{LoadBalancer: &ServersLoadBalancer{
|
||||
Servers: []Server{{URL: scheme + "://" + net.JoinHostPort(host, s.port)}},
|
||||
PassHostHeader: s.passHostHeader,
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.routers) > 0 {
|
||||
http.Routers = b.routers
|
||||
for name, r := range b.routers {
|
||||
if r.Rule == "" {
|
||||
b.errs = append(b.errs, fmt.Errorf("router %q: %w: rule fehlt", name, ErrIncomplete))
|
||||
}
|
||||
if r.Service != "" {
|
||||
continue
|
||||
}
|
||||
// Wie Traefiks Docker-Provider: ohne explizite Angabe wird der
|
||||
// Router an den einzigen Service des Containers gebunden.
|
||||
if len(b.services) != 1 {
|
||||
b.errs = append(b.errs, fmt.Errorf("router %q: %w: service fehlt und ist bei %d Services nicht eindeutig", name, ErrIncomplete, len(b.services)))
|
||||
continue
|
||||
}
|
||||
for svc := range b.services {
|
||||
r.Service = svc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.middlewares) > 0 {
|
||||
http.Middlewares = b.middlewares
|
||||
}
|
||||
return &Configuration{HTTP: http}
|
||||
}
|
||||
|
||||
func splitList(value string) []string {
|
||||
var out []string
|
||||
for _, v := range strings.Split(value, ",") {
|
||||
if v = strings.TrimSpace(v); v != "" {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user