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 }{ {"UDP nicht unterstützt", "h", map[string]string{"traefik.udp.routers.dns.entrypoints": "dns"}, 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.udp.routers.dns.entrypoints": "dns", "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) } } }