Files
sultan/pkg/traefik/labels_test.go
T
br c8df9d7651 erste module drin.
zwischenstand: Architektur aktualisiert, TOFU implementiert,
2026-10-01 22:22:13 +02:00

361 lines
14 KiB
Go
Raw 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 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)
}
}
}