erste module drin.

zwischenstand: Architektur aktualisiert, TOFU implementiert,
This commit is contained in:
br committed 2026-10-01 22:22:13 +02:00
1 parent 9d5789c5d7
commit c8df9d7651
41 files changed
+6455 -36

No files matched your search

+360
View File
@@ -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)
}
}
}