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,125 @@
|
||||
// Package traefik erzeugt die dynamische Traefik-Konfiguration, die der
|
||||
// Sultan-Server über den HTTP-Provider (GET /v1/traefik-config) ausliefert.
|
||||
//
|
||||
// Die Typen bilden die Teilmenge von Traefiks "dynamic configuration" ab,
|
||||
// die Sultan aus Docker-Labels bzw. /etc/sultan.d-Dateien erzeugt. Die
|
||||
// Ausgabe ist JSON oder YAML; Traefiks HTTP-Provider akzeptiert beides.
|
||||
package traefik
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Configuration ist die Wurzel der dynamischen Traefik-Konfiguration.
|
||||
type Configuration struct {
|
||||
HTTP *HTTPConfiguration `json:"http,omitempty" yaml:"http,omitempty"`
|
||||
}
|
||||
|
||||
// HTTPConfiguration enthält Router, Services und Middlewares der HTTP-Ebene.
|
||||
type HTTPConfiguration struct {
|
||||
Routers map[string]*Router `json:"routers,omitempty" yaml:"routers,omitempty"`
|
||||
Services map[string]*Service `json:"services,omitempty" yaml:"services,omitempty"`
|
||||
Middlewares map[string]Middleware `json:"middlewares,omitempty" yaml:"middlewares,omitempty"`
|
||||
}
|
||||
|
||||
// Router entspricht einem Traefik-HTTP-Router.
|
||||
type Router struct {
|
||||
EntryPoints []string `json:"entryPoints,omitempty" yaml:"entryPoints,omitempty"`
|
||||
Middlewares []string `json:"middlewares,omitempty" yaml:"middlewares,omitempty"`
|
||||
Service string `json:"service" yaml:"service"`
|
||||
Rule string `json:"rule" yaml:"rule"`
|
||||
Priority int `json:"priority,omitempty" yaml:"priority,omitempty"`
|
||||
TLS *RouterTLS `json:"tls,omitempty" yaml:"tls,omitempty"`
|
||||
}
|
||||
|
||||
// RouterTLS aktiviert TLS für einen Router. Ein leeres, nicht-nil RouterTLS
|
||||
// wird als "tls: {}" gerendert und schaltet TLS mit Standardwerten ein.
|
||||
type RouterTLS struct {
|
||||
Options string `json:"options,omitempty" yaml:"options,omitempty"`
|
||||
CertResolver string `json:"certResolver,omitempty" yaml:"certResolver,omitempty"`
|
||||
}
|
||||
|
||||
// Service entspricht einem Traefik-HTTP-Service.
|
||||
type Service struct {
|
||||
LoadBalancer *ServersLoadBalancer `json:"loadBalancer,omitempty" yaml:"loadBalancer,omitempty"`
|
||||
}
|
||||
|
||||
// ServersLoadBalancer verteilt Anfragen auf die angegebenen Server.
|
||||
type ServersLoadBalancer struct {
|
||||
Servers []Server `json:"servers" yaml:"servers"`
|
||||
PassHostHeader *bool `json:"passHostHeader,omitempty" yaml:"passHostHeader,omitempty"`
|
||||
}
|
||||
|
||||
// Server ist ein einzelnes Backend-Ziel.
|
||||
type Server struct {
|
||||
URL string `json:"url" yaml:"url"`
|
||||
}
|
||||
|
||||
// Middleware ist eine Traefik-Middleware als verschachtelte Map, z. B.
|
||||
// {"redirectRegex": {"regex": "...", "permanent": "true"}}.
|
||||
//
|
||||
// Die Blattwerte bleiben die unveränderten Label-Strings. Traefik wandelt sie
|
||||
// beim Einlesen genauso in Bool-, Zahl- und Listenwerte um wie Labels
|
||||
// (Listen kommagetrennt), deshalb braucht Sultan nicht jeden Middleware-Typ zu
|
||||
// kennen.
|
||||
type Middleware map[string]any
|
||||
|
||||
// Format ist das Ausgabeformat der Konfiguration.
|
||||
type Format string
|
||||
|
||||
const (
|
||||
FormatJSON Format = "json"
|
||||
FormatYAML Format = "yaml"
|
||||
)
|
||||
|
||||
// ParseFormat wandelt z. B. einen Query-Parameter in ein Format um.
|
||||
// Ein leerer String ergibt FormatJSON.
|
||||
func ParseFormat(s string) (Format, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(s)) {
|
||||
case "", "json":
|
||||
return FormatJSON, nil
|
||||
case "yaml", "yml":
|
||||
return FormatYAML, nil
|
||||
default:
|
||||
return "", fmt.Errorf("traefik: unbekanntes Format %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
// ContentType liefert den passenden HTTP-Content-Type.
|
||||
func (f Format) ContentType() string {
|
||||
if f == FormatYAML {
|
||||
return "application/yaml"
|
||||
}
|
||||
return "application/json"
|
||||
}
|
||||
|
||||
// Marshal rendert die Konfiguration im angegebenen Format. Eine nil-
|
||||
// Konfiguration ergibt ein leeres Objekt, das Traefik als "keine Routen"
|
||||
// akzeptiert.
|
||||
func (c *Configuration) Marshal(f Format) ([]byte, error) {
|
||||
if c == nil {
|
||||
c = &Configuration{}
|
||||
}
|
||||
switch f {
|
||||
case FormatJSON:
|
||||
return json.Marshal(c)
|
||||
case FormatYAML:
|
||||
var buf bytes.Buffer
|
||||
enc := yaml.NewEncoder(&buf)
|
||||
enc.SetIndent(2)
|
||||
if err := enc.Encode(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := enc.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("traefik: unbekanntes Format %q", f)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package traefik
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func mustJSON(t *testing.T, v any) string {
|
||||
t.Helper()
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal: %v", err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func sampleConfig() *Configuration {
|
||||
return &Configuration{HTTP: &HTTPConfiguration{
|
||||
Routers: map[string]*Router{
|
||||
"r": {EntryPoints: []string{"web"}, Service: "s", Rule: "Host(`a.de`)", TLS: &RouterTLS{}},
|
||||
},
|
||||
Services: map[string]*Service{
|
||||
"s": {LoadBalancer: &ServersLoadBalancer{Servers: []Server{{URL: "http://10.0.0.1:80"}}}},
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func TestMarshal_JSON(t *testing.T) {
|
||||
got, err := sampleConfig().Marshal(FormatJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("unerwarteter Fehler: %v", err)
|
||||
}
|
||||
want := `{"http":{"routers":{"r":{"entryPoints":["web"],"service":"s","rule":"Host(` + "`a.de`" + `)","tls":{}}},` +
|
||||
`"services":{"s":{"loadBalancer":{"servers":[{"url":"http://10.0.0.1:80"}]}}}}}`
|
||||
if string(got) != want {
|
||||
t.Errorf("JSON weicht ab\n got: %s\nwant: %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshal_YAML(t *testing.T) {
|
||||
got, err := sampleConfig().Marshal(FormatYAML)
|
||||
if err != nil {
|
||||
t.Fatalf("unerwarteter Fehler: %v", err)
|
||||
}
|
||||
want := "http:\n" +
|
||||
" routers:\n" +
|
||||
" r:\n" +
|
||||
" entryPoints:\n" +
|
||||
" - web\n" +
|
||||
" service: s\n" +
|
||||
" rule: Host(`a.de`)\n" +
|
||||
" tls: {}\n" +
|
||||
" services:\n" +
|
||||
" s:\n" +
|
||||
" loadBalancer:\n" +
|
||||
" servers:\n" +
|
||||
" - url: http://10.0.0.1:80\n"
|
||||
if string(got) != want {
|
||||
t.Errorf("YAML weicht ab\n got:\n%s\nwant:\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshal_Empty(t *testing.T) {
|
||||
for _, c := range []*Configuration{nil, {}} {
|
||||
j, err := c.Marshal(FormatJSON)
|
||||
if err != nil || string(j) != "{}" {
|
||||
t.Errorf("JSON = %q, %v; erwartet {}", j, err)
|
||||
}
|
||||
y, err := c.Marshal(FormatYAML)
|
||||
if err != nil || string(y) != "{}\n" {
|
||||
t.Errorf("YAML = %q, %v; erwartet {}\\n", y, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshal_UnknownFormat(t *testing.T) {
|
||||
if _, err := sampleConfig().Marshal("toml"); err == nil {
|
||||
t.Error("erwartet Fehler für unbekanntes Format")
|
||||
}
|
||||
}
|
||||
|
||||
// Die Ausgabe muss sich verlustfrei zurücklesen lassen – inklusive der
|
||||
// verschachtelten Middleware-Maps und der optionalen Felder.
|
||||
func TestMarshal_RoundTrip(t *testing.T) {
|
||||
orig, err := FromLabels("100.64.0.42", wordpressLabels())
|
||||
if err != nil {
|
||||
t.Fatalf("FromLabels: %v", err)
|
||||
}
|
||||
orig.HTTP.Services["wp"].LoadBalancer.PassHostHeader = boolPtr(false)
|
||||
orig.HTTP.Routers["wp"].Priority = 7
|
||||
|
||||
decoders := map[Format]func([]byte, any) error{
|
||||
FormatJSON: json.Unmarshal,
|
||||
FormatYAML: yaml.Unmarshal,
|
||||
}
|
||||
for f, decode := range decoders {
|
||||
t.Run(string(f), func(t *testing.T) {
|
||||
b, err := orig.Marshal(f)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal: %v", err)
|
||||
}
|
||||
var back Configuration
|
||||
if err := decode(b, &back); err != nil {
|
||||
t.Fatalf("Unmarshal: %v\n%s", err, b)
|
||||
}
|
||||
// Vergleich über JSON statt reflect.DeepEqual: yaml.v3 dekodiert
|
||||
// verschachtelte Maps in den Typ der äußeren Map (Middleware statt
|
||||
// map[string]any), inhaltlich ist das identisch.
|
||||
if got, want := mustJSON(t, &back), mustJSON(t, orig); got != want {
|
||||
t.Errorf("Round-Trip weicht ab\n got: %s\nwant: %s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFormat(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want Format
|
||||
wantErr bool
|
||||
}{
|
||||
{"", FormatJSON, false},
|
||||
{"json", FormatJSON, false},
|
||||
{" JSON ", FormatJSON, false},
|
||||
{"yaml", FormatYAML, false},
|
||||
{"YML", FormatYAML, false},
|
||||
{"toml", "", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got, err := ParseFormat(tt.in)
|
||||
if (err != nil) != tt.wantErr || got != tt.want {
|
||||
t.Errorf("ParseFormat(%q) = %q, %v; erwartet %q, Fehler=%v", tt.in, got, err, tt.want, tt.wantErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormat_ContentType(t *testing.T) {
|
||||
if got := FormatJSON.ContentType(); got != "application/json" {
|
||||
t.Errorf("JSON: %q", got)
|
||||
}
|
||||
if got := FormatYAML.ContentType(); got != "application/yaml" {
|
||||
t.Errorf("YAML: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
package traefik
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func boolPtr(b bool) *bool { return &b }
|
||||
|
||||
// Labels aus dem Compose-Beispiel der Architektur-Spec (Abschnitt 7.2).
|
||||
func wordpressLabels() map[string]string {
|
||||
return map[string]string{
|
||||
"traefik.enable": "true",
|
||||
"traefik.http.routers.wp.rule": "Host(`kunde.de`) || Host(`www.kunde.de`)",
|
||||
"traefik.http.routers.wp.entrypoints": "websecure",
|
||||
"traefik.http.routers.wp.tls": "true",
|
||||
"traefik.http.routers.wp.tls.certresolver": "dns-resolver",
|
||||
"traefik.http.middlewares.wp-www.redirectregex.regex": `^https?://kunde\.de/(.*)`,
|
||||
"traefik.http.middlewares.wp-www.redirectregex.replacement": "https://www.kunde.de/${1}",
|
||||
"traefik.http.middlewares.wp-www.redirectregex.permanent": "true",
|
||||
"traefik.http.routers.wp.middlewares": "wp-www",
|
||||
"traefik.http.services.wp.loadbalancer.server.port": "80",
|
||||
"com.docker.compose.project": "kunde",
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromLabels_SpecExample(t *testing.T) {
|
||||
got, err := FromLabels("100.64.0.42", wordpressLabels())
|
||||
if err != nil {
|
||||
t.Fatalf("unerwarteter Fehler: %v", err)
|
||||
}
|
||||
want := &Configuration{HTTP: &HTTPConfiguration{
|
||||
Routers: map[string]*Router{
|
||||
"wp": {
|
||||
EntryPoints: []string{"websecure"},
|
||||
Middlewares: []string{"wp-www"},
|
||||
Service: "wp",
|
||||
Rule: "Host(`kunde.de`) || Host(`www.kunde.de`)",
|
||||
TLS: &RouterTLS{CertResolver: "dns-resolver"},
|
||||
},
|
||||
},
|
||||
Services: map[string]*Service{
|
||||
"wp": {LoadBalancer: &ServersLoadBalancer{Servers: []Server{{URL: "http://100.64.0.42:80"}}}},
|
||||
},
|
||||
Middlewares: map[string]Middleware{
|
||||
"wp-www": {"redirectregex": map[string]any{
|
||||
"regex": `^https?://kunde\.de/(.*)`,
|
||||
"replacement": "https://www.kunde.de/${1}",
|
||||
"permanent": "true",
|
||||
}},
|
||||
},
|
||||
}}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Konfiguration weicht ab\n got: %s\nwant: %s", mustJSON(t, got), mustJSON(t, want))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromLabels_Enable(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
labels map[string]string
|
||||
wantErr bool
|
||||
}{
|
||||
{"fehlt", map[string]string{"traefik.http.routers.a.rule": "Host(`a.de`)"}, false},
|
||||
{"false", map[string]string{"traefik.enable": "false", "traefik.http.routers.a.rule": "x"}, false},
|
||||
{"leere Labels", nil, false},
|
||||
{"ungültig", map[string]string{"traefik.enable": "jein"}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := FromLabels("10.0.0.1", tt.labels)
|
||||
if tt.wantErr {
|
||||
if !errors.Is(err, ErrInvalidLabel) {
|
||||
t.Fatalf("erwartet ErrInvalidLabel, bekommen %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unerwarteter Fehler: %v", err)
|
||||
}
|
||||
if got == nil || got.HTTP != nil {
|
||||
t.Errorf("erwartet leere Konfiguration, bekommen %+v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromLabels_EnabledWithoutHTTPLabels(t *testing.T) {
|
||||
got, err := FromLabels("10.0.0.1", map[string]string{
|
||||
"traefik.enable": "true",
|
||||
"traefik.docker.network": "proxy",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unerwarteter Fehler: %v", err)
|
||||
}
|
||||
if got.HTTP != nil {
|
||||
t.Errorf("erwartet leere Konfiguration, bekommen %+v", got.HTTP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromLabels_CaseInsensitiveKeys(t *testing.T) {
|
||||
got, err := FromLabels("10.0.0.1", map[string]string{
|
||||
"Traefik.Enable": "TRUE",
|
||||
"TRAEFIK.HTTP.Routers.MyApp.Rule": "Host(`a.de`)",
|
||||
"traefik.http.SERVICES.MyApp.LoadBalancer.Server.Port": "8080",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unerwarteter Fehler: %v", err)
|
||||
}
|
||||
r, ok := got.HTTP.Routers["MyApp"]
|
||||
if !ok {
|
||||
t.Fatalf("Router MyApp fehlt (Name muss Schreibweise behalten): %+v", got.HTTP.Routers)
|
||||
}
|
||||
if r.Service != "MyApp" {
|
||||
t.Errorf("Service = %q, erwartet MyApp", r.Service)
|
||||
}
|
||||
if u := got.HTTP.Services["MyApp"].LoadBalancer.Servers[0].URL; u != "http://10.0.0.1:8080" {
|
||||
t.Errorf("URL = %q", u)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromLabels_RouterAttributes(t *testing.T) {
|
||||
got, err := FromLabels("10.0.0.1", map[string]string{
|
||||
"traefik.enable": "true",
|
||||
"traefik.http.routers.r.rule": " Host(`a.de`) ",
|
||||
"traefik.http.routers.r.entrypoints": "web, websecure ,,",
|
||||
"traefik.http.routers.r.middlewares": "auth@file,compress",
|
||||
"traefik.http.routers.r.priority": " 42 ",
|
||||
"traefik.http.routers.r.service": "api@internal",
|
||||
"traefik.http.routers.r.tls.options": "modern@file",
|
||||
"traefik.http.routers.plain.rule": "Host(`b.de`)",
|
||||
"traefik.http.routers.plain.service": "noop@internal",
|
||||
"traefik.http.routers.plain.tls": "false",
|
||||
"traefik.http.routers.tlsonly.rule": "Host(`c.de`)",
|
||||
"traefik.http.routers.tlsonly.service": "noop@internal",
|
||||
"traefik.http.routers.tlsonly.tls": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unerwarteter Fehler: %v", err)
|
||||
}
|
||||
want := map[string]*Router{
|
||||
"r": {
|
||||
EntryPoints: []string{"web", "websecure"},
|
||||
Middlewares: []string{"auth@file", "compress"},
|
||||
Service: "api@internal",
|
||||
Rule: "Host(`a.de`)",
|
||||
Priority: 42,
|
||||
TLS: &RouterTLS{Options: "modern@file"},
|
||||
},
|
||||
"plain": {Service: "noop@internal", Rule: "Host(`b.de`)"},
|
||||
"tlsonly": {Service: "noop@internal", Rule: "Host(`c.de`)", TLS: &RouterTLS{}},
|
||||
}
|
||||
if !reflect.DeepEqual(got.HTTP.Routers, want) {
|
||||
t.Errorf("Router weichen ab\n got: %s\nwant: %s", mustJSON(t, got.HTTP.Routers), mustJSON(t, want))
|
||||
}
|
||||
if got.HTTP.Services != nil {
|
||||
t.Errorf("keine Services erwartet, bekommen %+v", got.HTTP.Services)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromLabels_ServiceAttributes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
labels map[string]string
|
||||
want ServersLoadBalancer
|
||||
}{
|
||||
{
|
||||
name: "Standardschema http",
|
||||
host: "192.168.1.10",
|
||||
labels: map[string]string{"traefik.http.services.s.loadbalancer.server.port": "80"},
|
||||
want: ServersLoadBalancer{Servers: []Server{{URL: "http://192.168.1.10:80"}}},
|
||||
},
|
||||
{
|
||||
name: "https und passHostHeader",
|
||||
host: "100.64.1.2",
|
||||
labels: map[string]string{
|
||||
"traefik.http.services.s.loadbalancer.server.port": "8443",
|
||||
"traefik.http.services.s.loadbalancer.server.scheme": "HTTPS",
|
||||
"traefik.http.services.s.loadbalancer.passhostheader": "false",
|
||||
},
|
||||
want: ServersLoadBalancer{
|
||||
Servers: []Server{{URL: "https://100.64.1.2:8443"}},
|
||||
PassHostHeader: boolPtr(false),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "h2c",
|
||||
host: "backend.lan",
|
||||
labels: map[string]string{
|
||||
"traefik.http.services.s.loadbalancer.server.port": "9000",
|
||||
"traefik.http.services.s.loadbalancer.server.scheme": "h2c",
|
||||
},
|
||||
want: ServersLoadBalancer{Servers: []Server{{URL: "h2c://backend.lan:9000"}}},
|
||||
},
|
||||
{
|
||||
name: "IPv6 in Klammern",
|
||||
host: "fd00::1",
|
||||
labels: map[string]string{"traefik.http.services.s.loadbalancer.server.port": "80"},
|
||||
want: ServersLoadBalancer{Servers: []Server{{URL: "http://[fd00::1]:80"}}},
|
||||
},
|
||||
{
|
||||
name: "Port mit führender Null normalisiert",
|
||||
host: "10.0.0.1",
|
||||
labels: map[string]string{"traefik.http.services.s.loadbalancer.server.port": "080"},
|
||||
want: ServersLoadBalancer{Servers: []Server{{URL: "http://10.0.0.1:80"}}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.labels["traefik.enable"] = "true"
|
||||
got, err := FromLabels(tt.host, tt.labels)
|
||||
if err != nil {
|
||||
t.Fatalf("unerwarteter Fehler: %v", err)
|
||||
}
|
||||
if lb := got.HTTP.Services["s"].LoadBalancer; !reflect.DeepEqual(*lb, tt.want) {
|
||||
t.Errorf("LoadBalancer = %+v, erwartet %+v", *lb, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromLabels_DefaultServiceBinding(t *testing.T) {
|
||||
got, err := FromLabels("10.0.0.1", map[string]string{
|
||||
"traefik.enable": "true",
|
||||
"traefik.http.routers.a.rule": "Host(`a.de`)",
|
||||
"traefik.http.routers.b.rule": "Host(`b.de`)",
|
||||
"traefik.http.services.app.loadbalancer.server.port": "3000",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unerwarteter Fehler: %v", err)
|
||||
}
|
||||
for _, name := range []string{"a", "b"} {
|
||||
if s := got.HTTP.Routers[name].Service; s != "app" {
|
||||
t.Errorf("Router %s: Service = %q, erwartet app", name, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromLabels_Middlewares(t *testing.T) {
|
||||
got, err := FromLabels("", map[string]string{
|
||||
"traefik.enable": "true",
|
||||
"traefik.http.middlewares.gzip.compress": "true",
|
||||
"traefik.http.middlewares.hdr.headers.customrequestheaders.X-Forwarded-Proto": "https",
|
||||
"traefik.http.middlewares.hdr.headers.customrequestheaders.X-Tenant": "kunde",
|
||||
"traefik.http.middlewares.hdr.headers.stsseconds": "31536000",
|
||||
"traefik.http.middlewares.allow.ipallowlist.sourcerange": "10.0.0.0/8, 192.168.0.0/16",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unerwarteter Fehler: %v", err)
|
||||
}
|
||||
want := map[string]Middleware{
|
||||
"gzip": {"compress": "true"},
|
||||
"hdr": {"headers": map[string]any{
|
||||
"customrequestheaders": map[string]any{
|
||||
"X-Forwarded-Proto": "https",
|
||||
"X-Tenant": "kunde",
|
||||
},
|
||||
"stsseconds": "31536000",
|
||||
}},
|
||||
"allow": {"ipallowlist": map[string]any{"sourcerange": "10.0.0.0/8, 192.168.0.0/16"}},
|
||||
}
|
||||
if !reflect.DeepEqual(got.HTTP.Middlewares, want) {
|
||||
t.Errorf("Middlewares weichen ab\n got: %s\nwant: %s", mustJSON(t, got.HTTP.Middlewares), mustJSON(t, want))
|
||||
}
|
||||
if got.HTTP.Routers != nil || got.HTTP.Services != nil {
|
||||
t.Errorf("nur Middlewares erwartet, bekommen %+v", got.HTTP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromLabels_Errors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
labels map[string]string
|
||||
want error
|
||||
}{
|
||||
{"TCP nicht unterstützt", "h", map[string]string{"traefik.tcp.routers.db.rule": "HostSNI(`*`)"}, ErrUnsupportedLabel},
|
||||
{"UDP nicht unterstützt", "h", map[string]string{"traefik.udp.services.dns.loadbalancer.server.port": "53"}, ErrUnsupportedLabel},
|
||||
{"unbekannte HTTP-Art", "h", map[string]string{"traefik.http.serverstransports.x.insecureskipverify": "true"}, ErrUnsupportedLabel},
|
||||
{"Schlüssel zu kurz", "h", map[string]string{"traefik.http.routers.r": "x"}, ErrUnsupportedLabel},
|
||||
{"nur Präfix", "h", map[string]string{"traefik.": "x"}, ErrUnsupportedLabel},
|
||||
{"unbekanntes Router-Attribut", "h", map[string]string{"traefik.http.routers.r.rule": "x", "traefik.http.routers.r.service": "s@file", "traefik.http.routers.r.tls.domains[0].main": "a.de"}, ErrUnsupportedLabel},
|
||||
{"unbekanntes Service-Attribut", "h", map[string]string{"traefik.http.services.s.loadbalancer.server.port": "80", "traefik.http.services.s.loadbalancer.sticky.cookie": "true"}, ErrUnsupportedLabel},
|
||||
{"leerer Name", "h", map[string]string{"traefik.http.routers..rule": "x"}, ErrInvalidLabel},
|
||||
{"Priority keine Zahl", "h", map[string]string{"traefik.http.routers.r.rule": "x", "traefik.http.routers.r.service": "s@file", "traefik.http.routers.r.priority": "hoch"}, ErrInvalidLabel},
|
||||
{"TLS kein Bool", "h", map[string]string{"traefik.http.routers.r.rule": "x", "traefik.http.routers.r.service": "s@file", "traefik.http.routers.r.tls": "vielleicht"}, ErrInvalidLabel},
|
||||
{"Port keine Zahl", "h", map[string]string{"traefik.http.services.s.loadbalancer.server.port": "http"}, ErrInvalidLabel},
|
||||
{"Port 0", "h", map[string]string{"traefik.http.services.s.loadbalancer.server.port": "0"}, ErrInvalidLabel},
|
||||
{"Port zu groß", "h", map[string]string{"traefik.http.services.s.loadbalancer.server.port": "65536"}, ErrInvalidLabel},
|
||||
{"ungültiges Schema", "h", map[string]string{"traefik.http.services.s.loadbalancer.server.port": "80", "traefik.http.services.s.loadbalancer.server.scheme": "ftp"}, ErrInvalidLabel},
|
||||
{"passHostHeader kein Bool", "h", map[string]string{"traefik.http.services.s.loadbalancer.server.port": "80", "traefik.http.services.s.loadbalancer.passhostheader": "ja"}, ErrInvalidLabel},
|
||||
{"Middleware Wert dann Objekt", "h", map[string]string{"traefik.http.middlewares.m.headers": "x", "traefik.http.middlewares.m.headers.stsseconds": "1"}, ErrInvalidLabel},
|
||||
{"Middleware leeres Segment", "h", map[string]string{"traefik.http.middlewares.m.headers..x": "1"}, ErrInvalidLabel},
|
||||
{"Middleware ohne Typ", "h", map[string]string{"traefik.http.middlewares.m": "x"}, ErrUnsupportedLabel},
|
||||
{"Rule fehlt", "h", map[string]string{"traefik.http.routers.r.service": "s@file"}, ErrIncomplete},
|
||||
{"Port fehlt", "h", map[string]string{"traefik.http.services.s.loadbalancer.server.scheme": "https"}, ErrIncomplete},
|
||||
{"Host fehlt", " ", map[string]string{"traefik.http.services.s.loadbalancer.server.port": "80"}, ErrIncomplete},
|
||||
{"Service fehlt ohne Services", "h", map[string]string{"traefik.http.routers.r.rule": "x"}, ErrIncomplete},
|
||||
{"Service mehrdeutig", "h", map[string]string{
|
||||
"traefik.http.routers.r.rule": "x",
|
||||
"traefik.http.services.a.loadbalancer.server.port": "80",
|
||||
"traefik.http.services.b.loadbalancer.server.port": "81",
|
||||
}, ErrIncomplete},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.labels["traefik.enable"] = "true"
|
||||
got, err := FromLabels(tt.host, tt.labels)
|
||||
if !errors.Is(err, tt.want) {
|
||||
t.Fatalf("erwartet %v, bekommen %v", tt.want, err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Errorf("bei Fehler erwartet nil-Konfiguration, bekommen %+v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromLabels_Middlewares_ObjectThenValue(t *testing.T) {
|
||||
// Sortierte Verarbeitung: "...headers.a" kommt vor "...headers.a.b" – hier
|
||||
// wird der umgekehrte Fall (erst Objekt, dann Wert am selben Pfad) geprüft.
|
||||
m := Middleware{}
|
||||
if err := setMiddlewareAttr(m, []string{"headers", "a", "b"}, "1"); err != nil {
|
||||
t.Fatalf("unerwarteter Fehler: %v", err)
|
||||
}
|
||||
if err := setMiddlewareAttr(m, []string{"headers", "a"}, "2"); !errors.Is(err, ErrInvalidLabel) {
|
||||
t.Errorf("erwartet ErrInvalidLabel, bekommen %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromLabels_CollectsAllErrors(t *testing.T) {
|
||||
_, err := FromLabels("h", map[string]string{
|
||||
"traefik.enable": "true",
|
||||
"traefik.tcp.routers.db.rule": "HostSNI(`*`)",
|
||||
"traefik.http.services.s.loadbalancer.server.port": "nope",
|
||||
"traefik.http.routers.r.service": "s",
|
||||
})
|
||||
for _, want := range []error{ErrUnsupportedLabel, ErrInvalidLabel, ErrIncomplete} {
|
||||
if !errors.Is(err, want) {
|
||||
t.Errorf("Fehler enthält %v nicht: %v", want, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitList(t *testing.T) {
|
||||
tests := map[string][]string{
|
||||
"": nil,
|
||||
" , ,": nil,
|
||||
"a": {"a"},
|
||||
"a,b": {"a", "b"},
|
||||
" a , b ,, c ": {"a", "b", "c"},
|
||||
}
|
||||
for in, want := range tests {
|
||||
if got := splitList(in); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("splitList(%q) = %#v, erwartet %#v", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package traefik
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// ErrConflict meldet zwei unterschiedliche Definitionen unter demselben Namen.
|
||||
var ErrConflict = errors.New("Namenskonflikt")
|
||||
|
||||
// Merge führt die Konfigurationen mehrerer Knoten zu einer zusammen.
|
||||
// nil-Einträge werden übersprungen. Identische Definitionen unter demselben
|
||||
// Namen sind erlaubt, unterschiedliche ergeben ErrConflict; das Vergeben
|
||||
// eindeutiger Namen (z. B. pro Mandant/Knoten) ist Aufgabe des Aufrufers.
|
||||
// Die Eingaben werden nicht verändert, das Ergebnis teilt sich aber die
|
||||
// Router-, Service- und Middleware-Objekte mit ihnen.
|
||||
func Merge(configs ...*Configuration) (*Configuration, error) {
|
||||
out := &HTTPConfiguration{
|
||||
Routers: map[string]*Router{},
|
||||
Services: map[string]*Service{},
|
||||
Middlewares: map[string]Middleware{},
|
||||
}
|
||||
var errs []error
|
||||
for _, c := range configs {
|
||||
if c == nil || c.HTTP == nil {
|
||||
continue
|
||||
}
|
||||
errs = append(errs, mergeInto(out.Routers, c.HTTP.Routers, "router")...)
|
||||
errs = append(errs, mergeInto(out.Services, c.HTTP.Services, "service")...)
|
||||
errs = append(errs, mergeInto(out.Middlewares, c.HTTP.Middlewares, "middleware")...)
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return nil, errors.Join(errs...)
|
||||
}
|
||||
if len(out.Routers) == 0 && len(out.Services) == 0 && len(out.Middlewares) == 0 {
|
||||
return &Configuration{}, nil
|
||||
}
|
||||
return &Configuration{HTTP: out}, nil
|
||||
}
|
||||
|
||||
func mergeInto[V any](dst, src map[string]V, kind string) []error {
|
||||
var errs []error
|
||||
for name, v := range src {
|
||||
existing, ok := dst[name]
|
||||
if !ok {
|
||||
dst[name] = v
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(existing, v) {
|
||||
errs = append(errs, fmt.Errorf("%s %q: %w", kind, name, ErrConflict))
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package traefik
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func nodeConfig(t *testing.T, host, name, domain string) *Configuration {
|
||||
t.Helper()
|
||||
c, err := FromLabels(host, map[string]string{
|
||||
"traefik.enable": "true",
|
||||
"traefik.http.routers." + name + ".rule": "Host(`" + domain + "`)",
|
||||
"traefik.http.services." + name + ".loadbalancer.server.port": "80",
|
||||
"traefik.http.middlewares." + name + "-gzip.compress": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("FromLabels: %v", err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func TestMerge_Disjoint(t *testing.T) {
|
||||
a := nodeConfig(t, "10.0.0.1", "a", "a.de")
|
||||
b := nodeConfig(t, "10.0.0.2", "b", "b.de")
|
||||
|
||||
got, err := Merge(a, nil, &Configuration{}, b)
|
||||
if err != nil {
|
||||
t.Fatalf("unerwarteter Fehler: %v", err)
|
||||
}
|
||||
for _, name := range []string{"a", "b"} {
|
||||
if got.HTTP.Routers[name] == nil || got.HTTP.Services[name] == nil || got.HTTP.Middlewares[name+"-gzip"] == nil {
|
||||
t.Errorf("%s fehlt in %s", name, mustJSON(t, got))
|
||||
}
|
||||
}
|
||||
if n := len(a.HTTP.Routers); n != 1 {
|
||||
t.Errorf("Eingabe wurde verändert: %d Router in a", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMerge_IdenticalDuplicateAllowed(t *testing.T) {
|
||||
a := nodeConfig(t, "10.0.0.1", "a", "a.de")
|
||||
again := nodeConfig(t, "10.0.0.1", "a", "a.de")
|
||||
|
||||
got, err := Merge(a, again)
|
||||
if err != nil {
|
||||
t.Fatalf("unerwarteter Fehler: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, a) {
|
||||
t.Errorf("erwartet %s, bekommen %s", mustJSON(t, a), mustJSON(t, got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMerge_Conflicts(t *testing.T) {
|
||||
a := nodeConfig(t, "10.0.0.1", "a", "a.de")
|
||||
sameNameOtherHost := nodeConfig(t, "10.0.0.2", "a", "a.de")
|
||||
sameNameOtherRule := nodeConfig(t, "10.0.0.1", "a", "evil.de")
|
||||
otherMiddleware := &Configuration{HTTP: &HTTPConfiguration{
|
||||
Middlewares: map[string]Middleware{"a-gzip": {"compress": "false"}},
|
||||
}}
|
||||
|
||||
tests := map[string]*Configuration{
|
||||
"Service": sameNameOtherHost,
|
||||
"Router": sameNameOtherRule,
|
||||
"Middleware": otherMiddleware,
|
||||
}
|
||||
for name, other := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, err := Merge(a, other)
|
||||
if !errors.Is(err, ErrConflict) {
|
||||
t.Fatalf("erwartet ErrConflict, bekommen %v", err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Errorf("bei Konflikt erwartet nil, bekommen %+v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMerge_Empty(t *testing.T) {
|
||||
for _, in := range [][]*Configuration{nil, {nil}, {{}}, {{HTTP: &HTTPConfiguration{}}}} {
|
||||
got, err := Merge(in...)
|
||||
if err != nil {
|
||||
t.Fatalf("unerwarteter Fehler: %v", err)
|
||||
}
|
||||
if got == nil || got.HTTP != nil {
|
||||
t.Errorf("erwartet leere Konfiguration, bekommen %+v", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user