From 85cd5485b75a303baf17de20688ff368b6ab9748 Mon Sep 17 00:00:00 2001 From: Julien Salleyron Date: Mon, 26 Jan 2026 10:28:04 +0100 Subject: [PATCH] Avoid recursion with services --- pkg/metrics/prometheus_test.go | 24 +++--- pkg/server/configurationwatcher_test.go | 92 +++++++++++++++++------ pkg/server/middleware/middlewares.go | 22 +----- pkg/server/middleware/middlewares_test.go | 11 +-- pkg/server/recursion/recursion.go | 26 +++++++ pkg/server/routerfactory_test.go | 78 ++++++++++++++++--- pkg/server/service/service.go | 7 ++ pkg/testhelpers/config.go | 70 ++++++++++++++--- 8 files changed, 249 insertions(+), 81 deletions(-) create mode 100644 pkg/server/recursion/recursion.go diff --git a/pkg/metrics/prometheus_test.go b/pkg/metrics/prometheus_test.go index 953acfd62..39992e585 100644 --- a/pkg/metrics/prometheus_test.go +++ b/pkg/metrics/prometheus_test.go @@ -451,13 +451,13 @@ func TestPrometheusMetricRemoval(t *testing.T) { th.WithRouter("foo@providerName", th.WithServiceName("bar")), th.WithRouter("router2", th.WithServiceName("bar@providerName")), ), - th.WithLoadBalancerServices( - th.WithService("bar@providerName", th.WithServers( + th.WithServices( + th.WithService("bar@providerName", th.WithServiceServersLoadBalancer(th.WithServers( th.WithServer("http://localhost:9000"), th.WithServer("http://localhost:9999"), th.WithServer("http://localhost:9998"), - )), - th.WithService("service1", th.WithServers(th.WithServer("http://localhost:9000"))), + ))), + th.WithService("service1", th.WithServiceServersLoadBalancer(th.WithServers(th.WithServer("http://localhost:9000")))), ), ), } @@ -467,8 +467,8 @@ func TestPrometheusMetricRemoval(t *testing.T) { th.WithRouters( th.WithRouter("foo@providerName", th.WithServiceName("bar")), ), - th.WithLoadBalancerServices( - th.WithService("bar@providerName", th.WithServers(th.WithServer("http://localhost:9000"))), + th.WithServices( + th.WithService("bar@providerName", th.WithServiceServersLoadBalancer(th.WithServers(th.WithServer("http://localhost:9000")))), ), ), } @@ -538,8 +538,8 @@ func TestPrometheusMetricRemoveEndpointForRecoveredService(t *testing.T) { conf1 := dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithLoadBalancerServices( - th.WithService("service1", th.WithServers(th.WithServer("http://localhost:9000"))), + th.WithServices( + th.WithService("service1", th.WithServiceServersLoadBalancer(th.WithServers(th.WithServer("http://localhost:9000")))), ), ), } @@ -550,8 +550,8 @@ func TestPrometheusMetricRemoveEndpointForRecoveredService(t *testing.T) { conf3 := dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithLoadBalancerServices( - th.WithService("service1", th.WithServers(th.WithServer("http://localhost:9001"))), + th.WithServices( + th.WithService("service1", th.WithServiceServersLoadBalancer(th.WithServers(th.WithServer("http://localhost:9001")))), ), ), } @@ -577,8 +577,8 @@ func TestPrometheusRemovedMetricsReset(t *testing.T) { conf1 := dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithLoadBalancerServices(th.WithService("service", - th.WithServers(th.WithServer("http://localhost:9000"))), + th.WithServices( + th.WithService("service", th.WithServiceServersLoadBalancer(th.WithServers(th.WithServer("http://localhost:9000")))), ), ), } diff --git a/pkg/server/configurationwatcher_test.go b/pkg/server/configurationwatcher_test.go index 64019c191..3e6c32fd7 100644 --- a/pkg/server/configurationwatcher_test.go +++ b/pkg/server/configurationwatcher_test.go @@ -86,7 +86,7 @@ func TestNewConfigurationWatcher(t *testing.T) { th.WithEntryPoints("e"), th.WithServiceName("scv"))), th.WithMiddlewares(), - th.WithLoadBalancerServices(), + th.WithServices(), ), TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{}, @@ -123,7 +123,9 @@ func TestWaitForRequiredProvider(t *testing.T) { config := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer()), + ), ), } @@ -167,14 +169,18 @@ func TestIgnoreTransientConfiguration(t *testing.T) { config := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer()), + ), ), } expectedConfig := dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar@mock")), + th.WithServices( + th.WithService("bar@mock", th.WithServiceServersLoadBalancer()), + ), th.WithMiddlewares(), ), TCP: &dynamic.TCPConfiguration{ @@ -197,7 +203,9 @@ func TestIgnoreTransientConfiguration(t *testing.T) { expectedConfig3 := dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar-config3@mock")), + th.WithServices( + th.WithService("bar-config3@mock", th.WithServiceServersLoadBalancer()), + ), th.WithMiddlewares(), ), TCP: &dynamic.TCPConfiguration{ @@ -220,14 +228,18 @@ func TestIgnoreTransientConfiguration(t *testing.T) { config2 := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("baz", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("toto")), + th.WithServices( + th.WithService("toto", th.WithServiceServersLoadBalancer()), + ), ), } config3 := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar-config3")), + th.WithServices( + th.WithService("bar-config3", th.WithServiceServersLoadBalancer()), + ), ), } watcher := NewConfigurationWatcher(routinesPool, &mockProvider{}, []string{}, "") @@ -311,7 +323,9 @@ func TestListenProvidersThrottleProviderConfigReload(t *testing.T) { Configuration: &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo"+strconv.Itoa(i), th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer()), + ), ), }, }) @@ -371,7 +385,9 @@ func TestListenProvidersSkipsSameConfigurationForProvider(t *testing.T) { Configuration: &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer()), + ), ), }, } @@ -403,14 +419,18 @@ func TestListenProvidersDoesNotSkipFlappingConfiguration(t *testing.T) { configuration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer()), + ), ), } transientConfiguration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("bad", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bad")), + th.WithServices( + th.WithService("bad", th.WithServiceServersLoadBalancer()), + ), ), } @@ -442,7 +462,9 @@ func TestListenProvidersDoesNotSkipFlappingConfiguration(t *testing.T) { expected := dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar@mock")), + th.WithServices( + th.WithService("bar@mock", th.WithServiceServersLoadBalancer()), + ), th.WithMiddlewares(), ), TCP: &dynamic.TCPConfiguration{ @@ -471,14 +493,18 @@ func TestListenProvidersIgnoreSameConfig(t *testing.T) { configuration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer()), + ), ), } transientConfiguration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("bad", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bad")), + th.WithServices( + th.WithService("bad", th.WithServiceServersLoadBalancer()), + ), ), } @@ -531,7 +557,9 @@ func TestListenProvidersIgnoreSameConfig(t *testing.T) { expected := dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar@mock")), + th.WithServices( + th.WithService("bar@mock", th.WithServiceServersLoadBalancer()), + ), th.WithMiddlewares(), ), TCP: &dynamic.TCPConfiguration{ @@ -570,7 +598,9 @@ func TestApplyConfigUnderStress(t *testing.T) { case watcher.allProvidersConfigs <- dynamic.Message{ProviderName: "mock", Configuration: &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo"+strconv.Itoa(i), th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer()), + ), ), }}: } @@ -605,28 +635,36 @@ func TestListenProvidersIgnoreIntermediateConfigs(t *testing.T) { configuration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer()), + ), ), } transientConfiguration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("bad", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bad")), + th.WithServices( + th.WithService("bad", th.WithServiceServersLoadBalancer()), + ), ), } transientConfiguration2 := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("bad2", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bad2")), + th.WithServices( + th.WithService("bad2", th.WithServiceServersLoadBalancer()), + ), ), } finalConfiguration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("final", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("final")), + th.WithServices( + th.WithService("final", th.WithServiceServersLoadBalancer()), + ), ), } @@ -665,7 +703,9 @@ func TestListenProvidersIgnoreIntermediateConfigs(t *testing.T) { expected := dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("final@mock", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("final@mock")), + th.WithServices( + th.WithService("final@mock", th.WithServiceServersLoadBalancer()), + ), th.WithMiddlewares(), ), TCP: &dynamic.TCPConfiguration{ @@ -696,7 +736,9 @@ func TestListenProvidersPublishesConfigForEachProvider(t *testing.T) { configuration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer()), + ), ), } @@ -729,9 +771,9 @@ func TestListenProvidersPublishesConfigForEachProvider(t *testing.T) { th.WithRouter("foo@mock", th.WithEntryPoints("ep")), th.WithRouter("foo@mock2", th.WithEntryPoints("ep")), ), - th.WithLoadBalancerServices( - th.WithService("bar@mock"), - th.WithService("bar@mock2"), + th.WithServices( + th.WithService("bar@mock", th.WithServiceServersLoadBalancer()), + th.WithService("bar@mock2", th.WithServiceServersLoadBalancer()), ), th.WithMiddlewares(), ), diff --git a/pkg/server/middleware/middlewares.go b/pkg/server/middleware/middlewares.go index 05a4229d3..b9c72f6cd 100644 --- a/pkg/server/middleware/middlewares.go +++ b/pkg/server/middleware/middlewares.go @@ -6,8 +6,6 @@ import ( "fmt" "net/http" "reflect" - "slices" - "strings" "github.com/containous/alice" "github.com/traefik/traefik/v2/pkg/config/runtime" @@ -33,12 +31,7 @@ import ( "github.com/traefik/traefik/v2/pkg/middlewares/stripprefixregex" "github.com/traefik/traefik/v2/pkg/middlewares/tracing" "github.com/traefik/traefik/v2/pkg/server/provider" -) - -type middlewareStackType int - -const ( - middlewareStackKey middlewareStackType = iota + "github.com/traefik/traefik/v2/pkg/server/recursion" ) // Builder the middleware builder. @@ -70,7 +63,7 @@ func (b *Builder) BuildChain(ctx context.Context, middlewares []string) *alice.C } var err error - if constructorContext, err = checkRecursion(constructorContext, middlewareName); err != nil { + if constructorContext, err = recursion.CheckRecursion(constructorContext, "middleware", middlewareName); err != nil { b.configs[middlewareName].AddError(err, true) return nil, err } @@ -93,17 +86,6 @@ func (b *Builder) BuildChain(ctx context.Context, middlewares []string) *alice.C return &chain } -func checkRecursion(ctx context.Context, middlewareName string) (context.Context, error) { - currentStack, ok := ctx.Value(middlewareStackKey).([]string) - if !ok { - currentStack = []string{} - } - if slices.Contains(currentStack, middlewareName) { - return ctx, fmt.Errorf("could not instantiate middleware %s: recursion detected in %s", middlewareName, strings.Join(append(currentStack, middlewareName), "->")) - } - return context.WithValue(ctx, middlewareStackKey, append(currentStack, middlewareName)), nil -} - // it is the responsibility of the caller to make sure that b.configs[middlewareName].Middleware exists. func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (alice.Constructor, error) { config := b.configs[middlewareName] diff --git a/pkg/server/middleware/middlewares_test.go b/pkg/server/middleware/middlewares_test.go index ed4456b31..4f9d9c9ca 100644 --- a/pkg/server/middleware/middlewares_test.go +++ b/pkg/server/middleware/middlewares_test.go @@ -172,7 +172,7 @@ func TestBuilder_BuildChainWithContext(t *testing.T) { }, }, }, - expectedError: errors.New("could not instantiate middleware m1: recursion detected in m1->m2->m3->m1"), + expectedError: errors.New("could not instantiate middleware m1: recursion detected in middleware:m1->middleware:m2->middleware:m3->middleware:m1"), }, { desc: "Detects recursion in Middleware chain", @@ -197,9 +197,10 @@ func TestBuilder_BuildChainWithContext(t *testing.T) { }, }, }, - expectedError: errors.New("could not instantiate middleware m1@provider: recursion detected in m1@provider->m2@provider2->m3@provider->m1@provider"), + expectedError: errors.New("could not instantiate middleware m1@provider: recursion detected in middleware:m1@provider->middleware:m2@provider2->middleware:m3@provider->middleware:m1@provider"), }, { + desc: "Detects recursion in Middleware chain", buildChain: []string{"ok", "m0"}, configuration: map[string]*dynamic.Middleware{ "ok": { @@ -211,7 +212,7 @@ func TestBuilder_BuildChainWithContext(t *testing.T) { }, }, }, - expectedError: errors.New("could not instantiate middleware m0: recursion detected in m0->m0"), + expectedError: errors.New("could not instantiate middleware m0: recursion detected in middleware:m0->middleware:m0"), }, { desc: "Detects MiddlewareChain that references a Chain that references a Chain with a missing middleware", @@ -238,7 +239,7 @@ func TestBuilder_BuildChainWithContext(t *testing.T) { }, }, }, - expectedError: errors.New("could not instantiate middleware m2: recursion detected in m0->m1->m2->m3->m2"), + expectedError: errors.New("could not instantiate middleware m2: recursion detected in middleware:m0->middleware:m1->middleware:m2->middleware:m3->middleware:m2"), }, { desc: "--", @@ -250,7 +251,7 @@ func TestBuilder_BuildChainWithContext(t *testing.T) { }, }, }, - expectedError: errors.New("could not instantiate middleware m0: recursion detected in m0->m0"), + expectedError: errors.New("could not instantiate middleware m0: recursion detected in middleware:m0->middleware:m0"), }, } diff --git a/pkg/server/recursion/recursion.go b/pkg/server/recursion/recursion.go new file mode 100644 index 000000000..40998a37c --- /dev/null +++ b/pkg/server/recursion/recursion.go @@ -0,0 +1,26 @@ +package recursion + +import ( + "context" + "fmt" + "slices" + "strings" +) + +type stackType int + +const ( + stackKey stackType = iota +) + +func CheckRecursion(ctx context.Context, itemType, itemName string) (context.Context, error) { + currentStack, ok := ctx.Value(stackKey).([]string) + if !ok { + currentStack = []string{} + } + name := itemType + ":" + itemName + if slices.Contains(currentStack, name) { + return ctx, fmt.Errorf("could not instantiate %s %s: recursion detected in %s", itemType, itemName, strings.Join(append(currentStack, name), "->")) + } + return context.WithValue(ctx, stackKey, append(currentStack, name)), nil +} diff --git a/pkg/server/routerfactory_test.go b/pkg/server/routerfactory_test.go index f70b62933..9dc5de135 100644 --- a/pkg/server/routerfactory_test.go +++ b/pkg/server/routerfactory_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/config/runtime" "github.com/traefik/traefik/v2/pkg/config/static" @@ -43,8 +44,8 @@ func TestReuseService(t *testing.T) { th.WithMiddlewares(th.WithMiddleware("basicauth", th.WithBasicAuth(&dynamic.BasicAuth{Users: []string{"foo:bar"}}), )), - th.WithLoadBalancerServices(th.WithService("bar", - th.WithServers(th.WithServer(testServer.URL))), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer(th.WithServers(th.WithServer(testServer.URL)))), ), ) @@ -91,8 +92,8 @@ func TestServerResponseEmptyBackend(t *testing.T) { th.WithServiceName("bar"), th.WithRule(routeRule)), ), - th.WithLoadBalancerServices(th.WithService("bar", - th.WithServers(th.WithServer(testServerURL))), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer(th.WithServers(th.WithServer(testServerURL)))), ), ) }, @@ -114,7 +115,9 @@ func TestServerResponseEmptyBackend(t *testing.T) { th.WithServiceName("bar"), th.WithRule(routeRule)), ), - th.WithLoadBalancerServices(th.WithService("bar")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer()), + ), ) }, expectedStatusCode: http.StatusServiceUnavailable, @@ -128,8 +131,8 @@ func TestServerResponseEmptyBackend(t *testing.T) { th.WithServiceName("bar"), th.WithRule(routeRule)), ), - th.WithLoadBalancerServices(th.WithService("bar", - th.WithSticky("test")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer(th.WithSticky("test"))), ), ) }, @@ -144,7 +147,9 @@ func TestServerResponseEmptyBackend(t *testing.T) { th.WithServiceName("bar"), th.WithRule(routeRule)), ), - th.WithLoadBalancerServices(th.WithService("bar")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer()), + ), ) }, expectedStatusCode: http.StatusServiceUnavailable, @@ -158,8 +163,8 @@ func TestServerResponseEmptyBackend(t *testing.T) { th.WithServiceName("bar"), th.WithRule(routeRule)), ), - th.WithLoadBalancerServices(th.WithService("bar", - th.WithSticky("test")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer(th.WithSticky("test"))), ), ) }, @@ -241,3 +246,56 @@ func TestInternalServices(t *testing.T) { assert.Equal(t, http.StatusOK, responseRecorderOk.Result().StatusCode, "status code") } + +func TestRecursionService(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + })) + defer testServer.Close() + + staticConfig := static.Configuration{ + EntryPoints: map[string]*static.EntryPoint{ + "web": {}, + }, + } + + dynamicConfigs := th.BuildConfiguration( + th.WithRouters( + th.WithRouter("foo@provider1", + th.WithEntryPoints("web"), + th.WithServiceName("bar"), + th.WithRule("Path(`/ok`)")), + ), + th.WithMiddlewares(th.WithMiddleware("customerror", + th.WithErrorPage(&dynamic.ErrorPage{Service: "bar"}), + )), + th.WithServices( + th.WithService("bar@provider1", th.WithServiceWRR(th.WithWRRServices(th.WithWRRService("foo")))), + th.WithService("foo@provider1", th.WithServiceWRR(th.WithWRRServices(th.WithWRRService("bar")))), + ), + ) + + roundTripperManager := service.NewRoundTripperManager() + roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) + managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry(), roundTripperManager, nil) + tlsManager := tls.NewManager() + + voidRegistry := metrics.NewVoidRegistry() + + factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(voidRegistry, nil, nil), nil, voidRegistry) + + rtConf := runtime.NewConfig(dynamic.Configuration{HTTP: dynamicConfigs}) + entryPointsHandlers, _ := factory.CreateRouters(rtConf) + + // Test that the /ok path returns a status 404. + responseRecorderOk := &httptest.ResponseRecorder{} + requestOk := httptest.NewRequest(http.MethodGet, testServer.URL+"/ok", nil) + entryPointsHandlers["web"].GetHTTPHandler().ServeHTTP(responseRecorderOk, requestOk) + + assert.Equal(t, http.StatusNotFound, responseRecorderOk.Result().StatusCode, "status code") + + require.NotNil(t, rtConf.Routers["foo@provider1"]) + assert.Contains(t, rtConf.Routers["foo@provider1"].Err, "could not instantiate service bar@provider1: recursion detected in service:bar@provider1->service:foo@provider1->service:bar@provider1") + require.NotNil(t, rtConf.Services["bar@provider1"]) + assert.Contains(t, rtConf.Services["bar@provider1"].Err, "could not instantiate service bar@provider1: recursion detected in service:bar@provider1->service:foo@provider1->service:bar@provider1") +} diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go index 99c611bf9..033736e2e 100644 --- a/pkg/server/service/service.go +++ b/pkg/server/service/service.go @@ -25,6 +25,7 @@ import ( "github.com/traefik/traefik/v2/pkg/safe" "github.com/traefik/traefik/v2/pkg/server/cookie" "github.com/traefik/traefik/v2/pkg/server/provider" + "github.com/traefik/traefik/v2/pkg/server/recursion" "github.com/traefik/traefik/v2/pkg/server/service/loadbalancer/failover" "github.com/traefik/traefik/v2/pkg/server/service/loadbalancer/mirror" "github.com/traefik/traefik/v2/pkg/server/service/loadbalancer/wrr" @@ -116,6 +117,12 @@ func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string) (http.H return nil, err } + var errRecursion error + if ctx, errRecursion = recursion.CheckRecursion(ctx, "service", serviceName); errRecursion != nil { + conf.AddError(errRecursion, true) + return nil, errRecursion + } + var lb http.Handler switch { diff --git a/pkg/testhelpers/config.go b/pkg/testhelpers/config.go index ab810e4f0..303934f41 100644 --- a/pkg/testhelpers/config.go +++ b/pkg/testhelpers/config.go @@ -53,30 +53,75 @@ func WithServiceName(serviceName string) func(*dynamic.Router) { } } -// WithLoadBalancerServices is a helper to create a configuration. -func WithLoadBalancerServices(opts ...func(service *dynamic.ServersLoadBalancer) string) func(*dynamic.HTTPConfiguration) { +// WithServices is a helper to create a configuration. +func WithServices(opts ...func(service *dynamic.Service) string) func(*dynamic.HTTPConfiguration) { return func(c *dynamic.HTTPConfiguration) { c.Services = make(map[string]*dynamic.Service) for _, opt := range opts { - b := &dynamic.ServersLoadBalancer{} + b := &dynamic.Service{} name := opt(b) - c.Services[name] = &dynamic.Service{ - LoadBalancer: b, - } + c.Services[name] = b } } } // WithService is a helper to create a configuration. -func WithService(name string, opts ...func(*dynamic.ServersLoadBalancer)) func(*dynamic.ServersLoadBalancer) string { - return func(r *dynamic.ServersLoadBalancer) string { +func WithService(name string, opts ...func(*dynamic.Service)) func(*dynamic.Service) string { + return func(s *dynamic.Service) string { for _, opt := range opts { - opt(r) + opt(s) } + return name } } +func WithServiceServersLoadBalancer(opts ...func(*dynamic.ServersLoadBalancer)) func(*dynamic.Service) { + return func(s *dynamic.Service) { + b := &dynamic.ServersLoadBalancer{} + b.SetDefaults() + + for _, opt := range opts { + opt(b) + } + + s.LoadBalancer = b + } +} + +func WithServiceWRR(opts ...func(*dynamic.WeightedRoundRobin)) func(*dynamic.Service) { + return func(s *dynamic.Service) { + b := &dynamic.WeightedRoundRobin{} + + for _, opt := range opts { + opt(b) + } + + s.Weighted = b + } +} + +// WithWRRServices is a helper to create a configuration. +func WithWRRServices(opts ...func(*dynamic.WRRService)) func(*dynamic.WeightedRoundRobin) { + return func(b *dynamic.WeightedRoundRobin) { + for _, opt := range opts { + service := dynamic.WRRService{} + opt(&service) + b.Services = append(b.Services, service) + } + } +} + +// WithWRRService is a helper to create a configuration. +func WithWRRService(name string, opts ...func(*dynamic.WRRService)) func(*dynamic.WRRService) { + return func(s *dynamic.WRRService) { + for _, opt := range opts { + opt(s) + } + s.Name = name + } +} + // WithMiddlewares is a helper to create a configuration. func WithMiddlewares(opts ...func(*dynamic.Middleware) string) func(*dynamic.HTTPConfiguration) { return func(c *dynamic.HTTPConfiguration) { @@ -106,6 +151,13 @@ func WithBasicAuth(auth *dynamic.BasicAuth) func(*dynamic.Middleware) { } } +// WithErrorPage is a helper to create a configuration. +func WithErrorPage(errorPage *dynamic.ErrorPage) func(*dynamic.Middleware) { + return func(r *dynamic.Middleware) { + r.Errors = errorPage + } +} + // WithEntryPoints is a helper to create a configuration. func WithEntryPoints(eps ...string) func(*dynamic.Router) { return func(f *dynamic.Router) {