traefik/pkg/server/router/router_test.go
2024-03-12 12:08:03 +01:00

859 lines
22 KiB
Go

package router
import (
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/config/runtime"
"github.com/traefik/traefik/v3/pkg/middlewares/requestdecorator"
"github.com/traefik/traefik/v3/pkg/server/middleware"
"github.com/traefik/traefik/v3/pkg/server/service"
"github.com/traefik/traefik/v3/pkg/testhelpers"
"github.com/traefik/traefik/v3/pkg/tls"
)
func TestRouterManager_Get(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
t.Cleanup(func() { server.Close() })
type expectedResult struct {
StatusCode int
RequestHeaders map[string]string
}
testCases := []struct {
desc string
routersConfig map[string]*dynamic.Router
serviceConfig map[string]*dynamic.Service
middlewaresConfig map[string]*dynamic.Middleware
entryPoints []string
expected expectedResult
}{
{
desc: "no middleware",
routersConfig: map[string]*dynamic.Router{
"foo": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`foo.bar`)",
},
},
serviceConfig: map[string]*dynamic.Service{
"foo-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: server.URL,
},
},
},
},
},
entryPoints: []string{"web"},
expected: expectedResult{StatusCode: http.StatusOK},
},
{
desc: "empty host",
routersConfig: map[string]*dynamic.Router{
"foo": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(``)",
},
},
serviceConfig: map[string]*dynamic.Service{
"foo-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: server.URL,
},
},
},
},
},
entryPoints: []string{"web"},
expected: expectedResult{StatusCode: http.StatusNotFound},
},
{
desc: "no load balancer",
routersConfig: map[string]*dynamic.Router{
"foo": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`foo.bar`)",
},
},
serviceConfig: map[string]*dynamic.Service{
"foo-service": {},
},
entryPoints: []string{"web"},
expected: expectedResult{StatusCode: http.StatusNotFound},
},
{
desc: "no middleware, no matching",
routersConfig: map[string]*dynamic.Router{
"foo": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`bar.bar`)",
},
},
serviceConfig: map[string]*dynamic.Service{
"foo-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: server.URL,
},
},
},
},
},
entryPoints: []string{"web"},
expected: expectedResult{StatusCode: http.StatusNotFound},
},
{
desc: "middleware: headers > auth",
routersConfig: map[string]*dynamic.Router{
"foo": {
EntryPoints: []string{"web"},
Middlewares: []string{"headers-middle", "auth-middle"},
Service: "foo-service",
Rule: "Host(`foo.bar`)",
},
},
serviceConfig: map[string]*dynamic.Service{
"foo-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: server.URL,
},
},
},
},
},
middlewaresConfig: map[string]*dynamic.Middleware{
"auth-middle": {
BasicAuth: &dynamic.BasicAuth{
Users: []string{"toto:titi"},
},
},
"headers-middle": {
Headers: &dynamic.Headers{
CustomRequestHeaders: map[string]string{"X-Apero": "beer"},
},
},
},
entryPoints: []string{"web"},
expected: expectedResult{
StatusCode: http.StatusUnauthorized,
RequestHeaders: map[string]string{
"X-Apero": "beer",
},
},
},
{
desc: "middleware: auth > header",
routersConfig: map[string]*dynamic.Router{
"foo": {
EntryPoints: []string{"web"},
Middlewares: []string{"auth-middle", "headers-middle"},
Service: "foo-service",
Rule: "Host(`foo.bar`)",
},
},
serviceConfig: map[string]*dynamic.Service{
"foo-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: server.URL,
},
},
},
},
},
middlewaresConfig: map[string]*dynamic.Middleware{
"auth-middle": {
BasicAuth: &dynamic.BasicAuth{
Users: []string{"toto:titi"},
},
},
"headers-middle": {
Headers: &dynamic.Headers{
CustomRequestHeaders: map[string]string{"X-Apero": "beer"},
},
},
},
entryPoints: []string{"web"},
expected: expectedResult{
StatusCode: http.StatusUnauthorized,
RequestHeaders: map[string]string{
"X-Apero": "",
},
},
},
{
desc: "no middleware with provider name",
routersConfig: map[string]*dynamic.Router{
"foo@provider-1": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`foo.bar`)",
},
},
serviceConfig: map[string]*dynamic.Service{
"foo-service@provider-1": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: server.URL,
},
},
},
},
},
entryPoints: []string{"web"},
expected: expectedResult{StatusCode: http.StatusOK},
},
{
desc: "no middleware with specified provider name",
routersConfig: map[string]*dynamic.Router{
"foo@provider-1": {
EntryPoints: []string{"web"},
Service: "foo-service@provider-2",
Rule: "Host(`foo.bar`)",
},
},
serviceConfig: map[string]*dynamic.Service{
"foo-service@provider-2": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: server.URL,
},
},
},
},
},
entryPoints: []string{"web"},
expected: expectedResult{StatusCode: http.StatusOK},
},
{
desc: "middleware: chain with provider name",
routersConfig: map[string]*dynamic.Router{
"foo@provider-1": {
EntryPoints: []string{"web"},
Middlewares: []string{"chain-middle@provider-2", "headers-middle"},
Service: "foo-service",
Rule: "Host(`foo.bar`)",
},
},
serviceConfig: map[string]*dynamic.Service{
"foo-service@provider-1": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: server.URL,
},
},
},
},
},
middlewaresConfig: map[string]*dynamic.Middleware{
"chain-middle@provider-2": {
Chain: &dynamic.Chain{Middlewares: []string{"auth-middle"}},
},
"auth-middle@provider-2": {
BasicAuth: &dynamic.BasicAuth{
Users: []string{"toto:titi"},
},
},
"headers-middle@provider-1": {
Headers: &dynamic.Headers{
CustomRequestHeaders: map[string]string{"X-Apero": "beer"},
},
},
},
entryPoints: []string{"web"},
expected: expectedResult{
StatusCode: http.StatusUnauthorized,
RequestHeaders: map[string]string{
"X-Apero": "",
},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
rtConf := runtime.NewConfig(dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Services: test.serviceConfig,
Routers: test.routersConfig,
Middlewares: test.middlewaresConfig,
},
})
roundTripperManager := service.NewRoundTripperManager(nil)
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager)
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
tlsManager := tls.NewManager()
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager)
handlers := routerManager.BuildHandlers(context.Background(), test.entryPoints, false)
w := httptest.NewRecorder()
req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)
reqHost := requestdecorator.New(nil)
reqHost.ServeHTTP(w, req, handlers["web"].ServeHTTP)
assert.Equal(t, test.expected.StatusCode, w.Code)
for key, value := range test.expected.RequestHeaders {
assert.Equal(t, value, req.Header.Get(key))
}
})
}
}
func TestRuntimeConfiguration(t *testing.T) {
testCases := []struct {
desc string
serviceConfig map[string]*dynamic.Service
routerConfig map[string]*dynamic.Router
middlewareConfig map[string]*dynamic.Middleware
tlsOptions map[string]tls.Options
expectedError int
}{
{
desc: "No error",
serviceConfig: map[string]*dynamic.Service{
"foo-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.1:8085",
},
{
URL: "http://127.0.0.1:8086",
},
},
HealthCheck: &dynamic.ServerHealthCheck{
Interval: ptypes.Duration(500 * time.Millisecond),
Path: "/health",
},
},
},
},
routerConfig: map[string]*dynamic.Router{
"foo": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`bar.foo`)",
},
"bar": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`foo.bar`)",
},
},
expectedError: 0,
},
{
desc: "One router with wrong rule",
serviceConfig: map[string]*dynamic.Service{
"foo-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.1",
},
},
},
},
},
routerConfig: map[string]*dynamic.Router{
"foo": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "WrongRule(`bar.foo`)",
},
"bar": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`foo.bar`)",
},
},
expectedError: 1,
},
{
desc: "All router with wrong rule",
serviceConfig: map[string]*dynamic.Service{
"foo-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.1",
},
},
},
},
},
routerConfig: map[string]*dynamic.Router{
"foo": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "WrongRule(`bar.foo`)",
},
"bar": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "WrongRule(`foo.bar`)",
},
},
expectedError: 2,
},
{
desc: "Router with unknown service",
serviceConfig: map[string]*dynamic.Service{
"foo-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.1",
},
},
},
},
},
routerConfig: map[string]*dynamic.Router{
"foo": {
EntryPoints: []string{"web"},
Service: "wrong-service",
Rule: "Host(`bar.foo`)",
},
"bar": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`foo.bar`)",
},
},
expectedError: 1,
},
{
desc: "Router with broken service",
serviceConfig: map[string]*dynamic.Service{
"foo-service": {
LoadBalancer: nil,
},
},
routerConfig: map[string]*dynamic.Router{
"bar": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`foo.bar`)",
},
},
expectedError: 2,
},
{
desc: "Router with middleware",
serviceConfig: map[string]*dynamic.Service{
"foo-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.1",
},
},
},
},
},
middlewareConfig: map[string]*dynamic.Middleware{
"auth": {
BasicAuth: &dynamic.BasicAuth{
Users: []string{"admin:admin"},
},
},
"addPrefixTest": {
AddPrefix: &dynamic.AddPrefix{
Prefix: "/toto",
},
},
},
routerConfig: map[string]*dynamic.Router{
"bar": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`foo.bar`)",
Middlewares: []string{"auth", "addPrefixTest"},
},
"test": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`foo.bar.other`)",
Middlewares: []string{"addPrefixTest", "auth"},
},
},
},
{
desc: "Router with unknown middleware",
serviceConfig: map[string]*dynamic.Service{
"foo-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.1",
},
},
},
},
},
middlewareConfig: map[string]*dynamic.Middleware{
"auth": {
BasicAuth: &dynamic.BasicAuth{
Users: []string{"admin:admin"},
},
},
},
routerConfig: map[string]*dynamic.Router{
"bar": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`foo.bar`)",
Middlewares: []string{"unknown"},
},
},
expectedError: 1,
},
{
desc: "Router with broken middleware",
serviceConfig: map[string]*dynamic.Service{
"foo-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.1",
},
},
},
},
},
middlewareConfig: map[string]*dynamic.Middleware{
"auth": {
BasicAuth: &dynamic.BasicAuth{
Users: []string{"foo"},
},
},
},
routerConfig: map[string]*dynamic.Router{
"bar": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`foo.bar`)",
Middlewares: []string{"auth"},
},
},
expectedError: 2,
},
{
desc: "Router with broken tlsOption",
serviceConfig: map[string]*dynamic.Service{
"foo-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.1",
},
},
},
},
},
middlewareConfig: map[string]*dynamic.Middleware{},
routerConfig: map[string]*dynamic.Router{
"bar": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`foo.bar`)",
TLS: &dynamic.RouterTLSConfig{
Options: "broken-tlsOption",
},
},
},
tlsOptions: map[string]tls.Options{
"broken-tlsOption": {
ClientAuth: tls.ClientAuth{
ClientAuthType: "foobar",
},
},
},
expectedError: 1,
},
{
desc: "Router with broken default tlsOption",
serviceConfig: map[string]*dynamic.Service{
"foo-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://127.0.0.1",
},
},
},
},
},
middlewareConfig: map[string]*dynamic.Middleware{},
routerConfig: map[string]*dynamic.Router{
"bar": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`foo.bar`)",
TLS: &dynamic.RouterTLSConfig{},
},
},
tlsOptions: map[string]tls.Options{
"default": {
ClientAuth: tls.ClientAuth{
ClientAuthType: "foobar",
},
},
},
expectedError: 1,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
entryPoints := []string{"web"}
rtConf := runtime.NewConfig(dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Services: test.serviceConfig,
Routers: test.routerConfig,
Middlewares: test.middlewareConfig,
},
TLS: &dynamic.TLSConfiguration{
Options: test.tlsOptions,
},
})
roundTripperManager := service.NewRoundTripperManager(nil)
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager)
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
tlsManager := tls.NewManager()
tlsManager.UpdateConfigs(context.Background(), nil, test.tlsOptions, nil)
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager)
_ = routerManager.BuildHandlers(context.Background(), entryPoints, false)
_ = routerManager.BuildHandlers(context.Background(), entryPoints, true)
// even though rtConf was passed by argument to the manager builders above,
// it's ok to use it as the result we check, because everything worth checking
// can be accessed by pointers in it.
var allErrors int
for _, v := range rtConf.Services {
if v.Err != nil {
allErrors++
}
}
for _, v := range rtConf.Routers {
if len(v.Err) > 0 {
allErrors++
}
}
for _, v := range rtConf.Middlewares {
if v.Err != nil {
allErrors++
}
}
assert.Equal(t, test.expectedError, allErrors)
})
}
}
func TestProviderOnMiddlewares(t *testing.T) {
entryPoints := []string{"web"}
rtConf := runtime.NewConfig(dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Services: map[string]*dynamic.Service{
"test@file": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{},
},
},
},
Routers: map[string]*dynamic.Router{
"router@file": {
EntryPoints: []string{"web"},
Rule: "Host(`test`)",
Service: "test@file",
Middlewares: []string{"chain@file", "m1"},
},
"router@docker": {
EntryPoints: []string{"web"},
Rule: "Host(`test`)",
Service: "test@file",
Middlewares: []string{"chain", "m1@file"},
},
},
Middlewares: map[string]*dynamic.Middleware{
"chain@file": {
Chain: &dynamic.Chain{Middlewares: []string{"m1", "m2", "m1@file"}},
},
"chain@docker": {
Chain: &dynamic.Chain{Middlewares: []string{"m1", "m2", "m1@file"}},
},
"m1@file": {AddPrefix: &dynamic.AddPrefix{Prefix: "/m1"}},
"m2@file": {AddPrefix: &dynamic.AddPrefix{Prefix: "/m2"}},
"m1@docker": {AddPrefix: &dynamic.AddPrefix{Prefix: "/m1"}},
"m2@docker": {AddPrefix: &dynamic.AddPrefix{Prefix: "/m2"}},
},
},
})
roundTripperManager := service.NewRoundTripperManager(nil)
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager)
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
tlsManager := tls.NewManager()
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager)
_ = routerManager.BuildHandlers(context.Background(), entryPoints, false)
assert.Equal(t, []string{"chain@file", "m1@file"}, rtConf.Routers["router@file"].Middlewares)
assert.Equal(t, []string{"m1@file", "m2@file", "m1@file"}, rtConf.Middlewares["chain@file"].Chain.Middlewares)
assert.Equal(t, []string{"chain@docker", "m1@file"}, rtConf.Routers["router@docker"].Middlewares)
assert.Equal(t, []string{"m1@docker", "m2@docker", "m1@file"}, rtConf.Middlewares["chain@docker"].Chain.Middlewares)
}
type staticRoundTripperGetter struct {
res *http.Response
}
func (s staticRoundTripperGetter) Get(name string) (http.RoundTripper, error) {
return &staticTransport{res: s.res}, nil
}
type staticTransport struct {
res *http.Response
}
func (t *staticTransport) RoundTrip(_ *http.Request) (*http.Response, error) {
return t.res, nil
}
func BenchmarkRouterServe(b *testing.B) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
b.Cleanup(func() { server.Close() })
res := &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader("")),
}
routersConfig := map[string]*dynamic.Router{
"foo": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`foo.bar`) && Path(`/`)",
},
}
serviceConfig := map[string]*dynamic.Service{
"foo-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: server.URL,
},
},
},
},
}
entryPoints := []string{"web"}
rtConf := runtime.NewConfig(dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Services: serviceConfig,
Routers: routersConfig,
Middlewares: map[string]*dynamic.Middleware{},
},
})
serviceManager := service.NewManager(rtConf.Services, nil, nil, staticRoundTripperGetter{res})
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
tlsManager := tls.NewManager()
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager)
handlers := routerManager.BuildHandlers(context.Background(), entryPoints, false)
w := httptest.NewRecorder()
req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)
reqHost := requestdecorator.New(nil)
b.ReportAllocs()
for range b.N {
reqHost.ServeHTTP(w, req, handlers["web"].ServeHTTP)
}
}
func BenchmarkService(b *testing.B) {
res := &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader("")),
}
serviceConfig := map[string]*dynamic.Service{
"foo-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "tchouck",
},
},
},
},
}
rtConf := runtime.NewConfig(dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Services: serviceConfig,
},
})
serviceManager := service.NewManager(rtConf.Services, nil, nil, staticRoundTripperGetter{res})
w := httptest.NewRecorder()
req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)
handler, _ := serviceManager.BuildHTTP(context.Background(), "foo-service")
b.ReportAllocs()
for range b.N {
handler.ServeHTTP(w, req)
}
}