/
githubmirror
/
traefik
Обзор
Документация
Войти
/
githubmirror
/
traefik
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
pkg/server/aggregator_test.go
1 412 строк
41 KB
romain
Merge branch v2.11 into v3.6
10 авг 2026, 12:59
10 авг 2026, 12:59
f5eb1a0
Код
Авторство
О чём код?
package server import ( "slices" "testing" "github.com/go-acme/lego/v5/challenge/tlsalpn01" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/traefik/traefik/v3/pkg/config/dynamic" otypes "github.com/traefik/traefik/v3/pkg/observability/types" "github.com/traefik/traefik/v3/pkg/tls" ) func Test_mergeConfiguration(t *testing.T) { testCases := []struct { desc string given dynamic.Configurations expected *dynamic.HTTPConfiguration }{ { desc: "Nil returns an empty configuration", given: nil, expected: &dynamic.HTTPConfiguration{ Routers: make(map[string]*dynamic.Router), Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: make(map[string]*dynamic.Model), ServersTransports: make(map[string]*dynamic.ServersTransport), }, }, { desc: "Returns fully qualified elements from a mono-provider configuration map", given: dynamic.Configurations{ "provider-1": &dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "router-1": {}, }, Middlewares: map[string]*dynamic.Middleware{ "middleware-1": {}, }, Services: map[string]*dynamic.Service{ "service-1": {}, }, }, }, }, expected: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "router-1@provider-1": { EntryPoints: []string{"defaultEP"}, }, }, Middlewares: map[string]*dynamic.Middleware{ "middleware-1@provider-1": {}, }, Services: map[string]*dynamic.Service{ "service-1@provider-1": {}, }, Models: make(map[string]*dynamic.Model), ServersTransports: make(map[string]*dynamic.ServersTransport), }, }, { desc: "Returns fully qualified elements from a multi-provider configuration map", given: dynamic.Configurations{ "provider-1": &dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "router-1": {}, }, Middlewares: map[string]*dynamic.Middleware{ "middleware-1": {}, }, Services: map[string]*dynamic.Service{ "service-1": {}, }, }, }, "provider-2": &dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "router-1": {}, }, Middlewares: map[string]*dynamic.Middleware{ "middleware-1": {}, }, Services: map[string]*dynamic.Service{ "service-1": {}, }, }, }, }, expected: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "router-1@provider-1": { EntryPoints: []string{"defaultEP"}, }, "router-1@provider-2": { EntryPoints: []string{"defaultEP"}, }, }, Middlewares: map[string]*dynamic.Middleware{ "middleware-1@provider-1": {}, "middleware-1@provider-2": {}, }, Services: map[string]*dynamic.Service{ "service-1@provider-1": {}, "service-1@provider-2": {}, }, Models: make(map[string]*dynamic.Model), ServersTransports: make(map[string]*dynamic.ServersTransport), }, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { t.Parallel() actual := mergeConfiguration(test.given, []string{"defaultEP"}) assert.Equal(t, test.expected, actual.HTTP) }) } } func Test_mergeConfiguration_tlsCertificates(t *testing.T) { testCases := []struct { desc string given dynamic.Configurations expected []*tls.CertAndStores }{ { desc: "Skip temp certificates from another provider than tlsalpn", given: dynamic.Configurations{ "provider-1": &dynamic.Configuration{ TLS: &dynamic.TLSConfiguration{ Certificates: []*tls.CertAndStores{ {Certificate: tls.Certificate{}, Stores: []string{tlsalpn01.ACMETLS1Protocol}}, }, }, }, }, expected: nil, }, { desc: "Allows tlsalpn provider to give certificates", given: dynamic.Configurations{ "tlsalpn.acme": &dynamic.Configuration{ TLS: &dynamic.TLSConfiguration{ Certificates: []*tls.CertAndStores{{ Certificate: tls.Certificate{CertFile: "foo", KeyFile: "bar"}, Stores: []string{tlsalpn01.ACMETLS1Protocol}, }}, }, }, }, expected: []*tls.CertAndStores{{ Certificate: tls.Certificate{CertFile: "foo", KeyFile: "bar"}, Stores: []string{tlsalpn01.ACMETLS1Protocol}, }}, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { t.Parallel() actual := mergeConfiguration(test.given, []string{"defaultEP"}) assert.Equal(t, test.expected, actual.TLS.Certificates) }) } } func Test_mergeConfiguration_tlsOptions(t *testing.T) { testCases := []struct { desc string given dynamic.Configurations expected map[string]tls.Options }{ { desc: "Nil returns an empty configuration", given: nil, expected: map[string]tls.Options{ "default": tls.DefaultTLSOptions, }, }, { desc: "Returns fully qualified elements from a mono-provider configuration map", given: dynamic.Configurations{ "provider-1": &dynamic.Configuration{ TLS: &dynamic.TLSConfiguration{ Options: map[string]tls.Options{ "foo": { MinVersion: "VersionTLS12", }, }, }, }, }, expected: map[string]tls.Options{ "default": tls.DefaultTLSOptions, "foo@provider-1": { MinVersion: "VersionTLS12", }, }, }, { desc: "Returns fully qualified elements from a multi-provider configuration map", given: dynamic.Configurations{ "provider-1": &dynamic.Configuration{ TLS: &dynamic.TLSConfiguration{ Options: map[string]tls.Options{ "foo": { MinVersion: "VersionTLS13", }, }, }, }, "provider-2": &dynamic.Configuration{ TLS: &dynamic.TLSConfiguration{ Options: map[string]tls.Options{ "foo": { MinVersion: "VersionTLS12", }, }, }, }, }, expected: map[string]tls.Options{ "default": tls.DefaultTLSOptions, "foo@provider-1": { MinVersion: "VersionTLS13", }, "foo@provider-2": { MinVersion: "VersionTLS12", }, }, }, { desc: "Create a valid default tls option when appears only in one provider", given: dynamic.Configurations{ "provider-1": &dynamic.Configuration{ TLS: &dynamic.TLSConfiguration{ Options: map[string]tls.Options{ "foo": { MinVersion: "VersionTLS13", }, "default": { MinVersion: "VersionTLS11", }, }, }, }, "provider-2": &dynamic.Configuration{ TLS: &dynamic.TLSConfiguration{ Options: map[string]tls.Options{ "foo": { MinVersion: "VersionTLS12", }, }, }, }, }, expected: map[string]tls.Options{ "default": { MinVersion: "VersionTLS11", }, "foo@provider-1": { MinVersion: "VersionTLS13", }, "foo@provider-2": { MinVersion: "VersionTLS12", }, }, }, { desc: "No default tls option if it is defined in multiple providers", given: dynamic.Configurations{ "provider-1": &dynamic.Configuration{ TLS: &dynamic.TLSConfiguration{ Options: map[string]tls.Options{ "foo": { MinVersion: "VersionTLS12", }, "default": { MinVersion: "VersionTLS11", }, }, }, }, "provider-2": &dynamic.Configuration{ TLS: &dynamic.TLSConfiguration{ Options: map[string]tls.Options{ "foo": { MinVersion: "VersionTLS13", }, "default": { MinVersion: "VersionTLS12", }, }, }, }, }, expected: map[string]tls.Options{ "foo@provider-1": { MinVersion: "VersionTLS12", }, "foo@provider-2": { MinVersion: "VersionTLS13", }, }, }, { desc: "Create a default TLS Options configuration if none was provided", given: dynamic.Configurations{ "provider-1": &dynamic.Configuration{ TLS: &dynamic.TLSConfiguration{ Options: map[string]tls.Options{ "foo": { MinVersion: "VersionTLS12", }, }, }, }, "provider-2": &dynamic.Configuration{ TLS: &dynamic.TLSConfiguration{ Options: map[string]tls.Options{ "foo": { MinVersion: "VersionTLS13", }, }, }, }, }, expected: map[string]tls.Options{ "default": tls.DefaultTLSOptions, "foo@provider-1": { MinVersion: "VersionTLS12", }, "foo@provider-2": { MinVersion: "VersionTLS13", }, }, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { t.Parallel() actual := mergeConfiguration(test.given, []string{"defaultEP"}) assert.Equal(t, test.expected, actual.TLS.Options) }) } } func Test_mergeConfiguration_tlsStore(t *testing.T) { testCases := []struct { desc string given dynamic.Configurations expected map[string]tls.Store }{ { desc: "Create a valid default tls store when appears only in one provider", given: dynamic.Configurations{ "provider-1": &dynamic.Configuration{ TLS: &dynamic.TLSConfiguration{ Stores: map[string]tls.Store{ "default": { DefaultCertificate: &tls.Certificate{ CertFile: "foo", KeyFile: "bar", }, }, }, }, }, "provider-2": &dynamic.Configuration{ TLS: &dynamic.TLSConfiguration{ Stores: map[string]tls.Store{ "foo": { DefaultCertificate: &tls.Certificate{ CertFile: "foo", KeyFile: "bar", }, }, }, }, }, }, expected: map[string]tls.Store{ "default": { DefaultCertificate: &tls.Certificate{ CertFile: "foo", KeyFile: "bar", }, }, "foo@provider-2": { DefaultCertificate: &tls.Certificate{ CertFile: "foo", KeyFile: "bar", }, }, }, }, { desc: "Don't default tls store when appears two times", given: dynamic.Configurations{ "provider-1": &dynamic.Configuration{ TLS: &dynamic.TLSConfiguration{ Stores: map[string]tls.Store{ "default": { DefaultCertificate: &tls.Certificate{ CertFile: "foo", KeyFile: "bar", }, }, }, }, }, "provider-2": &dynamic.Configuration{ TLS: &dynamic.TLSConfiguration{ Stores: map[string]tls.Store{ "default": { DefaultCertificate: &tls.Certificate{ CertFile: "foo", KeyFile: "bar", }, }, }, }, }, }, expected: map[string]tls.Store{}, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { t.Parallel() actual := mergeConfiguration(test.given, []string{"defaultEP"}) assert.Equal(t, test.expected, actual.TLS.Stores) }) } } func Test_mergeConfiguration_defaultTCPEntryPoint(t *testing.T) { given := dynamic.Configurations{ "provider-1": &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ "router-1": {}, }, Services: map[string]*dynamic.TCPService{ "service-1": {}, }, }, }, } expected := &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ "router-1@provider-1": { EntryPoints: []string{"defaultEP"}, }, }, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{ "service-1@provider-1": {}, }, Models: map[string]*dynamic.TCPModel{}, ServersTransports: make(map[string]*dynamic.TCPServersTransport), } actual := mergeConfiguration(given, []string{"defaultEP"}) assert.Equal(t, expected, actual.TCP) } func Test_applyModel(t *testing.T) { testCases := []struct { desc string input dynamic.Configuration expected dynamic.Configuration }{ { desc: "empty configuration", input: dynamic.Configuration{}, expected: dynamic.Configuration{}, }, { desc: "without model", input: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: make(map[string]*dynamic.Router), Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: make(map[string]*dynamic.Model), }, }, expected: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: make(map[string]*dynamic.Router), Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: make(map[string]*dynamic.Model), }, }, }, { desc: "without model, one router", input: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{"test": {}}, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: make(map[string]*dynamic.Model), }, }, expected: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "test": { Observability: &dynamic.RouterObservabilityConfig{ AccessLogs: new(true), Metrics: new(true), Tracing: new(true), TraceVerbosity: otypes.MinimalVerbosity, }, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: make(map[string]*dynamic.Model), }, }, }, { desc: "with model, not used", input: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: make(map[string]*dynamic.Router), Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: map[string]*dynamic.Model{ "ep@internal": { Middlewares: []string{"test"}, TLS: &dynamic.RouterTLSConfig{}, }, }, }, }, expected: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: make(map[string]*dynamic.Router), Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: map[string]*dynamic.Model{ "ep@internal": { Middlewares: []string{"test"}, TLS: &dynamic.RouterTLSConfig{}, }, }, }, }, }, { desc: "with model, one entry point", input: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "test": { EntryPoints: []string{"websecure"}, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: map[string]*dynamic.Model{ "websecure@internal": { Middlewares: []string{"test"}, TLS: &dynamic.RouterTLSConfig{}, }, }, }, }, expected: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "test": { EntryPoints: []string{"websecure"}, Middlewares: []string{"test"}, TLS: &dynamic.RouterTLSConfig{}, Observability: &dynamic.RouterObservabilityConfig{ AccessLogs: new(true), Metrics: new(true), Tracing: new(true), TraceVerbosity: otypes.MinimalVerbosity, }, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: map[string]*dynamic.Model{ "websecure@internal": { Middlewares: []string{"test"}, TLS: &dynamic.RouterTLSConfig{}, }, }, }, }, }, { desc: "with model, one entry point with observability", input: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "test": { EntryPoints: []string{"websecure"}, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: map[string]*dynamic.Model{ "websecure@internal": { Middlewares: []string{"test"}, TLS: &dynamic.RouterTLSConfig{}, Observability: dynamic.RouterObservabilityConfig{ AccessLogs: new(true), Tracing: new(true), Metrics: new(true), TraceVerbosity: otypes.MinimalVerbosity, }, }, }, }, }, expected: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "test": { EntryPoints: []string{"websecure"}, Middlewares: []string{"test"}, TLS: &dynamic.RouterTLSConfig{}, Observability: &dynamic.RouterObservabilityConfig{ AccessLogs: new(true), Tracing: new(true), Metrics: new(true), TraceVerbosity: otypes.MinimalVerbosity, }, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: map[string]*dynamic.Model{ "websecure@internal": { Middlewares: []string{"test"}, TLS: &dynamic.RouterTLSConfig{}, Observability: dynamic.RouterObservabilityConfig{ AccessLogs: new(true), Tracing: new(true), Metrics: new(true), TraceVerbosity: otypes.MinimalVerbosity, }, }, }, }, }, }, { desc: "with model, one entry point, and router with tls", input: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "test": { EntryPoints: []string{"websecure"}, TLS: &dynamic.RouterTLSConfig{CertResolver: "router"}, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: map[string]*dynamic.Model{ "websecure@internal": { Middlewares: []string{"test"}, TLS: &dynamic.RouterTLSConfig{CertResolver: "ep"}, }, }, }, }, expected: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "test": { EntryPoints: []string{"websecure"}, Middlewares: []string{"test"}, TLS: &dynamic.RouterTLSConfig{CertResolver: "router"}, Observability: &dynamic.RouterObservabilityConfig{ AccessLogs: new(true), Metrics: new(true), Tracing: new(true), TraceVerbosity: otypes.MinimalVerbosity, }, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: map[string]*dynamic.Model{ "websecure@internal": { Middlewares: []string{"test"}, TLS: &dynamic.RouterTLSConfig{CertResolver: "ep"}, }, }, }, }, }, { desc: "with model, two entry points", input: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "test": { EntryPoints: []string{"websecure", "web"}, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: map[string]*dynamic.Model{ "websecure@internal": { Middlewares: []string{"test"}, TLS: &dynamic.RouterTLSConfig{}, }, }, }, }, expected: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "test": { EntryPoints: []string{"web"}, Observability: &dynamic.RouterObservabilityConfig{ AccessLogs: new(true), Metrics: new(true), Tracing: new(true), TraceVerbosity: otypes.MinimalVerbosity, }, }, "websecure-test": { EntryPoints: []string{"websecure"}, Middlewares: []string{"test"}, TLS: &dynamic.RouterTLSConfig{}, Observability: &dynamic.RouterObservabilityConfig{ AccessLogs: new(true), Metrics: new(true), Tracing: new(true), TraceVerbosity: otypes.MinimalVerbosity, }, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: map[string]*dynamic.Model{ "websecure@internal": { Middlewares: []string{"test"}, TLS: &dynamic.RouterTLSConfig{}, }, }, }, }, }, { desc: "with TCP model, two entry points", input: dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ "test": { EntryPoints: []string{"websecure", "web"}, }, "test2": { EntryPoints: []string{"web"}, RuleSyntax: "barfoo", }, }, Middlewares: make(map[string]*dynamic.TCPMiddleware), Services: make(map[string]*dynamic.TCPService), Models: map[string]*dynamic.TCPModel{ "websecure@internal": { DefaultRuleSyntax: "foobar", }, }, }, }, expected: dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ "test": { EntryPoints: []string{"websecure", "web"}, RuleSyntax: "foobar", }, "test2": { EntryPoints: []string{"web"}, RuleSyntax: "barfoo", }, }, Middlewares: make(map[string]*dynamic.TCPMiddleware), Services: make(map[string]*dynamic.TCPService), Models: map[string]*dynamic.TCPModel{ "websecure@internal": { DefaultRuleSyntax: "foobar", }, }, }, }, }, { desc: "child router with parentRefs, parent not split", input: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "parent": { EntryPoints: []string{"web"}, }, "child": { ParentRefs: []string{"parent"}, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: make(map[string]*dynamic.Model), }, }, expected: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "parent": { EntryPoints: []string{"web"}, Observability: &dynamic.RouterObservabilityConfig{ AccessLogs: new(true), Metrics: new(true), Tracing: new(true), TraceVerbosity: otypes.MinimalVerbosity, }, }, "child": { ParentRefs: []string{"parent"}, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: make(map[string]*dynamic.Model), }, }, }, { desc: "child router with parentRefs, parent split by model", input: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "parent": { EntryPoints: []string{"websecure", "web"}, }, "child": { ParentRefs: []string{"parent"}, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: map[string]*dynamic.Model{ "websecure@internal": { Middlewares: []string{"test"}, TLS: &dynamic.RouterTLSConfig{}, }, }, }, }, expected: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "parent": { EntryPoints: []string{"web"}, Observability: &dynamic.RouterObservabilityConfig{ AccessLogs: new(true), Metrics: new(true), Tracing: new(true), TraceVerbosity: otypes.MinimalVerbosity, }, }, "websecure-parent": { EntryPoints: []string{"websecure"}, Middlewares: []string{"test"}, TLS: &dynamic.RouterTLSConfig{}, Observability: &dynamic.RouterObservabilityConfig{ AccessLogs: new(true), Metrics: new(true), Tracing: new(true), TraceVerbosity: otypes.MinimalVerbosity, }, }, "child": { ParentRefs: []string{"parent", "websecure-parent"}, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: map[string]*dynamic.Model{ "websecure@internal": { Middlewares: []string{"test"}, TLS: &dynamic.RouterTLSConfig{}, }, }, }, }, }, { desc: "multiple child routers with parentRefs, parent split by model", input: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "parent": { EntryPoints: []string{"websecure", "web"}, }, "child1": { ParentRefs: []string{"parent"}, }, "child2": { ParentRefs: []string{"parent"}, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: map[string]*dynamic.Model{ "websecure@internal": { Middlewares: []string{"auth"}, }, }, }, }, expected: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "parent": { EntryPoints: []string{"web"}, Observability: &dynamic.RouterObservabilityConfig{ AccessLogs: new(true), Metrics: new(true), Tracing: new(true), TraceVerbosity: otypes.MinimalVerbosity, }, }, "websecure-parent": { EntryPoints: []string{"websecure"}, Middlewares: []string{"auth"}, Observability: &dynamic.RouterObservabilityConfig{ AccessLogs: new(true), Metrics: new(true), Tracing: new(true), TraceVerbosity: otypes.MinimalVerbosity, }, }, "child1": { ParentRefs: []string{"parent", "websecure-parent"}, }, "child2": { ParentRefs: []string{"parent", "websecure-parent"}, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: map[string]*dynamic.Model{ "websecure@internal": { Middlewares: []string{"auth"}, }, }, }, }, }, { desc: "child router with parentRefs to non-existing parent", input: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "child": { ParentRefs: []string{"nonexistent"}, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: make(map[string]*dynamic.Model), }, }, expected: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "child": { ParentRefs: []string{"nonexistent"}, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: make(map[string]*dynamic.Model), }, }, }, { desc: "child router with multiple parentRefs, some split", input: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "parent1": { EntryPoints: []string{"websecure", "web"}, }, "parent2": { EntryPoints: []string{"web"}, }, "child": { ParentRefs: []string{"parent1", "parent2"}, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: map[string]*dynamic.Model{ "websecure@internal": { TLS: &dynamic.RouterTLSConfig{}, }, }, }, }, expected: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "parent1": { EntryPoints: []string{"web"}, Observability: &dynamic.RouterObservabilityConfig{ AccessLogs: new(true), Metrics: new(true), Tracing: new(true), TraceVerbosity: otypes.MinimalVerbosity, }, }, "websecure-parent1": { EntryPoints: []string{"websecure"}, TLS: &dynamic.RouterTLSConfig{}, Observability: &dynamic.RouterObservabilityConfig{ AccessLogs: new(true), Metrics: new(true), Tracing: new(true), TraceVerbosity: otypes.MinimalVerbosity, }, }, "parent2": { EntryPoints: []string{"web"}, Observability: &dynamic.RouterObservabilityConfig{ AccessLogs: new(true), Metrics: new(true), Tracing: new(true), TraceVerbosity: otypes.MinimalVerbosity, }, }, "child": { ParentRefs: []string{"parent1", "websecure-parent1", "parent2"}, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: map[string]*dynamic.Model{ "websecure@internal": { TLS: &dynamic.RouterTLSConfig{}, }, }, }, }, }, { desc: "child router with multiple parentRefs, all split", input: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "parent1": { EntryPoints: []string{"websecure", "web"}, }, "parent2": { EntryPoints: []string{"web"}, }, "child": { ParentRefs: []string{"parent1", "parent2"}, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: map[string]*dynamic.Model{ "web@internal": { TLS: &dynamic.RouterTLSConfig{}, }, "websecure@internal": { TLS: &dynamic.RouterTLSConfig{}, }, }, }, }, expected: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "web-parent1": { EntryPoints: []string{"web"}, TLS: &dynamic.RouterTLSConfig{}, Observability: &dynamic.RouterObservabilityConfig{ AccessLogs: new(true), Metrics: new(true), Tracing: new(true), TraceVerbosity: otypes.MinimalVerbosity, }, }, "websecure-parent1": { EntryPoints: []string{"websecure"}, TLS: &dynamic.RouterTLSConfig{}, Observability: &dynamic.RouterObservabilityConfig{ AccessLogs: new(true), Metrics: new(true), Tracing: new(true), TraceVerbosity: otypes.MinimalVerbosity, }, }, "parent2": { EntryPoints: []string{"web"}, TLS: &dynamic.RouterTLSConfig{}, Observability: &dynamic.RouterObservabilityConfig{ AccessLogs: new(true), Metrics: new(true), Tracing: new(true), TraceVerbosity: otypes.MinimalVerbosity, }, }, "child": { ParentRefs: []string{"websecure-parent1", "web-parent1", "parent2"}, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: map[string]*dynamic.Model{ "web@internal": { TLS: &dynamic.RouterTLSConfig{}, }, "websecure@internal": { TLS: &dynamic.RouterTLSConfig{}, }, }, }, }, }, { desc: "child router with parentRefs, parent split into three routers", input: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "parent": { EntryPoints: []string{"websecure", "web", "admin"}, }, "child": { ParentRefs: []string{"parent"}, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: map[string]*dynamic.Model{ "websecure@internal": { TLS: &dynamic.RouterTLSConfig{}, }, "admin@internal": { Middlewares: []string{"admin-auth"}, }, }, }, }, expected: dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ "parent": { EntryPoints: []string{"web"}, Observability: &dynamic.RouterObservabilityConfig{ AccessLogs: new(true), Metrics: new(true), Tracing: new(true), TraceVerbosity: otypes.MinimalVerbosity, }, }, "websecure-parent": { EntryPoints: []string{"websecure"}, TLS: &dynamic.RouterTLSConfig{}, Observability: &dynamic.RouterObservabilityConfig{ AccessLogs: new(true), Metrics: new(true), Tracing: new(true), TraceVerbosity: otypes.MinimalVerbosity, }, }, "admin-parent": { EntryPoints: []string{"admin"}, Middlewares: []string{"admin-auth"}, Observability: &dynamic.RouterObservabilityConfig{ AccessLogs: new(true), Metrics: new(true), Tracing: new(true), TraceVerbosity: otypes.MinimalVerbosity, }, }, "child": { ParentRefs: []string{"parent", "websecure-parent", "admin-parent"}, }, }, Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), Models: map[string]*dynamic.Model{ "websecure@internal": { TLS: &dynamic.RouterTLSConfig{}, }, "admin@internal": { Middlewares: []string{"admin-auth"}, }, }, }, }, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { t.Parallel() actual := applyModel(test.input) assert.Equal(t, test.expected, actual) }) } } func Test_resolveHTTPTLSOptions(t *testing.T) { testCases := []struct { desc string strictTLSOptions bool routers map[string]*dynamic.Router expected map[string]string // router name -> ResolvedOptions conflicting []string // router names expected to be flagged as conflicting unexpectedRouters []string }{ { desc: "same host, different options, different entryPoints: no conflict", routers: map[string]*dynamic.Router{ "router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}}, "router-b@file": {EntryPoints: []string{"ep-b"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsB"}}, }, expected: map[string]string{ "router-a@file": "optsA@file", "router-b@file": "optsB@file", }, }, { desc: "same host, different options, same entryPoint: conflict falls back to default", routers: map[string]*dynamic.Router{ "router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}}, "router-b@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsB"}}, }, expected: map[string]string{ "ep-a-conflicted-router-a@file": "default", "ep-a-conflicted-router-b@file": "default", }, unexpectedRouters: []string{"router-a@file", "router-b@file"}, }, { desc: "same host, same options, same entryPoint: keeps the configured options", routers: map[string]*dynamic.Router{ "router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}}, "router-b@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`) && PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}}, }, expected: map[string]string{ "router-a@file": "optsA@file", "router-b@file": "optsA@file", }, }, { desc: "router spanning two entryPoints, conflict on one only: router is duplicated", routers: map[string]*dynamic.Router{ "shared@file": {EntryPoints: []string{"ep-a", "ep-b"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsX"}}, "other@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsY"}}, }, expected: map[string]string{ "ep-a-conflicted-shared@file": "default", // conflicts with other@file on ep-a "shared@file": "optsX@file", // alone on ep-b "ep-a-conflicted-other@file": "default", }, unexpectedRouters: []string{"other@file"}, }, { desc: "no domain in rule, non-default options: forced to default and renamed", routers: map[string]*dynamic.Router{ "router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}}, }, expected: map[string]string{ "ep-a-conflicted-router-a@file": "default", }, unexpectedRouters: []string{"router-a@file"}, }, { desc: "no domain in rule, implicit default options: not conflicting, keeps its name", routers: map[string]*dynamic.Router{ "router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{}}, }, expected: map[string]string{ "router-a@file": "default", }, unexpectedRouters: []string{"ep-a-conflicted-router-a@file"}, }, { desc: "strict: same host, different options, different entryPoints: no conflict", strictTLSOptions: true, routers: map[string]*dynamic.Router{ "router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}}, "router-b@file": {EntryPoints: []string{"ep-b"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsB"}}, }, expected: map[string]string{ "router-a@file": "optsA@file", "router-b@file": "optsB@file", }, }, { desc: "strict: same host, different options, same entryPoint: conflict is not arbitrated", strictTLSOptions: true, routers: map[string]*dynamic.Router{ "router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}}, "router-b@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsB"}}, }, expected: map[string]string{ "ep-a-conflicted-router-a@file": "optsA@file", "ep-a-conflicted-router-b@file": "optsB@file", }, conflicting: []string{"ep-a-conflicted-router-a@file", "ep-a-conflicted-router-b@file"}, unexpectedRouters: []string{"router-a@file", "router-b@file"}, }, { desc: "strict: router spanning two entryPoints, conflict on one only: router is duplicated", strictTLSOptions: true, routers: map[string]*dynamic.Router{ "shared@file": {EntryPoints: []string{"ep-a", "ep-b"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsX"}}, "other@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsY"}}, }, expected: map[string]string{ "ep-a-conflicted-shared@file": "optsX@file", "shared@file": "optsX@file", // alone on ep-b "ep-a-conflicted-other@file": "optsY@file", }, conflicting: []string{"ep-a-conflicted-shared@file", "ep-a-conflicted-other@file"}, unexpectedRouters: []string{"other@file"}, }, { desc: "strict: no domain in rule, non-default options: flagged as conflicting", strictTLSOptions: true, routers: map[string]*dynamic.Router{ "router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}}, }, expected: map[string]string{ "ep-a-conflicted-router-a@file": "optsA@file", }, conflicting: []string{"ep-a-conflicted-router-a@file"}, unexpectedRouters: []string{"router-a@file"}, }, { desc: "strict: same host, same options, same entryPoint: keeps the configured options", strictTLSOptions: true, routers: map[string]*dynamic.Router{ "router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}}, "router-b@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`) && PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}}, }, expected: map[string]string{ "router-a@file": "optsA@file", "router-b@file": "optsA@file", }, }, { desc: "no domain in rule, explicit default options: not conflicting, keeps its name", routers: map[string]*dynamic.Router{ "router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{ Options: "default", }}, }, expected: map[string]string{ "router-a@file": "default", }, unexpectedRouters: []string{"ep-a-conflicted-router-a@file"}, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { t.Parallel() got := resolveHTTPTLSOptions(test.routers, test.strictTLSOptions) for name, want := range test.expected { rt, ok := got[name] require.True(t, ok, "router %q is missing", name) require.NotNil(t, rt.TLS, "router %q has no TLS config", name) assert.Equal(t, want, rt.TLS.ResolvedOptions, "router %q %v", name, rt.EntryPoints) assert.Equal(t, slices.Contains(test.conflicting, name), rt.TLS.ConflictingOptions, "router %q", name) } for _, name := range test.unexpectedRouters { _, ok := got[name] require.False(t, ok, "router %q is present", name) } }) } }