Dynamic Configuration Refactoring
This commit is contained in:
parent
d3ae88f108
commit
a09dfa3ce1
452 changed files with 21023 additions and 9419 deletions
119
server/router/route_appender_aggregator.go
Normal file
119
server/router/route_appender_aggregator.go
Normal file
|
@ -0,0 +1,119 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/containous/alice"
|
||||
"github.com/containous/mux"
|
||||
"github.com/containous/traefik/api"
|
||||
"github.com/containous/traefik/config/static"
|
||||
"github.com/containous/traefik/log"
|
||||
"github.com/containous/traefik/metrics"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
)
|
||||
|
||||
// chainBuilder The contract of the middleware builder
|
||||
type chainBuilder interface {
|
||||
BuildChain(ctx context.Context, middlewares []string) (*alice.Chain, error)
|
||||
}
|
||||
|
||||
// NewRouteAppenderAggregator Creates a new RouteAppenderAggregator
|
||||
func NewRouteAppenderAggregator(ctx context.Context, chainBuilder chainBuilder, conf static.Configuration, entryPointName string, currentConfiguration *safe.Safe) *RouteAppenderAggregator {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
aggregator := &RouteAppenderAggregator{}
|
||||
|
||||
// FIXME add REST
|
||||
|
||||
if conf.API != nil && conf.API.EntryPoint == entryPointName {
|
||||
chain, err := chainBuilder.BuildChain(ctx, conf.API.Middlewares)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
} else {
|
||||
aggregator.AddAppender(&WithMiddleware{
|
||||
appender: api.Handler{
|
||||
EntryPoint: conf.API.EntryPoint,
|
||||
Dashboard: conf.API.Dashboard,
|
||||
Statistics: conf.API.Statistics,
|
||||
DashboardAssets: conf.API.DashboardAssets,
|
||||
CurrentConfigurations: currentConfiguration,
|
||||
},
|
||||
routerMiddlewares: chain,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if conf.Ping != nil && conf.Ping.EntryPoint == entryPointName {
|
||||
chain, err := chainBuilder.BuildChain(ctx, conf.Ping.Middlewares)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
} else {
|
||||
aggregator.AddAppender(&WithMiddleware{
|
||||
appender: conf.Ping,
|
||||
routerMiddlewares: chain,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if conf.Metrics != nil && conf.Metrics.Prometheus != nil && conf.Metrics.Prometheus.EntryPoint == entryPointName {
|
||||
chain, err := chainBuilder.BuildChain(ctx, conf.Metrics.Prometheus.Middlewares)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
} else {
|
||||
aggregator.AddAppender(&WithMiddleware{
|
||||
appender: metrics.PrometheusHandler{},
|
||||
routerMiddlewares: chain,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return aggregator
|
||||
}
|
||||
|
||||
// RouteAppenderAggregator RouteAppender that aggregate other RouteAppender
|
||||
type RouteAppenderAggregator struct {
|
||||
appenders []types.RouteAppender
|
||||
}
|
||||
|
||||
// Append Adds routes to the router
|
||||
func (r *RouteAppenderAggregator) Append(systemRouter *mux.Router) {
|
||||
for _, router := range r.appenders {
|
||||
router.Append(systemRouter)
|
||||
}
|
||||
}
|
||||
|
||||
// AddAppender adds a router in the aggregator
|
||||
func (r *RouteAppenderAggregator) AddAppender(router types.RouteAppender) {
|
||||
r.appenders = append(r.appenders, router)
|
||||
}
|
||||
|
||||
// WithMiddleware router with internal middleware
|
||||
type WithMiddleware struct {
|
||||
appender types.RouteAppender
|
||||
routerMiddlewares *alice.Chain
|
||||
}
|
||||
|
||||
// Append Adds routes to the router
|
||||
func (wm *WithMiddleware) Append(systemRouter *mux.Router) {
|
||||
realRouter := systemRouter.PathPrefix("/").Subrouter()
|
||||
|
||||
wm.appender.Append(realRouter)
|
||||
|
||||
if err := realRouter.Walk(wrapRoute(wm.routerMiddlewares)); err != nil {
|
||||
log.WithoutContext().Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// wrapRoute with middlewares
|
||||
func wrapRoute(middlewares *alice.Chain) func(*mux.Route, *mux.Router, []*mux.Route) error {
|
||||
return func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
|
||||
handler, err := middlewares.Then(route.GetHandler())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
route.Handler(handler)
|
||||
return nil
|
||||
}
|
||||
}
|
116
server/router/route_appender_aggregator_test.go
Normal file
116
server/router/route_appender_aggregator_test.go
Normal file
|
@ -0,0 +1,116 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/containous/alice"
|
||||
"github.com/containous/mux"
|
||||
"github.com/containous/traefik/config/static"
|
||||
"github.com/containous/traefik/ping"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type ChainBuilderMock struct {
|
||||
middles map[string]alice.Constructor
|
||||
}
|
||||
|
||||
func (c *ChainBuilderMock) BuildChain(ctx context.Context, middles []string) (*alice.Chain, error) {
|
||||
chain := alice.New()
|
||||
|
||||
for _, mName := range middles {
|
||||
if constructor, ok := c.middles[mName]; ok {
|
||||
chain = chain.Append(constructor)
|
||||
}
|
||||
}
|
||||
|
||||
return &chain, nil
|
||||
}
|
||||
|
||||
func TestNewRouteAppenderAggregator(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
staticConf static.Configuration
|
||||
middles map[string]alice.Constructor
|
||||
expected map[string]int
|
||||
}{
|
||||
{
|
||||
desc: "API with auth, ping without auth",
|
||||
staticConf: static.Configuration{
|
||||
API: &static.API{
|
||||
EntryPoint: "traefik",
|
||||
Middlewares: []string{"dumb"},
|
||||
},
|
||||
Ping: &ping.Handler{
|
||||
EntryPoint: "traefik",
|
||||
},
|
||||
EntryPoints: &static.EntryPoints{
|
||||
EntryPointList: map[string]static.EntryPoint{
|
||||
"traefik": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
middles: map[string]alice.Constructor{
|
||||
"dumb": func(_ http.Handler) (http.Handler, error) {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}), nil
|
||||
},
|
||||
},
|
||||
expected: map[string]int{
|
||||
"/wrong": http.StatusBadGateway,
|
||||
"/ping": http.StatusOK,
|
||||
//"/.well-known/acme-challenge/token": http.StatusNotFound, // FIXME
|
||||
"/api/providers": http.StatusUnauthorized,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Wrong entrypoint name",
|
||||
staticConf: static.Configuration{
|
||||
API: &static.API{
|
||||
EntryPoint: "no",
|
||||
},
|
||||
EntryPoints: &static.EntryPoints{
|
||||
EntryPointList: map[string]static.EntryPoint{
|
||||
"traefik": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]int{
|
||||
"/api/providers": http.StatusBadGateway,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
chainBuilder := &ChainBuilderMock{middles: test.middles}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
router := NewRouteAppenderAggregator(ctx, chainBuilder, test.staticConf, "traefik", nil)
|
||||
|
||||
internalMuxRouter := mux.NewRouter()
|
||||
router.Append(internalMuxRouter)
|
||||
|
||||
internalMuxRouter.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
})
|
||||
|
||||
actual := make(map[string]int)
|
||||
for calledURL := range test.expected {
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, calledURL, nil)
|
||||
internalMuxRouter.ServeHTTP(recorder, request)
|
||||
actual[calledURL] = recorder.Code
|
||||
}
|
||||
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
38
server/router/route_appender_factory.go
Normal file
38
server/router/route_appender_factory.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/containous/traefik/config/static"
|
||||
"github.com/containous/traefik/provider/acme"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/server/middleware"
|
||||
"github.com/containous/traefik/types"
|
||||
)
|
||||
|
||||
// NewRouteAppenderFactory Creates a new RouteAppenderFactory
|
||||
func NewRouteAppenderFactory(staticConfiguration static.Configuration, entryPointName string, acmeProvider *acme.Provider) *RouteAppenderFactory {
|
||||
return &RouteAppenderFactory{
|
||||
staticConfiguration: staticConfiguration,
|
||||
entryPointName: entryPointName,
|
||||
acmeProvider: acmeProvider,
|
||||
}
|
||||
}
|
||||
|
||||
// RouteAppenderFactory A factory of RouteAppender
|
||||
type RouteAppenderFactory struct {
|
||||
staticConfiguration static.Configuration
|
||||
entryPointName string
|
||||
acmeProvider *acme.Provider
|
||||
}
|
||||
|
||||
// NewAppender Creates a new RouteAppender
|
||||
func (r *RouteAppenderFactory) NewAppender(ctx context.Context, middlewaresBuilder *middleware.Builder, currentConfiguration *safe.Safe) types.RouteAppender {
|
||||
aggregator := NewRouteAppenderAggregator(ctx, middlewaresBuilder, r.staticConfiguration, r.entryPointName, currentConfiguration)
|
||||
|
||||
if r.acmeProvider != nil && r.acmeProvider.HTTPChallenge != nil && r.acmeProvider.HTTPChallenge.EntryPoint == r.entryPointName {
|
||||
aggregator.AddAppender(r.acmeProvider)
|
||||
}
|
||||
|
||||
return aggregator
|
||||
}
|
183
server/router/router.go
Normal file
183
server/router/router.go
Normal file
|
@ -0,0 +1,183 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/containous/alice"
|
||||
"github.com/containous/mux"
|
||||
"github.com/containous/traefik/config"
|
||||
"github.com/containous/traefik/log"
|
||||
"github.com/containous/traefik/middlewares/accesslog"
|
||||
"github.com/containous/traefik/middlewares/recovery"
|
||||
"github.com/containous/traefik/middlewares/tracing"
|
||||
"github.com/containous/traefik/responsemodifiers"
|
||||
"github.com/containous/traefik/server/middleware"
|
||||
"github.com/containous/traefik/server/service"
|
||||
)
|
||||
|
||||
const (
|
||||
recoveryMiddlewareName = "traefik-internal-recovery"
|
||||
)
|
||||
|
||||
// NewManager Creates a new Manager
|
||||
func NewManager(routers map[string]*config.Router,
|
||||
serviceManager *service.Manager, middlewaresBuilder *middleware.Builder, modifierBuilder *responsemodifiers.Builder,
|
||||
) *Manager {
|
||||
return &Manager{
|
||||
routerHandlers: make(map[string]http.Handler),
|
||||
configs: routers,
|
||||
serviceManager: serviceManager,
|
||||
middlewaresBuilder: middlewaresBuilder,
|
||||
modifierBuilder: modifierBuilder,
|
||||
}
|
||||
}
|
||||
|
||||
// Manager A route/router manager
|
||||
type Manager struct {
|
||||
routerHandlers map[string]http.Handler
|
||||
configs map[string]*config.Router
|
||||
serviceManager *service.Manager
|
||||
middlewaresBuilder *middleware.Builder
|
||||
modifierBuilder *responsemodifiers.Builder
|
||||
}
|
||||
|
||||
// BuildHandlers Builds handler for all entry points
|
||||
func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string, defaultEntryPoints []string) map[string]http.Handler {
|
||||
entryPointsRouters := m.filteredRouters(rootCtx, entryPoints, defaultEntryPoints)
|
||||
|
||||
entryPointHandlers := make(map[string]http.Handler)
|
||||
for entryPointName, routers := range entryPointsRouters {
|
||||
ctx := log.With(rootCtx, log.Str(log.EntryPointName, entryPointName))
|
||||
|
||||
handler, err := m.buildEntryPointHandler(ctx, routers)
|
||||
if err != nil {
|
||||
log.FromContext(ctx).Error(err)
|
||||
continue
|
||||
}
|
||||
entryPointHandlers[entryPointName] = handler
|
||||
}
|
||||
|
||||
m.serviceManager.LaunchHealthCheck()
|
||||
|
||||
return entryPointHandlers
|
||||
}
|
||||
|
||||
func contains(entryPoints []string, entryPointName string) bool {
|
||||
for _, name := range entryPoints {
|
||||
if name == entryPointName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Manager) filteredRouters(ctx context.Context, entryPoints []string, defaultEntryPoints []string) map[string]map[string]*config.Router {
|
||||
entryPointsRouters := make(map[string]map[string]*config.Router)
|
||||
|
||||
for rtName, rt := range m.configs {
|
||||
eps := rt.EntryPoints
|
||||
if len(eps) == 0 {
|
||||
eps = defaultEntryPoints
|
||||
}
|
||||
for _, entryPointName := range eps {
|
||||
if !contains(entryPoints, entryPointName) {
|
||||
log.FromContext(log.With(ctx, log.Str(log.EntryPointName, entryPointName))).
|
||||
Errorf("entryPoint %q doesn't exist", entryPointName)
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := entryPointsRouters[entryPointName]; !ok {
|
||||
entryPointsRouters[entryPointName] = make(map[string]*config.Router)
|
||||
}
|
||||
|
||||
entryPointsRouters[entryPointName][rtName] = rt
|
||||
}
|
||||
}
|
||||
|
||||
return entryPointsRouters
|
||||
}
|
||||
|
||||
func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*config.Router) (http.Handler, error) {
|
||||
router := mux.NewRouter().
|
||||
SkipClean(true)
|
||||
|
||||
for routerName, routerConfig := range configs {
|
||||
ctx = log.With(ctx, log.Str(log.RouterName, routerName))
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
handler, err := m.buildRouterHandler(ctx, routerName)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = addRoute(ctx, router, routerConfig.Rule, routerConfig.Priority, handler)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
router.SortRoutes()
|
||||
|
||||
chain := alice.New()
|
||||
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
|
||||
return recovery.New(ctx, next, recoveryMiddlewareName)
|
||||
})
|
||||
|
||||
return chain.Then(router)
|
||||
}
|
||||
|
||||
func (m *Manager) buildRouterHandler(ctx context.Context, routerName string) (http.Handler, error) {
|
||||
if handler, ok := m.routerHandlers[routerName]; ok {
|
||||
return handler, nil
|
||||
}
|
||||
|
||||
configRouter, ok := m.configs[routerName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no configuration for %s", routerName)
|
||||
}
|
||||
|
||||
handler, err := m.buildHandler(ctx, configRouter, routerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
handlerWithAccessLog, err := alice.New(func(next http.Handler) (http.Handler, error) {
|
||||
return accesslog.NewFieldHandler(next, accesslog.RouterName, routerName, nil), nil
|
||||
}).Then(handler)
|
||||
if err != nil {
|
||||
log.FromContext(ctx).Error(err)
|
||||
m.routerHandlers[routerName] = handler
|
||||
} else {
|
||||
m.routerHandlers[routerName] = handlerWithAccessLog
|
||||
}
|
||||
|
||||
return m.routerHandlers[routerName], nil
|
||||
}
|
||||
|
||||
func (m *Manager) buildHandler(ctx context.Context, router *config.Router, routerName string) (http.Handler, error) {
|
||||
rm := m.modifierBuilder.Build(ctx, router.Middlewares)
|
||||
|
||||
sHandler, err := m.serviceManager.Build(ctx, router.Service, rm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mHandler, err := m.middlewaresBuilder.BuildChain(ctx, router.Middlewares)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
alHandler := func(next http.Handler) (http.Handler, error) {
|
||||
return accesslog.NewFieldHandler(next, accesslog.ServiceName, router.Service, accesslog.AddServiceFields), nil
|
||||
}
|
||||
|
||||
tHandler := func(next http.Handler) (http.Handler, error) {
|
||||
return tracing.NewForwarder(ctx, routerName, router.Service, next), nil
|
||||
}
|
||||
|
||||
return alice.New().Append(alHandler).Extend(*mHandler).Append(tHandler).Then(sHandler)
|
||||
}
|
334
server/router/router_test.go
Normal file
334
server/router/router_test.go
Normal file
|
@ -0,0 +1,334 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/containous/traefik/config"
|
||||
"github.com/containous/traefik/middlewares/accesslog"
|
||||
"github.com/containous/traefik/middlewares/requestdecorator"
|
||||
"github.com/containous/traefik/responsemodifiers"
|
||||
"github.com/containous/traefik/server/middleware"
|
||||
"github.com/containous/traefik/server/service"
|
||||
"github.com/containous/traefik/testhelpers"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRouterManager_Get(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
|
||||
type ExpectedResult struct {
|
||||
StatusCode int
|
||||
RequestHeaders map[string]string
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
routersConfig map[string]*config.Router
|
||||
serviceConfig map[string]*config.Service
|
||||
middlewaresConfig map[string]*config.Middleware
|
||||
entryPoints []string
|
||||
defaultEntryPoints []string
|
||||
expected ExpectedResult
|
||||
}{
|
||||
{
|
||||
desc: "no middleware",
|
||||
routersConfig: map[string]*config.Router{
|
||||
"foo": {
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "foo-service",
|
||||
Rule: "Host:foo.bar",
|
||||
},
|
||||
},
|
||||
serviceConfig: map[string]*config.Service{
|
||||
"foo-service": {
|
||||
LoadBalancer: &config.LoadBalancerService{
|
||||
Servers: []config.Server{
|
||||
{
|
||||
URL: server.URL,
|
||||
Weight: 1,
|
||||
},
|
||||
},
|
||||
Method: "wrr",
|
||||
},
|
||||
},
|
||||
},
|
||||
entryPoints: []string{"web"},
|
||||
expected: ExpectedResult{StatusCode: http.StatusOK},
|
||||
},
|
||||
{
|
||||
desc: "no middleware, default entry point",
|
||||
routersConfig: map[string]*config.Router{
|
||||
"foo": {
|
||||
Service: "foo-service",
|
||||
Rule: "Host:foo.bar",
|
||||
},
|
||||
},
|
||||
serviceConfig: map[string]*config.Service{
|
||||
"foo-service": {
|
||||
LoadBalancer: &config.LoadBalancerService{
|
||||
Servers: []config.Server{
|
||||
{
|
||||
URL: server.URL,
|
||||
Weight: 1,
|
||||
},
|
||||
},
|
||||
Method: "wrr",
|
||||
},
|
||||
},
|
||||
},
|
||||
entryPoints: []string{"web"},
|
||||
defaultEntryPoints: []string{"web"},
|
||||
expected: ExpectedResult{StatusCode: http.StatusOK},
|
||||
},
|
||||
{
|
||||
desc: "no middleware, no matching",
|
||||
routersConfig: map[string]*config.Router{
|
||||
"foo": {
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "foo-service",
|
||||
Rule: "Host:bar.bar",
|
||||
},
|
||||
},
|
||||
serviceConfig: map[string]*config.Service{
|
||||
"foo-service": {
|
||||
LoadBalancer: &config.LoadBalancerService{
|
||||
Servers: []config.Server{
|
||||
{
|
||||
URL: server.URL,
|
||||
Weight: 1,
|
||||
},
|
||||
},
|
||||
Method: "wrr",
|
||||
},
|
||||
},
|
||||
},
|
||||
entryPoints: []string{"web"},
|
||||
expected: ExpectedResult{StatusCode: http.StatusNotFound},
|
||||
},
|
||||
{
|
||||
desc: "middleware: headers > auth",
|
||||
routersConfig: map[string]*config.Router{
|
||||
"foo": {
|
||||
EntryPoints: []string{"web"},
|
||||
Middlewares: []string{"headers-middle", "auth-middle"},
|
||||
Service: "foo-service",
|
||||
Rule: "Host:foo.bar",
|
||||
},
|
||||
},
|
||||
serviceConfig: map[string]*config.Service{
|
||||
"foo-service": {
|
||||
LoadBalancer: &config.LoadBalancerService{
|
||||
Servers: []config.Server{
|
||||
{
|
||||
URL: server.URL,
|
||||
Weight: 1,
|
||||
},
|
||||
},
|
||||
Method: "wrr",
|
||||
},
|
||||
},
|
||||
},
|
||||
middlewaresConfig: map[string]*config.Middleware{
|
||||
"auth-middle": {
|
||||
BasicAuth: &config.BasicAuth{
|
||||
Users: []string{"toto:titi"},
|
||||
},
|
||||
},
|
||||
"headers-middle": {
|
||||
Headers: &config.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]*config.Router{
|
||||
"foo": {
|
||||
EntryPoints: []string{"web"},
|
||||
Middlewares: []string{"auth-middle", "headers-middle"},
|
||||
Service: "foo-service",
|
||||
Rule: "Host:foo.bar",
|
||||
},
|
||||
},
|
||||
serviceConfig: map[string]*config.Service{
|
||||
"foo-service": {
|
||||
LoadBalancer: &config.LoadBalancerService{
|
||||
Servers: []config.Server{
|
||||
{
|
||||
URL: server.URL,
|
||||
Weight: 1,
|
||||
},
|
||||
},
|
||||
Method: "wrr",
|
||||
},
|
||||
},
|
||||
},
|
||||
middlewaresConfig: map[string]*config.Middleware{
|
||||
"auth-middle": {
|
||||
BasicAuth: &config.BasicAuth{
|
||||
Users: []string{"toto:titi"},
|
||||
},
|
||||
},
|
||||
"headers-middle": {
|
||||
Headers: &config.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()
|
||||
|
||||
serviceManager := service.NewManager(test.serviceConfig, http.DefaultTransport)
|
||||
middlewaresBuilder := middleware.NewBuilder(test.middlewaresConfig, serviceManager)
|
||||
responseModifierFactory := responsemodifiers.NewBuilder(test.middlewaresConfig)
|
||||
|
||||
routerManager := NewManager(test.routersConfig, serviceManager, middlewaresBuilder, responseModifierFactory)
|
||||
|
||||
handlers := routerManager.BuildHandlers(context.Background(), test.entryPoints, test.defaultEntryPoints)
|
||||
|
||||
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 TestAccessLog(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
routersConfig map[string]*config.Router
|
||||
serviceConfig map[string]*config.Service
|
||||
middlewaresConfig map[string]*config.Middleware
|
||||
entryPoints []string
|
||||
defaultEntryPoints []string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "apply routerName in accesslog (first match)",
|
||||
routersConfig: map[string]*config.Router{
|
||||
"foo": {
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "foo-service",
|
||||
Rule: "Host:foo.bar",
|
||||
},
|
||||
"bar": {
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "foo-service",
|
||||
Rule: "Host:bar.foo",
|
||||
},
|
||||
},
|
||||
serviceConfig: map[string]*config.Service{
|
||||
"foo-service": {
|
||||
LoadBalancer: &config.LoadBalancerService{
|
||||
Servers: []config.Server{
|
||||
{
|
||||
URL: server.URL,
|
||||
Weight: 1,
|
||||
},
|
||||
},
|
||||
Method: "wrr",
|
||||
},
|
||||
},
|
||||
},
|
||||
entryPoints: []string{"web"},
|
||||
expected: "foo",
|
||||
},
|
||||
{
|
||||
desc: "apply routerName in accesslog (second match)",
|
||||
routersConfig: map[string]*config.Router{
|
||||
"foo": {
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "foo-service",
|
||||
Rule: "Host:bar.foo",
|
||||
},
|
||||
"bar": {
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "foo-service",
|
||||
Rule: "Host:foo.bar",
|
||||
},
|
||||
},
|
||||
serviceConfig: map[string]*config.Service{
|
||||
"foo-service": {
|
||||
LoadBalancer: &config.LoadBalancerService{
|
||||
Servers: []config.Server{
|
||||
{
|
||||
URL: server.URL,
|
||||
Weight: 1,
|
||||
},
|
||||
},
|
||||
Method: "wrr",
|
||||
},
|
||||
},
|
||||
},
|
||||
entryPoints: []string{"web"},
|
||||
expected: "bar",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
|
||||
serviceManager := service.NewManager(test.serviceConfig, http.DefaultTransport)
|
||||
middlewaresBuilder := middleware.NewBuilder(test.middlewaresConfig, serviceManager)
|
||||
responseModifierFactory := responsemodifiers.NewBuilder(test.middlewaresConfig)
|
||||
|
||||
routerManager := NewManager(test.routersConfig, serviceManager, middlewaresBuilder, responseModifierFactory)
|
||||
|
||||
handlers := routerManager.BuildHandlers(context.Background(), test.entryPoints, test.defaultEntryPoints)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)
|
||||
|
||||
accesslogger, err := accesslog.NewHandler(&types.AccessLog{
|
||||
Format: "json",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
reqHost := requestdecorator.New(nil)
|
||||
|
||||
accesslogger.ServeHTTP(w, req, http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
reqHost.ServeHTTP(w, req, handlers["web"].ServeHTTP)
|
||||
|
||||
data := accesslog.GetLogData(req)
|
||||
require.NotNil(t, data)
|
||||
|
||||
assert.Equal(t, test.expected, data.Core[accesslog.RouterName])
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
163
server/router/rules.go
Normal file
163
server/router/rules.go
Normal file
|
@ -0,0 +1,163 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/containous/mux"
|
||||
"github.com/containous/traefik/log"
|
||||
"github.com/containous/traefik/middlewares/requestdecorator"
|
||||
)
|
||||
|
||||
func addRoute(ctx context.Context, router *mux.Router, rule string, priority int, handler http.Handler) error {
|
||||
matchers, err := parseRule(rule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if priority == 0 {
|
||||
priority = len(rule)
|
||||
}
|
||||
|
||||
route := router.NewRoute().Handler(handler).Priority(priority)
|
||||
for _, matcher := range matchers {
|
||||
matcher(route)
|
||||
if route.GetError() != nil {
|
||||
log.FromContext(ctx).Error(route.GetError())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseRule(rule string) ([]func(*mux.Route), error) {
|
||||
funcs := map[string]func(*mux.Route, ...string){
|
||||
"Host": host,
|
||||
"HostRegexp": hostRegexp,
|
||||
"Path": path,
|
||||
"PathPrefix": pathPrefix,
|
||||
"Method": methods,
|
||||
"Headers": headers,
|
||||
"HeadersRegexp": headersRegexp,
|
||||
"Query": query,
|
||||
}
|
||||
|
||||
splitRule := func(c rune) bool {
|
||||
return c == ';'
|
||||
}
|
||||
parsedRules := strings.FieldsFunc(rule, splitRule)
|
||||
|
||||
var matchers []func(*mux.Route)
|
||||
|
||||
for _, expression := range parsedRules {
|
||||
expParts := strings.Split(expression, ":")
|
||||
if len(expParts) > 1 && len(expParts[1]) > 0 {
|
||||
if fn, ok := funcs[expParts[0]]; ok {
|
||||
|
||||
parseOr := func(c rune) bool {
|
||||
return c == ','
|
||||
}
|
||||
|
||||
exp := strings.FieldsFunc(strings.Join(expParts[1:], ":"), parseOr)
|
||||
|
||||
var trimmedExp []string
|
||||
for _, value := range exp {
|
||||
trimmedExp = append(trimmedExp, strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
// FIXME struct for onhostrule ?
|
||||
matcher := func(rt *mux.Route) {
|
||||
fn(rt, trimmedExp...)
|
||||
}
|
||||
|
||||
matchers = append(matchers, matcher)
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid matcher: %s", expression)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matchers, nil
|
||||
}
|
||||
|
||||
func path(route *mux.Route, paths ...string) {
|
||||
rt := route.Subrouter()
|
||||
for _, path := range paths {
|
||||
tmpRt := rt.Path(path)
|
||||
if tmpRt.GetError() != nil {
|
||||
log.WithoutContext().WithField("paths", strings.Join(paths, ",")).Error(tmpRt.GetError())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pathPrefix(route *mux.Route, paths ...string) {
|
||||
rt := route.Subrouter()
|
||||
for _, path := range paths {
|
||||
tmpRt := rt.PathPrefix(path)
|
||||
if tmpRt.GetError() != nil {
|
||||
log.WithoutContext().WithField("paths", strings.Join(paths, ",")).Error(tmpRt.GetError())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func host(route *mux.Route, hosts ...string) {
|
||||
for i, host := range hosts {
|
||||
hosts[i] = strings.ToLower(host)
|
||||
}
|
||||
|
||||
route.MatcherFunc(func(req *http.Request, route *mux.RouteMatch) bool {
|
||||
reqHost := requestdecorator.GetCanonizedHost(req.Context())
|
||||
if len(reqHost) == 0 {
|
||||
log.FromContext(req.Context()).Warnf("Could not retrieve CanonizedHost, rejecting %s", req.Host)
|
||||
return false
|
||||
}
|
||||
|
||||
flatH := requestdecorator.GetCNAMEFlatten(req.Context())
|
||||
if len(flatH) > 0 {
|
||||
for _, host := range hosts {
|
||||
if strings.EqualFold(reqHost, host) || strings.EqualFold(flatH, host) {
|
||||
return true
|
||||
}
|
||||
log.FromContext(req.Context()).Debugf("CNAMEFlattening: request %s which resolved to %s, is not matched to route %s", reqHost, flatH, host)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for _, host := range hosts {
|
||||
if reqHost == host {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func hostRegexp(route *mux.Route, hosts ...string) {
|
||||
router := route.Subrouter()
|
||||
for _, host := range hosts {
|
||||
router.Host(host)
|
||||
}
|
||||
}
|
||||
|
||||
func methods(route *mux.Route, methods ...string) {
|
||||
route.Methods(methods...)
|
||||
}
|
||||
|
||||
func headers(route *mux.Route, headers ...string) {
|
||||
route.Headers(headers...)
|
||||
}
|
||||
|
||||
func headersRegexp(route *mux.Route, headers ...string) {
|
||||
route.HeadersRegexp(headers...)
|
||||
}
|
||||
|
||||
func query(route *mux.Route, query ...string) {
|
||||
var queries []string
|
||||
for _, elem := range query {
|
||||
queries = append(queries, strings.Split(elem, "=")...)
|
||||
}
|
||||
|
||||
route.Queries(queries...)
|
||||
}
|
442
server/router/rules_test.go
Normal file
442
server/router/rules_test.go
Normal file
|
@ -0,0 +1,442 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/containous/mux"
|
||||
"github.com/containous/traefik/middlewares/requestdecorator"
|
||||
"github.com/containous/traefik/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_addRoute(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
rule string
|
||||
headers map[string]string
|
||||
expected map[string]int
|
||||
}{
|
||||
{
|
||||
desc: "no rule",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "PathPrefix",
|
||||
rule: "PathPrefix:/foo",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "wrong PathPrefix",
|
||||
rule: "PathPrefix:/bar",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Host",
|
||||
rule: "Host:localhost",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "wrong Host",
|
||||
rule: "Host:nope",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Host and PathPrefix",
|
||||
rule: "Host:localhost;PathPrefix:/foo",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Host and PathPrefix: wrong PathPrefix",
|
||||
rule: "Host:localhost;PathPrefix:/bar",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Host and PathPrefix: wrong Host",
|
||||
rule: "Host:nope;PathPrefix:/bar",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Host and PathPrefix: Host OR, first host",
|
||||
rule: "Host:nope,localhost;PathPrefix:/foo",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Host and PathPrefix: Host OR, second host",
|
||||
rule: "Host:nope,localhost;PathPrefix:/foo",
|
||||
expected: map[string]int{
|
||||
"http://nope/foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Host and PathPrefix: Host OR, first host and wrong PathPrefix",
|
||||
rule: "Host:nope,localhost;PathPrefix:/bar",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "HostRegexp with capturing group",
|
||||
rule: "HostRegexp: {subdomain:(foo\\.)?bar\\.com}",
|
||||
expected: map[string]int{
|
||||
"http://foo.bar.com": http.StatusOK,
|
||||
"http://bar.com": http.StatusOK,
|
||||
"http://fooubar.com": http.StatusNotFound,
|
||||
"http://barucom": http.StatusNotFound,
|
||||
"http://barcom": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "HostRegexp with non capturing group",
|
||||
rule: "HostRegexp: {subdomain:(?:foo\\.)?bar\\.com}",
|
||||
expected: map[string]int{
|
||||
"http://foo.bar.com": http.StatusOK,
|
||||
"http://bar.com": http.StatusOK,
|
||||
"http://fooubar.com": http.StatusNotFound,
|
||||
"http://barucom": http.StatusNotFound,
|
||||
"http://barcom": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Methods with GET",
|
||||
rule: "Method: GET",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Methods with GET and POST",
|
||||
rule: "Method: GET,POST",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Methods with POST",
|
||||
rule: "Method: POST",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusMethodNotAllowed,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Header with matching header",
|
||||
rule: "Headers: Content-Type,application/json",
|
||||
headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Header without matching header",
|
||||
rule: "Headers: Content-Type,application/foo",
|
||||
headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "HeaderRegExp with matching header",
|
||||
rule: "HeadersRegexp: Content-Type, application/(text|json)",
|
||||
headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "HeaderRegExp without matching header",
|
||||
rule: "HeadersRegexp: Content-Type, application/(text|json)",
|
||||
headers: map[string]string{
|
||||
"Content-Type": "application/foo",
|
||||
},
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "HeaderRegExp with matching second header",
|
||||
rule: "HeadersRegexp: Content-Type, application/(text|json)",
|
||||
headers: map[string]string{
|
||||
"Content-Type": "application/text",
|
||||
},
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Query with multiple params",
|
||||
rule: "Query: foo=bar, bar=baz",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo?foo=bar&bar=baz": http.StatusOK,
|
||||
"http://localhost/foo?bar=baz": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Invalid rule syntax",
|
||||
rule: "Query:param_one=true, /path2;Path: /path1",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo?bar=baz": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
|
||||
router := mux.NewRouter()
|
||||
router.SkipClean(true)
|
||||
|
||||
err := addRoute(context.Background(), router, test.rule, 0, handler)
|
||||
require.NoError(t, err)
|
||||
|
||||
// RequestDecorator is necessary for the host rule
|
||||
reqHost := requestdecorator.New(nil)
|
||||
|
||||
results := make(map[string]int)
|
||||
for calledURL := range test.expected {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
req := testhelpers.MustNewRequest(http.MethodGet, calledURL, nil)
|
||||
for key, value := range test.headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
reqHost.ServeHTTP(w, req, router.ServeHTTP)
|
||||
results[calledURL] = w.Code
|
||||
}
|
||||
assert.Equal(t, test.expected, results)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_addRoutePriority(t *testing.T) {
|
||||
type Case struct {
|
||||
xFrom string
|
||||
rule string
|
||||
priority int
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
path string
|
||||
cases []Case
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "Higher priority on second rule",
|
||||
path: "/my",
|
||||
cases: []Case{
|
||||
{
|
||||
xFrom: "header1",
|
||||
rule: "PathPrefix:/my",
|
||||
priority: 10,
|
||||
},
|
||||
{
|
||||
xFrom: "header2",
|
||||
rule: "PathPrefix:/my",
|
||||
priority: 20,
|
||||
},
|
||||
},
|
||||
expected: "header2",
|
||||
},
|
||||
{
|
||||
desc: "Higher priority on first rule",
|
||||
path: "/my",
|
||||
cases: []Case{
|
||||
{
|
||||
xFrom: "header1",
|
||||
rule: "PathPrefix:/my",
|
||||
priority: 20,
|
||||
},
|
||||
{
|
||||
xFrom: "header2",
|
||||
rule: "PathPrefix:/my",
|
||||
priority: 10,
|
||||
},
|
||||
},
|
||||
expected: "header1",
|
||||
},
|
||||
{
|
||||
desc: "Higher priority on second rule with different rule",
|
||||
path: "/mypath",
|
||||
cases: []Case{
|
||||
{
|
||||
xFrom: "header1",
|
||||
rule: "PathPrefix:/mypath",
|
||||
priority: 10,
|
||||
},
|
||||
{
|
||||
xFrom: "header2",
|
||||
rule: "PathPrefix:/my",
|
||||
priority: 20,
|
||||
},
|
||||
},
|
||||
expected: "header2",
|
||||
},
|
||||
{
|
||||
desc: "Higher priority on longest rule (longest first)",
|
||||
path: "/mypath",
|
||||
cases: []Case{
|
||||
{
|
||||
xFrom: "header1",
|
||||
rule: "PathPrefix:/mypath",
|
||||
},
|
||||
{
|
||||
xFrom: "header2",
|
||||
rule: "PathPrefix:/my",
|
||||
},
|
||||
},
|
||||
expected: "header1",
|
||||
},
|
||||
{
|
||||
desc: "Higher priority on longest rule (longest second)",
|
||||
path: "/mypath",
|
||||
cases: []Case{
|
||||
{
|
||||
xFrom: "header1",
|
||||
rule: "PathPrefix:/my",
|
||||
},
|
||||
{
|
||||
xFrom: "header2",
|
||||
rule: "PathPrefix:/mypath",
|
||||
},
|
||||
},
|
||||
expected: "header2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
router := mux.NewRouter()
|
||||
|
||||
for _, route := range test.cases {
|
||||
route := route
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-From", route.xFrom)
|
||||
})
|
||||
err := addRoute(context.Background(), router, route.rule, route.priority, handler)
|
||||
require.NoError(t, err, route)
|
||||
}
|
||||
|
||||
router.SortRoutes()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := testhelpers.MustNewRequest(http.MethodGet, test.path, nil)
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, test.expected, w.Header().Get("X-From"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostRegexp(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
hostExp string
|
||||
urls map[string]bool
|
||||
}{
|
||||
{
|
||||
desc: "capturing group",
|
||||
hostExp: "{subdomain:(foo\\.)?bar\\.com}",
|
||||
urls: map[string]bool{
|
||||
"http://foo.bar.com": true,
|
||||
"http://bar.com": true,
|
||||
"http://fooubar.com": false,
|
||||
"http://barucom": false,
|
||||
"http://barcom": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "non capturing group",
|
||||
hostExp: "{subdomain:(?:foo\\.)?bar\\.com}",
|
||||
urls: map[string]bool{
|
||||
"http://foo.bar.com": true,
|
||||
"http://bar.com": true,
|
||||
"http://fooubar.com": false,
|
||||
"http://barucom": false,
|
||||
"http://barcom": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "regex insensitive",
|
||||
hostExp: "{dummy:[A-Za-z-]+\\.bar\\.com}",
|
||||
urls: map[string]bool{
|
||||
"http://FOO.bar.com": true,
|
||||
"http://foo.bar.com": true,
|
||||
"http://fooubar.com": false,
|
||||
"http://barucom": false,
|
||||
"http://barcom": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "insensitive host",
|
||||
hostExp: "{dummy:[a-z-]+\\.bar\\.com}",
|
||||
urls: map[string]bool{
|
||||
"http://FOO.bar.com": true,
|
||||
"http://foo.bar.com": true,
|
||||
"http://fooubar.com": false,
|
||||
"http://barucom": false,
|
||||
"http://barcom": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "insensitive host simple",
|
||||
hostExp: "foo.bar.com",
|
||||
urls: map[string]bool{
|
||||
"http://FOO.bar.com": true,
|
||||
"http://foo.bar.com": true,
|
||||
"http://fooubar.com": false,
|
||||
"http://barucom": false,
|
||||
"http://barcom": false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rt := &mux.Route{}
|
||||
hostRegexp(rt, test.hostExp)
|
||||
|
||||
for testURL, match := range test.urls {
|
||||
req := testhelpers.MustNewRequest(http.MethodGet, testURL, nil)
|
||||
assert.Equal(t, match, rt.Match(req, &mux.RouteMatch{}), testURL)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue