mirror of
https://github.com/traefik/traefik.git
synced 2025-10-26 05:51:20 +01:00
1875 lines
49 KiB
Go
1875 lines
49 KiB
Go
package router
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"io"
|
|
"math"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/containous/alice"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
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"
|
|
httpmuxer "github.com/traefik/traefik/v3/pkg/muxer/http"
|
|
"github.com/traefik/traefik/v3/pkg/server/middleware"
|
|
"github.com/traefik/traefik/v3/pkg/server/service"
|
|
"github.com/traefik/traefik/v3/pkg/testhelpers"
|
|
traefiktls "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{
|
|
Strategy: dynamic.BalancerStrategyWRR,
|
|
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{
|
|
Strategy: dynamic.BalancerStrategyWRR,
|
|
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{
|
|
Strategy: dynamic.BalancerStrategyWRR,
|
|
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{
|
|
Strategy: dynamic.BalancerStrategyWRR,
|
|
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{
|
|
Strategy: dynamic.BalancerStrategyWRR,
|
|
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{
|
|
Strategy: dynamic.BalancerStrategyWRR,
|
|
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{
|
|
Strategy: dynamic.BalancerStrategyWRR,
|
|
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{
|
|
Strategy: dynamic.BalancerStrategyWRR,
|
|
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 {
|
|
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,
|
|
},
|
|
})
|
|
|
|
transportManager := service.NewTransportManager(nil)
|
|
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
|
|
|
serviceManager := service.NewManager(rtConf.Services, nil, nil, transportManager, proxyBuilderMock{})
|
|
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
|
|
tlsManager := traefiktls.NewManager(nil)
|
|
|
|
parser, err := httpmuxer.NewSyntaxParser()
|
|
require.NoError(t, err)
|
|
|
|
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser)
|
|
|
|
handlers := routerManager.BuildHandlers(t.Context(), 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]traefiktls.Options
|
|
expectedError int
|
|
}{
|
|
{
|
|
desc: "No error",
|
|
serviceConfig: map[string]*dynamic.Service{
|
|
"foo-service": {
|
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
Strategy: dynamic.BalancerStrategyWRR,
|
|
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{
|
|
Strategy: dynamic.BalancerStrategyWRR,
|
|
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{
|
|
Strategy: dynamic.BalancerStrategyWRR,
|
|
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{
|
|
Strategy: dynamic.BalancerStrategyWRR,
|
|
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{
|
|
Strategy: dynamic.BalancerStrategyWRR,
|
|
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{
|
|
Strategy: dynamic.BalancerStrategyWRR,
|
|
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{
|
|
Strategy: dynamic.BalancerStrategyWRR,
|
|
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 priority exceeding max user-defined priority",
|
|
serviceConfig: map[string]*dynamic.Service{
|
|
"foo-service": {
|
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
Strategy: dynamic.BalancerStrategyWRR,
|
|
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`)",
|
|
Priority: math.MaxInt,
|
|
TLS: &dynamic.RouterTLSConfig{},
|
|
},
|
|
},
|
|
tlsOptions: map[string]traefiktls.Options{},
|
|
expectedError: 1,
|
|
},
|
|
{
|
|
desc: "Router with broken tlsOption",
|
|
serviceConfig: map[string]*dynamic.Service{
|
|
"foo-service": {
|
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
Strategy: dynamic.BalancerStrategyWRR,
|
|
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]traefiktls.Options{
|
|
"broken-tlsOption": {
|
|
ClientAuth: traefiktls.ClientAuth{
|
|
ClientAuthType: "foobar",
|
|
},
|
|
},
|
|
},
|
|
expectedError: 1,
|
|
},
|
|
{
|
|
desc: "Router with broken default tlsOption",
|
|
serviceConfig: map[string]*dynamic.Service{
|
|
"foo-service": {
|
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
Strategy: dynamic.BalancerStrategyWRR,
|
|
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]traefiktls.Options{
|
|
"default": {
|
|
ClientAuth: traefiktls.ClientAuth{
|
|
ClientAuthType: "foobar",
|
|
},
|
|
},
|
|
},
|
|
expectedError: 1,
|
|
},
|
|
}
|
|
for _, test := range testCases {
|
|
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,
|
|
},
|
|
})
|
|
|
|
transportManager := service.NewTransportManager(nil)
|
|
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
|
|
|
serviceManager := service.NewManager(rtConf.Services, nil, nil, transportManager, proxyBuilderMock{})
|
|
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
|
|
tlsManager := traefiktls.NewManager(nil)
|
|
tlsManager.UpdateConfigs(t.Context(), nil, test.tlsOptions, nil)
|
|
|
|
parser, err := httpmuxer.NewSyntaxParser()
|
|
require.NoError(t, err)
|
|
|
|
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser)
|
|
|
|
_ = routerManager.BuildHandlers(t.Context(), entryPoints, false)
|
|
_ = routerManager.BuildHandlers(t.Context(), 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{
|
|
Strategy: dynamic.BalancerStrategyWRR,
|
|
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"}},
|
|
},
|
|
},
|
|
})
|
|
|
|
transportManager := service.NewTransportManager(nil)
|
|
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
|
|
|
serviceManager := service.NewManager(rtConf.Services, nil, nil, transportManager, nil)
|
|
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
|
|
tlsManager := traefiktls.NewManager(nil)
|
|
|
|
parser, err := httpmuxer.NewSyntaxParser()
|
|
require.NoError(t, err)
|
|
|
|
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser)
|
|
|
|
_ = routerManager.BuildHandlers(t.Context(), 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 staticTransportManager struct {
|
|
res *http.Response
|
|
}
|
|
|
|
func (s staticTransportManager) GetRoundTripper(_ string) (http.RoundTripper, error) {
|
|
return &staticTransport{res: s.res}, nil
|
|
}
|
|
|
|
func (s staticTransportManager) GetTLSConfig(_ string) (*tls.Config, error) {
|
|
panic("implement me")
|
|
}
|
|
|
|
func (s staticTransportManager) Get(_ string) (*dynamic.ServersTransport, error) {
|
|
panic("implement me")
|
|
}
|
|
|
|
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, staticTransportManager{res}, nil)
|
|
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
|
|
tlsManager := traefiktls.NewManager(nil)
|
|
|
|
parser, err := httpmuxer.NewSyntaxParser()
|
|
require.NoError(b, err)
|
|
|
|
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager, parser)
|
|
|
|
handlers := routerManager.BuildHandlers(b.Context(), 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: "tchouk",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
rtConf := runtime.NewConfig(dynamic.Configuration{
|
|
HTTP: &dynamic.HTTPConfiguration{
|
|
Services: serviceConfig,
|
|
},
|
|
})
|
|
|
|
serviceManager := service.NewManager(rtConf.Services, nil, nil, staticTransportManager{res}, nil)
|
|
w := httptest.NewRecorder()
|
|
req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)
|
|
|
|
handler, _ := serviceManager.BuildHTTP(b.Context(), "foo-service")
|
|
b.ReportAllocs()
|
|
for range b.N {
|
|
handler.ServeHTTP(w, req)
|
|
}
|
|
}
|
|
|
|
func TestManager_ComputeMultiLayerRouting(t *testing.T) {
|
|
testCases := []struct {
|
|
desc string
|
|
routers map[string]*dynamic.Router
|
|
expectedStatuses map[string]string
|
|
expectedChildRefs map[string][]string
|
|
expectedErrors map[string][]string
|
|
expectedErrorCounts map[string]int
|
|
}{
|
|
{
|
|
desc: "Simple router",
|
|
routers: map[string]*dynamic.Router{
|
|
"A": {
|
|
Service: "A-service",
|
|
},
|
|
},
|
|
expectedStatuses: map[string]string{
|
|
"A": runtime.StatusEnabled,
|
|
},
|
|
expectedChildRefs: map[string][]string{
|
|
"A": {},
|
|
},
|
|
},
|
|
{
|
|
// A->B1
|
|
// ->B2
|
|
desc: "Router with two children",
|
|
routers: map[string]*dynamic.Router{
|
|
"A": {},
|
|
"B1": {
|
|
ParentRefs: []string{"A"},
|
|
Service: "B1-service",
|
|
},
|
|
"B2": {
|
|
ParentRefs: []string{"A"},
|
|
Service: "B2-service",
|
|
},
|
|
},
|
|
expectedStatuses: map[string]string{
|
|
"A": runtime.StatusEnabled,
|
|
"B1": runtime.StatusEnabled,
|
|
"B2": runtime.StatusEnabled,
|
|
},
|
|
expectedChildRefs: map[string][]string{
|
|
"A": {"B1", "B2"},
|
|
"B1": nil,
|
|
"B2": nil,
|
|
},
|
|
},
|
|
{
|
|
desc: "Non-root router with TLS config",
|
|
routers: map[string]*dynamic.Router{
|
|
"A": {},
|
|
"B": {
|
|
ParentRefs: []string{"A"},
|
|
Service: "B-service",
|
|
TLS: &dynamic.RouterTLSConfig{},
|
|
},
|
|
},
|
|
expectedStatuses: map[string]string{
|
|
"A": runtime.StatusEnabled,
|
|
"B": runtime.StatusDisabled,
|
|
},
|
|
expectedChildRefs: map[string][]string{
|
|
"A": {"B"},
|
|
"B": nil,
|
|
},
|
|
expectedErrors: map[string][]string{
|
|
"B": {"non-root router cannot have TLS configuration"},
|
|
},
|
|
},
|
|
{
|
|
desc: "Non-root router with observability config",
|
|
routers: map[string]*dynamic.Router{
|
|
"A": {},
|
|
"B": {
|
|
ParentRefs: []string{"A"},
|
|
Service: "B-service",
|
|
Observability: &dynamic.RouterObservabilityConfig{},
|
|
},
|
|
},
|
|
expectedStatuses: map[string]string{
|
|
"A": runtime.StatusEnabled,
|
|
"B": runtime.StatusDisabled,
|
|
},
|
|
expectedChildRefs: map[string][]string{
|
|
"A": {"B"},
|
|
"B": nil,
|
|
},
|
|
expectedErrors: map[string][]string{
|
|
"B": {"non-root router cannot have Observability configuration"},
|
|
},
|
|
},
|
|
{
|
|
desc: "Non-root router with EntryPoints config",
|
|
routers: map[string]*dynamic.Router{
|
|
"A": {},
|
|
"B": {
|
|
ParentRefs: []string{"A"},
|
|
Service: "B-service",
|
|
EntryPoints: []string{"web"},
|
|
},
|
|
},
|
|
expectedStatuses: map[string]string{
|
|
"A": runtime.StatusEnabled,
|
|
"B": runtime.StatusDisabled,
|
|
},
|
|
expectedChildRefs: map[string][]string{
|
|
"A": {"B"},
|
|
"B": nil,
|
|
},
|
|
expectedErrors: map[string][]string{
|
|
"B": {"non-root router cannot have Entrypoints configuration"},
|
|
},
|
|
},
|
|
|
|
{
|
|
desc: "Router with non-existing parent",
|
|
routers: map[string]*dynamic.Router{
|
|
"B": {
|
|
ParentRefs: []string{"A"},
|
|
Service: "B-service",
|
|
},
|
|
},
|
|
expectedStatuses: map[string]string{
|
|
"B": runtime.StatusDisabled,
|
|
},
|
|
expectedChildRefs: map[string][]string{
|
|
"B": nil,
|
|
},
|
|
expectedErrors: map[string][]string{
|
|
"B": {"parent router \"A\" does not exist", "router is not reachable"},
|
|
},
|
|
},
|
|
{
|
|
desc: "Dead-end router with no child and no service",
|
|
routers: map[string]*dynamic.Router{
|
|
"A": {},
|
|
},
|
|
expectedStatuses: map[string]string{
|
|
"A": runtime.StatusDisabled,
|
|
},
|
|
expectedChildRefs: map[string][]string{
|
|
"A": {},
|
|
},
|
|
expectedErrors: map[string][]string{
|
|
"A": {"router has no service and no child routers"},
|
|
},
|
|
},
|
|
{
|
|
// A->B->A
|
|
desc: "Router is not reachable",
|
|
routers: map[string]*dynamic.Router{
|
|
"A": {
|
|
ParentRefs: []string{"B"},
|
|
},
|
|
"B": {
|
|
ParentRefs: []string{"A"},
|
|
},
|
|
},
|
|
expectedStatuses: map[string]string{
|
|
"A": runtime.StatusDisabled,
|
|
"B": runtime.StatusDisabled,
|
|
},
|
|
expectedChildRefs: map[string][]string{
|
|
"A": {"B"},
|
|
"B": {"A"},
|
|
},
|
|
// Cycle detection does not visit unreachable routers (it avoids computing the cycle dependency graph for unreachable routers).
|
|
expectedErrors: map[string][]string{
|
|
"A": {"router is not reachable"},
|
|
"B": {"router is not reachable"},
|
|
},
|
|
},
|
|
{
|
|
// A->B->C->D->B
|
|
desc: "Router creating a cycle is a dead-end and should be disabled",
|
|
routers: map[string]*dynamic.Router{
|
|
"A": {},
|
|
"B": {
|
|
ParentRefs: []string{"A", "D"},
|
|
},
|
|
"C": {
|
|
ParentRefs: []string{"B"},
|
|
},
|
|
"D": {
|
|
ParentRefs: []string{"C"},
|
|
},
|
|
},
|
|
expectedStatuses: map[string]string{
|
|
"A": runtime.StatusEnabled,
|
|
"B": runtime.StatusEnabled,
|
|
"C": runtime.StatusEnabled,
|
|
"D": runtime.StatusDisabled, // Dead-end router is disabled, because the cycle error broke the link with B.
|
|
},
|
|
expectedChildRefs: map[string][]string{
|
|
"A": {"B"},
|
|
"B": {"C"},
|
|
"C": {"D"},
|
|
"D": {},
|
|
},
|
|
expectedErrors: map[string][]string{
|
|
"D": {
|
|
"cyclic reference detected in router tree: B -> C -> D -> B",
|
|
"router has no service and no child routers",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// A->B->C->D->B
|
|
// ->E
|
|
desc: "Router creating a cycle A->B->C->D->B but which is referenced elsewhere, must be set to warning status",
|
|
routers: map[string]*dynamic.Router{
|
|
"A": {},
|
|
"B": {
|
|
ParentRefs: []string{"A", "D"},
|
|
},
|
|
"C": {
|
|
ParentRefs: []string{"B"},
|
|
},
|
|
"D": {
|
|
ParentRefs: []string{"C"},
|
|
},
|
|
"E": {
|
|
ParentRefs: []string{"D"},
|
|
Service: "E-service",
|
|
},
|
|
},
|
|
expectedStatuses: map[string]string{
|
|
"A": runtime.StatusEnabled,
|
|
"B": runtime.StatusEnabled,
|
|
"C": runtime.StatusEnabled,
|
|
"D": runtime.StatusWarning,
|
|
"E": runtime.StatusEnabled,
|
|
},
|
|
expectedChildRefs: map[string][]string{
|
|
"A": {"B"},
|
|
"B": {"C"},
|
|
"C": {"D"},
|
|
"D": {"E"},
|
|
},
|
|
expectedErrors: map[string][]string{
|
|
"D": {"cyclic reference detected in router tree: B -> C -> D -> B"},
|
|
},
|
|
},
|
|
{
|
|
desc: "Parent router with all children having errors",
|
|
routers: map[string]*dynamic.Router{
|
|
"parent": {},
|
|
"child-a": {
|
|
ParentRefs: []string{"parent"},
|
|
Service: "child-a-service",
|
|
TLS: &dynamic.RouterTLSConfig{}, // Invalid: non-root cannot have TLS
|
|
},
|
|
"child-b": {
|
|
ParentRefs: []string{"parent"},
|
|
Service: "child-b-service",
|
|
TLS: &dynamic.RouterTLSConfig{}, // Invalid: non-root cannot have TLS
|
|
},
|
|
},
|
|
expectedStatuses: map[string]string{
|
|
"parent": runtime.StatusEnabled, // Enabled during ParseRouterTree (no config errors). Would be disabled during handler building when empty muxer is detected.
|
|
"child-a": runtime.StatusDisabled,
|
|
"child-b": runtime.StatusDisabled,
|
|
},
|
|
expectedChildRefs: map[string][]string{
|
|
"parent": {"child-a", "child-b"},
|
|
"child-a": nil,
|
|
"child-b": nil,
|
|
},
|
|
expectedErrors: map[string][]string{
|
|
"child-a": {"non-root router cannot have TLS configuration"},
|
|
"child-b": {"non-root router cannot have TLS configuration"},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range testCases {
|
|
t.Run(test.desc, func(t *testing.T) {
|
|
// Create runtime routers
|
|
runtimeRouters := make(map[string]*runtime.RouterInfo)
|
|
for name, router := range test.routers {
|
|
runtimeRouters[name] = &runtime.RouterInfo{
|
|
Router: router,
|
|
Status: runtime.StatusEnabled,
|
|
}
|
|
}
|
|
|
|
conf := &runtime.Configuration{
|
|
Routers: runtimeRouters,
|
|
}
|
|
|
|
manager := &Manager{
|
|
conf: conf,
|
|
}
|
|
|
|
// Execute the function we're testing
|
|
manager.ParseRouterTree()
|
|
|
|
// Verify ChildRefs are populated correctly
|
|
for routerName, expectedChildren := range test.expectedChildRefs {
|
|
router := runtimeRouters[routerName]
|
|
assert.ElementsMatch(t, expectedChildren, router.ChildRefs)
|
|
}
|
|
|
|
// Verify statuses are set correctly
|
|
var gotStatuses map[string]string
|
|
for routerName, router := range runtimeRouters {
|
|
if gotStatuses == nil {
|
|
gotStatuses = make(map[string]string)
|
|
}
|
|
gotStatuses[routerName] = router.Status
|
|
}
|
|
assert.Equal(t, test.expectedStatuses, gotStatuses)
|
|
|
|
// Verify errors are added correctly
|
|
var gotErrors map[string][]string
|
|
for routerName, router := range runtimeRouters {
|
|
for _, err := range router.Err {
|
|
if gotErrors == nil {
|
|
gotErrors = make(map[string][]string)
|
|
}
|
|
gotErrors[routerName] = append(gotErrors[routerName], err)
|
|
}
|
|
}
|
|
assert.Equal(t, test.expectedErrors, gotErrors)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestManager_buildChildRoutersMuxer(t *testing.T) {
|
|
testCases := []struct {
|
|
desc string
|
|
childRefs []string
|
|
routers map[string]*dynamic.Router
|
|
services map[string]*dynamic.Service
|
|
middlewares map[string]*dynamic.Middleware
|
|
expectedError string
|
|
expectedRequests []struct {
|
|
path string
|
|
statusCode int
|
|
}
|
|
}{
|
|
{
|
|
desc: "simple child router with service",
|
|
childRefs: []string{"child1"},
|
|
routers: map[string]*dynamic.Router{
|
|
"child1": {
|
|
Rule: "Path(`/api`)",
|
|
Service: "child1-service",
|
|
},
|
|
},
|
|
services: map[string]*dynamic.Service{
|
|
"child1-service": {
|
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
|
},
|
|
},
|
|
},
|
|
expectedRequests: []struct {
|
|
path string
|
|
statusCode int
|
|
}{
|
|
{path: "/api", statusCode: http.StatusOK},
|
|
{path: "/unknown", statusCode: http.StatusNotFound},
|
|
},
|
|
},
|
|
{
|
|
desc: "multiple child routers with different rules",
|
|
childRefs: []string{"child1", "child2"},
|
|
routers: map[string]*dynamic.Router{
|
|
"child1": {
|
|
Rule: "Path(`/api`)",
|
|
Service: "child1-service",
|
|
},
|
|
"child2": {
|
|
Rule: "Path(`/web`)",
|
|
Service: "child2-service",
|
|
},
|
|
},
|
|
services: map[string]*dynamic.Service{
|
|
"child1-service": {
|
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
|
},
|
|
},
|
|
"child2-service": {
|
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
Servers: []dynamic.Server{{URL: "http://localhost:8081"}},
|
|
},
|
|
},
|
|
},
|
|
expectedRequests: []struct {
|
|
path string
|
|
statusCode int
|
|
}{
|
|
{path: "/api", statusCode: http.StatusOK},
|
|
{path: "/web", statusCode: http.StatusOK},
|
|
{path: "/unknown", statusCode: http.StatusNotFound},
|
|
},
|
|
},
|
|
{
|
|
desc: "child router with middleware",
|
|
childRefs: []string{"child1"},
|
|
routers: map[string]*dynamic.Router{
|
|
"child1": {
|
|
Rule: "Path(`/api`)",
|
|
Service: "child1-service",
|
|
Middlewares: []string{"test-middleware"},
|
|
},
|
|
},
|
|
services: map[string]*dynamic.Service{
|
|
"child1-service": {
|
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
|
},
|
|
},
|
|
},
|
|
middlewares: map[string]*dynamic.Middleware{
|
|
"test-middleware": {
|
|
Headers: &dynamic.Headers{
|
|
CustomRequestHeaders: map[string]string{"X-Test": "value"},
|
|
},
|
|
},
|
|
},
|
|
expectedRequests: []struct {
|
|
path string
|
|
statusCode int
|
|
}{
|
|
{path: "/api", statusCode: http.StatusOK},
|
|
{path: "/unknown", statusCode: http.StatusNotFound},
|
|
},
|
|
},
|
|
{
|
|
desc: "nested child routers (child with its own children)",
|
|
childRefs: []string{"intermediate"},
|
|
routers: map[string]*dynamic.Router{
|
|
"intermediate": {
|
|
Rule: "PathPrefix(`/api`)",
|
|
// No service - this will have its own children
|
|
},
|
|
"leaf1": {
|
|
Rule: "Path(`/api/v1`)",
|
|
Service: "leaf1-service",
|
|
ParentRefs: []string{"intermediate"},
|
|
},
|
|
"leaf2": {
|
|
Rule: "Path(`/api/v2`)",
|
|
Service: "leaf2-service",
|
|
ParentRefs: []string{"intermediate"},
|
|
},
|
|
},
|
|
services: map[string]*dynamic.Service{
|
|
"leaf1-service": {
|
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
|
},
|
|
},
|
|
"leaf2-service": {
|
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
Servers: []dynamic.Server{{URL: "http://localhost:8081"}},
|
|
},
|
|
},
|
|
},
|
|
expectedRequests: []struct {
|
|
path string
|
|
statusCode int
|
|
}{
|
|
{path: "/api/v1", statusCode: http.StatusOK},
|
|
{path: "/api/v2", statusCode: http.StatusOK},
|
|
{path: "/unknown", statusCode: http.StatusNotFound},
|
|
},
|
|
},
|
|
{
|
|
desc: "all child routers have errors - should return error",
|
|
childRefs: []string{"child1", "child2"},
|
|
routers: map[string]*dynamic.Router{
|
|
"child1": {
|
|
Rule: "Path(`/api`)",
|
|
Service: "child1-service",
|
|
ParentRefs: []string{"parent"},
|
|
TLS: &dynamic.RouterTLSConfig{}, // Invalid: non-root router cannot have TLS
|
|
},
|
|
"child2": {
|
|
Rule: "Path(`/web`)",
|
|
Service: "child2-service",
|
|
ParentRefs: []string{"parent"},
|
|
TLS: &dynamic.RouterTLSConfig{}, // Invalid: non-root router cannot have TLS
|
|
},
|
|
},
|
|
services: map[string]*dynamic.Service{
|
|
"child1-service": {
|
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
|
},
|
|
},
|
|
"child2-service": {
|
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
Servers: []dynamic.Server{{URL: "http://localhost:8081"}},
|
|
},
|
|
},
|
|
},
|
|
expectedError: "no child routers could be added to muxer (2 skipped)",
|
|
},
|
|
}
|
|
|
|
for _, test := range testCases {
|
|
t.Run(test.desc, func(t *testing.T) {
|
|
// Create runtime routers
|
|
runtimeRouters := make(map[string]*runtime.RouterInfo)
|
|
for name, router := range test.routers {
|
|
runtimeRouters[name] = &runtime.RouterInfo{
|
|
Router: router,
|
|
}
|
|
}
|
|
|
|
// Create runtime services
|
|
runtimeServices := make(map[string]*runtime.ServiceInfo)
|
|
for name, service := range test.services {
|
|
runtimeServices[name] = &runtime.ServiceInfo{
|
|
Service: service,
|
|
}
|
|
}
|
|
|
|
// Create runtime middlewares
|
|
runtimeMiddlewares := make(map[string]*runtime.MiddlewareInfo)
|
|
for name, middleware := range test.middlewares {
|
|
runtimeMiddlewares[name] = &runtime.MiddlewareInfo{
|
|
Middleware: middleware,
|
|
}
|
|
}
|
|
|
|
conf := &runtime.Configuration{
|
|
Routers: runtimeRouters,
|
|
Services: runtimeServices,
|
|
Middlewares: runtimeMiddlewares,
|
|
}
|
|
|
|
// Set up the manager with mocks
|
|
serviceManager := &mockServiceManager{}
|
|
middlewareBuilder := &mockMiddlewareBuilder{}
|
|
parser, err := httpmuxer.NewSyntaxParser()
|
|
require.NoError(t, err)
|
|
|
|
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser)
|
|
|
|
// Compute multi-layer routing to populate ChildRefs
|
|
manager.ParseRouterTree()
|
|
|
|
// Build the child routers muxer
|
|
ctx := t.Context()
|
|
muxer, err := manager.buildChildRoutersMuxer(ctx, test.childRefs)
|
|
|
|
if test.expectedError != "" {
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), test.expectedError)
|
|
return
|
|
}
|
|
|
|
if len(test.childRefs) == 0 {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, muxer)
|
|
|
|
// Test that the muxer routes requests correctly
|
|
for _, req := range test.expectedRequests {
|
|
recorder := httptest.NewRecorder()
|
|
request := httptest.NewRequest(http.MethodGet, req.path, nil)
|
|
muxer.ServeHTTP(recorder, request)
|
|
|
|
assert.Equal(t, req.statusCode, recorder.Code, "unexpected status code for path %s", req.path)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestManager_buildHTTPHandler_WithChildRouters(t *testing.T) {
|
|
testCases := []struct {
|
|
desc string
|
|
router *runtime.RouterInfo
|
|
childRouters map[string]*dynamic.Router
|
|
services map[string]*dynamic.Service
|
|
expectedError string
|
|
expectedRequests []struct {
|
|
path string
|
|
statusCode int
|
|
}
|
|
}{
|
|
{
|
|
desc: "router with child routers",
|
|
router: &runtime.RouterInfo{
|
|
Router: &dynamic.Router{
|
|
Rule: "PathPrefix(`/api`)",
|
|
},
|
|
ChildRefs: []string{"child1", "child2"},
|
|
},
|
|
childRouters: map[string]*dynamic.Router{
|
|
"child1": {
|
|
Rule: "Path(`/api/v1`)",
|
|
Service: "child1-service",
|
|
},
|
|
"child2": {
|
|
Rule: "Path(`/api/v2`)",
|
|
Service: "child2-service",
|
|
},
|
|
},
|
|
services: map[string]*dynamic.Service{
|
|
"child1-service": {
|
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
|
},
|
|
},
|
|
"child2-service": {
|
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
Servers: []dynamic.Server{{URL: "http://localhost:8081"}},
|
|
},
|
|
},
|
|
},
|
|
expectedRequests: []struct {
|
|
path string
|
|
statusCode int
|
|
}{
|
|
{path: "/unknown", statusCode: http.StatusNotFound},
|
|
},
|
|
},
|
|
{
|
|
desc: "router with service (normal case)",
|
|
router: &runtime.RouterInfo{
|
|
Router: &dynamic.Router{
|
|
Rule: "PathPrefix(`/api`)",
|
|
Service: "main-service",
|
|
},
|
|
},
|
|
services: map[string]*dynamic.Service{
|
|
"main-service": {
|
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
|
},
|
|
},
|
|
},
|
|
expectedRequests: []struct {
|
|
path string
|
|
statusCode int
|
|
}{},
|
|
},
|
|
{
|
|
desc: "router with neither service nor child routers - error",
|
|
router: &runtime.RouterInfo{
|
|
Router: &dynamic.Router{
|
|
Rule: "PathPrefix(`/api`)",
|
|
},
|
|
},
|
|
expectedError: "router must have either a service or child routers",
|
|
},
|
|
{
|
|
desc: "router with child routers but missing child - error",
|
|
router: &runtime.RouterInfo{
|
|
Router: &dynamic.Router{
|
|
Rule: "PathPrefix(`/api`)",
|
|
},
|
|
ChildRefs: []string{"nonexistent"},
|
|
},
|
|
expectedError: "child router \"nonexistent\" does not exist",
|
|
},
|
|
{
|
|
desc: "router with all children having errors - returns empty muxer error",
|
|
router: &runtime.RouterInfo{
|
|
Router: &dynamic.Router{
|
|
Rule: "PathPrefix(`/api`)",
|
|
},
|
|
ChildRefs: []string{"child1", "child2"},
|
|
},
|
|
childRouters: map[string]*dynamic.Router{
|
|
"child1": {
|
|
Rule: "Path(`/api/v1`)",
|
|
Service: "child1-service",
|
|
ParentRefs: []string{"parent"},
|
|
TLS: &dynamic.RouterTLSConfig{}, // Invalid for non-root
|
|
},
|
|
"child2": {
|
|
Rule: "Path(`/api/v2`)",
|
|
Service: "child2-service",
|
|
ParentRefs: []string{"parent"},
|
|
TLS: &dynamic.RouterTLSConfig{}, // Invalid for non-root
|
|
},
|
|
},
|
|
services: map[string]*dynamic.Service{
|
|
"child1-service": {
|
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
|
},
|
|
},
|
|
"child2-service": {
|
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
Servers: []dynamic.Server{{URL: "http://localhost:8081"}},
|
|
},
|
|
},
|
|
},
|
|
expectedError: "no child routers could be added to muxer (2 skipped)",
|
|
},
|
|
}
|
|
|
|
for _, test := range testCases {
|
|
t.Run(test.desc, func(t *testing.T) {
|
|
// Create runtime routers
|
|
runtimeRouters := make(map[string]*runtime.RouterInfo)
|
|
runtimeRouters["test-router"] = test.router
|
|
for name, router := range test.childRouters {
|
|
runtimeRouters[name] = &runtime.RouterInfo{
|
|
Router: router,
|
|
}
|
|
}
|
|
|
|
// Create runtime services
|
|
runtimeServices := make(map[string]*runtime.ServiceInfo)
|
|
for name, service := range test.services {
|
|
runtimeServices[name] = &runtime.ServiceInfo{
|
|
Service: service,
|
|
}
|
|
}
|
|
|
|
conf := &runtime.Configuration{
|
|
Routers: runtimeRouters,
|
|
Services: runtimeServices,
|
|
}
|
|
|
|
// Set up the manager with mocks
|
|
serviceManager := &mockServiceManager{}
|
|
middlewareBuilder := &mockMiddlewareBuilder{}
|
|
parser, err := httpmuxer.NewSyntaxParser()
|
|
require.NoError(t, err)
|
|
|
|
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser)
|
|
|
|
// Run ParseRouterTree to validate configuration and populate ChildRefs/errors
|
|
manager.ParseRouterTree()
|
|
|
|
// Build the HTTP handler
|
|
ctx := t.Context()
|
|
handler, err := manager.buildHTTPHandler(ctx, test.router, "test-router")
|
|
|
|
if test.expectedError != "" {
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), test.expectedError)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, handler)
|
|
|
|
// Test that the handler routes requests correctly
|
|
for _, req := range test.expectedRequests {
|
|
recorder := httptest.NewRecorder()
|
|
request := httptest.NewRequest(http.MethodGet, req.path, nil)
|
|
handler.ServeHTTP(recorder, request)
|
|
|
|
assert.Equal(t, req.statusCode, recorder.Code, "unexpected status code for path %s", req.path)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestManager_BuildHandlers_WithChildRouters(t *testing.T) {
|
|
testCases := []struct {
|
|
desc string
|
|
routers map[string]*dynamic.Router
|
|
services map[string]*dynamic.Service
|
|
entryPoints []string
|
|
expectedEntryPoint string
|
|
expectedRequests []struct {
|
|
path string
|
|
statusCode int
|
|
}
|
|
}{
|
|
{
|
|
desc: "parent router with child routers",
|
|
routers: map[string]*dynamic.Router{
|
|
"parent": {
|
|
EntryPoints: []string{"web"},
|
|
Rule: "PathPrefix(`/api`)",
|
|
},
|
|
"child1": {
|
|
Rule: "Path(`/api/v1`)",
|
|
Service: "child1-service",
|
|
ParentRefs: []string{"parent"},
|
|
},
|
|
"child2": {
|
|
Rule: "Path(`/api/v2`)",
|
|
Service: "child2-service",
|
|
ParentRefs: []string{"parent"},
|
|
},
|
|
},
|
|
services: map[string]*dynamic.Service{
|
|
"child1-service": {
|
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
|
},
|
|
},
|
|
"child2-service": {
|
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
Servers: []dynamic.Server{{URL: "http://localhost:8081"}},
|
|
},
|
|
},
|
|
},
|
|
entryPoints: []string{"web"},
|
|
expectedEntryPoint: "web",
|
|
expectedRequests: []struct {
|
|
path string
|
|
statusCode int
|
|
}{
|
|
{path: "/unknown", statusCode: http.StatusNotFound},
|
|
},
|
|
},
|
|
{
|
|
desc: "multiple parent routers with children",
|
|
routers: map[string]*dynamic.Router{
|
|
"api-parent": {
|
|
EntryPoints: []string{"web"},
|
|
Rule: "PathPrefix(`/api`)",
|
|
},
|
|
"web-parent": {
|
|
EntryPoints: []string{"web"},
|
|
Rule: "PathPrefix(`/web`)",
|
|
},
|
|
"api-child": {
|
|
Rule: "Path(`/api/v1`)",
|
|
Service: "api-service",
|
|
ParentRefs: []string{"api-parent"},
|
|
},
|
|
"web-child": {
|
|
Rule: "Path(`/web/index`)",
|
|
Service: "web-service",
|
|
ParentRefs: []string{"web-parent"},
|
|
},
|
|
},
|
|
services: map[string]*dynamic.Service{
|
|
"api-service": {
|
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
|
},
|
|
},
|
|
"web-service": {
|
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
Servers: []dynamic.Server{{URL: "http://localhost:8081"}},
|
|
},
|
|
},
|
|
},
|
|
entryPoints: []string{"web"},
|
|
expectedEntryPoint: "web",
|
|
expectedRequests: []struct {
|
|
path string
|
|
statusCode int
|
|
}{},
|
|
},
|
|
}
|
|
|
|
for _, test := range testCases {
|
|
t.Run(test.desc, func(t *testing.T) {
|
|
// Create runtime routers
|
|
runtimeRouters := make(map[string]*runtime.RouterInfo)
|
|
for name, router := range test.routers {
|
|
runtimeRouters[name] = &runtime.RouterInfo{
|
|
Router: router,
|
|
}
|
|
}
|
|
|
|
// Create runtime services
|
|
runtimeServices := make(map[string]*runtime.ServiceInfo)
|
|
for name, service := range test.services {
|
|
runtimeServices[name] = &runtime.ServiceInfo{
|
|
Service: service,
|
|
}
|
|
}
|
|
|
|
conf := &runtime.Configuration{
|
|
Routers: runtimeRouters,
|
|
Services: runtimeServices,
|
|
}
|
|
|
|
// Set up the manager with mocks
|
|
serviceManager := &mockServiceManager{}
|
|
middlewareBuilder := &mockMiddlewareBuilder{}
|
|
parser, err := httpmuxer.NewSyntaxParser()
|
|
require.NoError(t, err)
|
|
|
|
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser)
|
|
|
|
// Compute multi-layer routing to set up parent-child relationships
|
|
manager.ParseRouterTree()
|
|
|
|
// Build handlers
|
|
ctx := t.Context()
|
|
handlers := manager.BuildHandlers(ctx, test.entryPoints, false)
|
|
|
|
require.Contains(t, handlers, test.expectedEntryPoint)
|
|
handler := handlers[test.expectedEntryPoint]
|
|
require.NotNil(t, handler)
|
|
|
|
// Test that the handler routes requests correctly
|
|
for _, req := range test.expectedRequests {
|
|
recorder := httptest.NewRecorder()
|
|
request := httptest.NewRequest(http.MethodGet, req.path, nil)
|
|
request.Host = "test.com"
|
|
handler.ServeHTTP(recorder, request)
|
|
|
|
assert.Equal(t, req.statusCode, recorder.Code, "unexpected status code for path %s", req.path)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Mock implementations for testing
|
|
|
|
type mockServiceManager struct{}
|
|
|
|
func (m *mockServiceManager) BuildHTTP(_ context.Context, _ string) (http.Handler, error) {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("mock service response"))
|
|
}), nil
|
|
}
|
|
|
|
func (m *mockServiceManager) LaunchHealthCheck(_ context.Context) {}
|
|
|
|
type mockMiddlewareBuilder struct{}
|
|
|
|
func (m *mockMiddlewareBuilder) BuildChain(_ context.Context, _ []string) *alice.Chain {
|
|
chain := alice.New()
|
|
return &chain
|
|
}
|
|
|
|
type proxyBuilderMock struct{}
|
|
|
|
func (p proxyBuilderMock) Build(_ string, _ *url.URL, _, _ bool, _ time.Duration) (http.Handler, error) {
|
|
return http.HandlerFunc(func(responseWriter http.ResponseWriter, req *http.Request) {}), nil
|
|
}
|
|
|
|
func (p proxyBuilderMock) Update(_ map[string]*dynamic.ServersTransport) {
|
|
panic("implement me")
|
|
}
|