From d2717500623bbeb35fcb4d425d209a4ddc321aaa Mon Sep 17 00:00:00 2001 From: Julien Salleyron Date: Wed, 12 Nov 2025 14:13:39 +0100 Subject: [PATCH] Fix multi-layer routing with models --- pkg/server/aggregator.go | 24 ++ pkg/server/aggregator_test.go | 408 ++++++++++++++++++++++++++++++++++ 2 files changed, 432 insertions(+) diff --git a/pkg/server/aggregator.go b/pkg/server/aggregator.go index 49f311a84..dc342ad3a 100644 --- a/pkg/server/aggregator.go +++ b/pkg/server/aggregator.go @@ -175,9 +175,11 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration { if cfg.HTTP != nil && len(cfg.HTTP.Models) > 0 { rts := make(map[string]*dynamic.Router) + modelRouterNames := make(map[string][]string) for name, rt := range cfg.HTTP.Routers { // Only root routers can have models applied. if rt.ParentRefs != nil { + rts[name] = rt continue } @@ -233,7 +235,9 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration { rtName := name if len(eps) > 1 { rtName = epName + "-" + name + modelRouterNames[name] = append(modelRouterNames[name], rtName) } + rts[rtName] = cp } else { router.EntryPoints = append(router.EntryPoints, epName) @@ -243,6 +247,26 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration { } } + for _, rt := range rts { + if rt.ParentRefs == nil { + continue + } + + var parentRefs []string + for _, ref := range rt.ParentRefs { + // Only add the initial parent ref if it still exists. + if _, ok := rts[ref]; ok { + parentRefs = append(parentRefs, ref) + } + + if names, ok := modelRouterNames[ref]; ok { + parentRefs = append(parentRefs, names...) + } + } + + rt.ParentRefs = parentRefs + } + cfg.HTTP.Routers = rts } diff --git a/pkg/server/aggregator_test.go b/pkg/server/aggregator_test.go index e33ef7ae1..8d78447d2 100644 --- a/pkg/server/aggregator_test.go +++ b/pkg/server/aggregator_test.go @@ -810,6 +810,414 @@ func Test_applyModel(t *testing.T) { }, }, }, + { + 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: pointer(true), + Metrics: pointer(true), + Tracing: pointer(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: pointer(true), + Metrics: pointer(true), + Tracing: pointer(true), + TraceVerbosity: otypes.MinimalVerbosity, + }, + }, + "websecure-parent": { + EntryPoints: []string{"websecure"}, + Middlewares: []string{"test"}, + TLS: &dynamic.RouterTLSConfig{}, + Observability: &dynamic.RouterObservabilityConfig{ + AccessLogs: pointer(true), + Metrics: pointer(true), + Tracing: pointer(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: pointer(true), + Metrics: pointer(true), + Tracing: pointer(true), + TraceVerbosity: otypes.MinimalVerbosity, + }, + }, + "websecure-parent": { + EntryPoints: []string{"websecure"}, + Middlewares: []string{"auth"}, + Observability: &dynamic.RouterObservabilityConfig{ + AccessLogs: pointer(true), + Metrics: pointer(true), + Tracing: pointer(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: pointer(true), + Metrics: pointer(true), + Tracing: pointer(true), + TraceVerbosity: otypes.MinimalVerbosity, + }, + }, + "websecure-parent1": { + EntryPoints: []string{"websecure"}, + TLS: &dynamic.RouterTLSConfig{}, + Observability: &dynamic.RouterObservabilityConfig{ + AccessLogs: pointer(true), + Metrics: pointer(true), + Tracing: pointer(true), + TraceVerbosity: otypes.MinimalVerbosity, + }, + }, + "parent2": { + EntryPoints: []string{"web"}, + Observability: &dynamic.RouterObservabilityConfig{ + AccessLogs: pointer(true), + Metrics: pointer(true), + Tracing: pointer(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: pointer(true), + Metrics: pointer(true), + Tracing: pointer(true), + TraceVerbosity: otypes.MinimalVerbosity, + }, + }, + "websecure-parent1": { + EntryPoints: []string{"websecure"}, + TLS: &dynamic.RouterTLSConfig{}, + Observability: &dynamic.RouterObservabilityConfig{ + AccessLogs: pointer(true), + Metrics: pointer(true), + Tracing: pointer(true), + TraceVerbosity: otypes.MinimalVerbosity, + }, + }, + "parent2": { + EntryPoints: []string{"web"}, + TLS: &dynamic.RouterTLSConfig{}, + Observability: &dynamic.RouterObservabilityConfig{ + AccessLogs: pointer(true), + Metrics: pointer(true), + Tracing: pointer(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: pointer(true), + Metrics: pointer(true), + Tracing: pointer(true), + TraceVerbosity: otypes.MinimalVerbosity, + }, + }, + "websecure-parent": { + EntryPoints: []string{"websecure"}, + TLS: &dynamic.RouterTLSConfig{}, + Observability: &dynamic.RouterObservabilityConfig{ + AccessLogs: pointer(true), + Metrics: pointer(true), + Tracing: pointer(true), + TraceVerbosity: otypes.MinimalVerbosity, + }, + }, + "admin-parent": { + EntryPoints: []string{"admin"}, + Middlewares: []string{"admin-auth"}, + Observability: &dynamic.RouterObservabilityConfig{ + AccessLogs: pointer(true), + Metrics: pointer(true), + Tracing: pointer(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 {