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,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user