Enables the use of elements declared in other providers
This commit is contained in:
parent
a79d6aa669
commit
8735263930
16 changed files with 753 additions and 94 deletions
24
integration/fixtures/multiprovider.toml
Normal file
24
integration/fixtures/multiprovider.toml
Normal file
|
@ -0,0 +1,24 @@
|
|||
[log]
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[api]
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":8000"
|
||||
|
||||
[Providers]
|
||||
[Providers.Rest]
|
||||
|
||||
[Providers.File]
|
||||
|
||||
[Services]
|
||||
[Services.service]
|
||||
[Services.service.LoadBalancer]
|
||||
|
||||
[[Services.service.LoadBalancer.Servers]]
|
||||
URL = "{{.Server}}"
|
||||
Weight = 1
|
||||
[Middlewares]
|
||||
[Middlewares.customheader.Headers.CustomRequestHeaders]
|
||||
X-Custom="CustomValue"
|
|
@ -1,6 +1,8 @@
|
|||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -9,6 +11,7 @@ import (
|
|||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/containous/traefik/config"
|
||||
"github.com/containous/traefik/integration/try"
|
||||
"github.com/go-check/check"
|
||||
checker "github.com/vdemeester/shakers"
|
||||
|
@ -434,3 +437,53 @@ func (s *SimpleSuite) TestKeepTrailingSlash(c *check.C) {
|
|||
|
||||
http.DefaultClient.CheckRedirect = oldCheckRedirect
|
||||
}
|
||||
|
||||
func (s *SimpleSuite) TestMultiprovider(c *check.C) {
|
||||
s.createComposeProject(c, "base")
|
||||
s.composeProject.Start(c)
|
||||
|
||||
server := "http://" + s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress
|
||||
|
||||
file := s.adaptFile(c, "fixtures/multiprovider.toml", struct {
|
||||
Server string
|
||||
}{Server: server})
|
||||
defer os.Remove(file)
|
||||
|
||||
cmd, output := s.traefikCmd(withConfigFile(file))
|
||||
defer output(c)
|
||||
|
||||
err := cmd.Start()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cmd.Process.Kill()
|
||||
|
||||
err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/services", 1000*time.Millisecond, try.BodyContains("service"))
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
config := config.Configuration{
|
||||
Routers: map[string]*config.Router{
|
||||
"router1": {
|
||||
EntryPoints: []string{"http"},
|
||||
Middlewares: []string{"file.customheader"},
|
||||
Service: "file.service",
|
||||
Rule: "PathPrefix:/",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
json, err := json.Marshal(config)
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
request, err := http.NewRequest(http.MethodPut, "http://127.0.0.1:8080/api/providers/rest", bytes.NewReader(json))
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(response.StatusCode, checker.Equals, http.StatusOK)
|
||||
|
||||
err = try.GetRequest("http://127.0.0.1:8080/api/providers/rest/routers", 1000*time.Millisecond, try.BodyContains("PathPrefix:/"))
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
err = try.GetRequest("http://127.0.0.1:8000/", 1*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("CustomValue"))
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ func (s *TracingSuite) TestZipkinRateLimit(c *check.C) {
|
|||
err = try.GetRequest("http://127.0.0.1:8000/ratelimit", 500*time.Millisecond, try.StatusCodeIs(http.StatusTooManyRequests))
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
err = try.GetRequest("http://"+s.ZipkinIP+":9411/api/v2/spans?serviceName=tracing", 20*time.Second, try.BodyContains("forward service1/router1", "ratelimit"))
|
||||
err = try.GetRequest("http://"+s.ZipkinIP+":9411/api/v2/spans?serviceName=tracing", 20*time.Second, try.BodyContains("forward service1/file.router1", "file.ratelimit"))
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
}
|
||||
|
@ -109,7 +109,7 @@ func (s *TracingSuite) TestZipkinRetry(c *check.C) {
|
|||
err = try.GetRequest("http://127.0.0.1:8000/retry", 500*time.Millisecond, try.StatusCodeIs(http.StatusBadGateway))
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
err = try.GetRequest("http://"+s.ZipkinIP+":9411/api/v2/spans?serviceName=tracing", 20*time.Second, try.BodyContains("forward service2/router2", "retry"))
|
||||
err = try.GetRequest("http://"+s.ZipkinIP+":9411/api/v2/spans?serviceName=tracing", 20*time.Second, try.BodyContains("forward service2/file.router2", "file.retry"))
|
||||
c.Assert(err, checker.IsNil)
|
||||
}
|
||||
|
||||
|
@ -132,6 +132,6 @@ func (s *TracingSuite) TestZipkinAuth(c *check.C) {
|
|||
err = try.GetRequest("http://127.0.0.1:8000/auth", 500*time.Millisecond, try.StatusCodeIs(http.StatusUnauthorized))
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
err = try.GetRequest("http://"+s.ZipkinIP+":9411/api/v2/spans?serviceName=tracing", 20*time.Second, try.BodyContains("entrypoint http", "basic-auth"))
|
||||
err = try.GetRequest("http://"+s.ZipkinIP+":9411/api/v2/spans?serviceName=tracing", 20*time.Second, try.BodyContains("entrypoint http", "file.basic-auth"))
|
||||
c.Assert(err, checker.IsNil)
|
||||
}
|
||||
|
|
|
@ -14,17 +14,13 @@ const (
|
|||
)
|
||||
|
||||
type chainBuilder interface {
|
||||
BuildChain(ctx context.Context, middlewares []string) (*alice.Chain, error)
|
||||
BuildChain(ctx context.Context, middlewares []string) *alice.Chain
|
||||
}
|
||||
|
||||
// New creates a chain middleware
|
||||
func New(ctx context.Context, next http.Handler, config config.Chain, builder chainBuilder, name string) (http.Handler, error) {
|
||||
middlewares.GetLogger(ctx, name, typeName).Debug("Creating middleware")
|
||||
|
||||
middlewareChain, err := builder.BuildChain(ctx, config.Middlewares)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
middlewareChain := builder.BuildChain(ctx, config.Middlewares)
|
||||
return middlewareChain.Then(next)
|
||||
}
|
||||
|
|
29
server/aggregator.go
Normal file
29
server/aggregator.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"github.com/containous/traefik/config"
|
||||
"github.com/containous/traefik/server/internal"
|
||||
)
|
||||
|
||||
func mergeConfiguration(configurations config.Configurations) config.Configuration {
|
||||
conf := config.Configuration{
|
||||
Routers: make(map[string]*config.Router),
|
||||
Middlewares: make(map[string]*config.Middleware),
|
||||
Services: make(map[string]*config.Service),
|
||||
}
|
||||
|
||||
for provider, configuration := range configurations {
|
||||
for routerName, router := range configuration.Routers {
|
||||
conf.Routers[internal.MakeQualifiedName(provider, routerName)] = router
|
||||
}
|
||||
for middlewareName, middleware := range configuration.Middlewares {
|
||||
conf.Middlewares[internal.MakeQualifiedName(provider, middlewareName)] = middleware
|
||||
}
|
||||
for serviceName, service := range configuration.Services {
|
||||
conf.Services[internal.MakeQualifiedName(provider, serviceName)] = service
|
||||
}
|
||||
conf.TLS = append(conf.TLS, configuration.TLS...)
|
||||
}
|
||||
|
||||
return conf
|
||||
}
|
103
server/aggregator_test.go
Normal file
103
server/aggregator_test.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/containous/traefik/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAggregator(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
given config.Configurations
|
||||
expected config.Configuration
|
||||
}{
|
||||
{
|
||||
desc: "Nil returns an empty configuration",
|
||||
given: nil,
|
||||
expected: config.Configuration{
|
||||
Routers: make(map[string]*config.Router),
|
||||
Middlewares: make(map[string]*config.Middleware),
|
||||
Services: make(map[string]*config.Service),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Returns fully qualified elements from a mono-provider configuration map",
|
||||
given: config.Configurations{
|
||||
"provider-1": &config.Configuration{
|
||||
Routers: map[string]*config.Router{
|
||||
"router-1": {},
|
||||
},
|
||||
Middlewares: map[string]*config.Middleware{
|
||||
"middleware-1": {},
|
||||
},
|
||||
Services: map[string]*config.Service{
|
||||
"service-1": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: config.Configuration{
|
||||
Routers: map[string]*config.Router{
|
||||
"provider-1.router-1": {},
|
||||
},
|
||||
Middlewares: map[string]*config.Middleware{
|
||||
"provider-1.middleware-1": {},
|
||||
},
|
||||
Services: map[string]*config.Service{
|
||||
"provider-1.service-1": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Returns fully qualified elements from a multi-provider configuration map",
|
||||
given: config.Configurations{
|
||||
"provider-1": &config.Configuration{
|
||||
Routers: map[string]*config.Router{
|
||||
"router-1": {},
|
||||
},
|
||||
Middlewares: map[string]*config.Middleware{
|
||||
"middleware-1": {},
|
||||
},
|
||||
Services: map[string]*config.Service{
|
||||
"service-1": {},
|
||||
},
|
||||
},
|
||||
"provider-2": &config.Configuration{
|
||||
Routers: map[string]*config.Router{
|
||||
"router-1": {},
|
||||
},
|
||||
Middlewares: map[string]*config.Middleware{
|
||||
"middleware-1": {},
|
||||
},
|
||||
Services: map[string]*config.Service{
|
||||
"service-1": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: config.Configuration{
|
||||
Routers: map[string]*config.Router{
|
||||
"provider-1.router-1": {},
|
||||
"provider-2.router-1": {},
|
||||
},
|
||||
Middlewares: map[string]*config.Middleware{
|
||||
"provider-1.middleware-1": {},
|
||||
"provider-2.middleware-1": {},
|
||||
},
|
||||
Services: map[string]*config.Service{
|
||||
"provider-1.service-1": {},
|
||||
"provider-2.service-1": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
actual := mergeConfiguration(test.given)
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
45
server/internal/provider.go
Normal file
45
server/internal/provider.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/containous/traefik/log"
|
||||
)
|
||||
|
||||
type contextKey int
|
||||
|
||||
const (
|
||||
providerKey contextKey = iota
|
||||
)
|
||||
|
||||
// AddProviderInContext Adds the provider name in the context
|
||||
func AddProviderInContext(ctx context.Context, elementName string) context.Context {
|
||||
parts := strings.Split(elementName, ".")
|
||||
if len(parts) == 1 {
|
||||
log.FromContext(ctx).Debugf("Could not find a provider for %s.", elementName)
|
||||
return ctx
|
||||
}
|
||||
|
||||
if name, ok := ctx.Value(providerKey).(string); ok && name == parts[0] {
|
||||
return ctx
|
||||
}
|
||||
|
||||
return context.WithValue(ctx, providerKey, parts[0])
|
||||
}
|
||||
|
||||
// GetQualifiedName Gets the fully qualified name.
|
||||
func GetQualifiedName(ctx context.Context, elementName string) string {
|
||||
parts := strings.Split(elementName, ".")
|
||||
if len(parts) == 1 {
|
||||
if providerName, ok := ctx.Value(providerKey).(string); ok {
|
||||
return MakeQualifiedName(providerName, parts[0])
|
||||
}
|
||||
}
|
||||
return elementName
|
||||
}
|
||||
|
||||
// MakeQualifiedName Creates a qualified name for an element
|
||||
func MakeQualifiedName(providerName string, elementName string) string {
|
||||
return providerName + "." + elementName
|
||||
}
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/containous/alice"
|
||||
"github.com/containous/traefik/config"
|
||||
|
@ -26,9 +27,16 @@ import (
|
|||
"github.com/containous/traefik/middlewares/stripprefix"
|
||||
"github.com/containous/traefik/middlewares/stripprefixregex"
|
||||
"github.com/containous/traefik/middlewares/tracing"
|
||||
"github.com/containous/traefik/server/internal"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type middlewareStackType int
|
||||
|
||||
const (
|
||||
middlewareStackKey middlewareStackType = iota
|
||||
)
|
||||
|
||||
// Builder the middleware builder
|
||||
type Builder struct {
|
||||
configs map[string]*config.Middleware
|
||||
|
@ -45,22 +53,40 @@ func NewBuilder(configs map[string]*config.Middleware, serviceBuilder serviceBui
|
|||
}
|
||||
|
||||
// BuildChain creates a middleware chain
|
||||
func (b *Builder) BuildChain(ctx context.Context, middlewares []string) (*alice.Chain, error) {
|
||||
func (b *Builder) BuildChain(ctx context.Context, middlewares []string) *alice.Chain {
|
||||
chain := alice.New()
|
||||
for _, middlewareName := range middlewares {
|
||||
middlewareName := internal.GetQualifiedName(ctx, middlewareName)
|
||||
constructorContext := internal.AddProviderInContext(ctx, middlewareName)
|
||||
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
|
||||
var err error
|
||||
if constructorContext, err = checkRecursivity(constructorContext, middlewareName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, ok := b.configs[middlewareName]; !ok {
|
||||
return nil, fmt.Errorf("middleware %q does not exist", middlewareName)
|
||||
}
|
||||
|
||||
constructor, err := b.buildConstructor(ctx, middlewareName, *b.configs[middlewareName])
|
||||
constructor, err := b.buildConstructor(constructorContext, middlewareName, *b.configs[middlewareName])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("error while instanciation of %s: %v", middlewareName, err)
|
||||
}
|
||||
if constructor != nil {
|
||||
chain = chain.Append(constructor)
|
||||
return constructor(next)
|
||||
})
|
||||
}
|
||||
return &chain
|
||||
}
|
||||
|
||||
func checkRecursivity(ctx context.Context, middlewareName string) (context.Context, error) {
|
||||
currentStack, ok := ctx.Value(middlewareStackKey).([]string)
|
||||
if !ok {
|
||||
currentStack = []string{}
|
||||
}
|
||||
return &chain, nil
|
||||
if inSlice(middlewareName, currentStack) {
|
||||
return ctx, fmt.Errorf("could not instantiate middleware %s: recursion detected in %s", middlewareName, strings.Join(append(currentStack, middlewareName), "->"))
|
||||
}
|
||||
return context.WithValue(ctx, middlewareStackKey, append(currentStack, middlewareName)), nil
|
||||
}
|
||||
|
||||
func (b *Builder) buildConstructor(ctx context.Context, middlewareName string, config config.Middleware) (alice.Constructor, error) {
|
||||
|
@ -290,3 +316,12 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string, c
|
|||
|
||||
return tracing.Wrap(ctx, middleware), nil
|
||||
}
|
||||
|
||||
func inSlice(element string, stack []string) bool {
|
||||
for _, value := range stack {
|
||||
if value == element {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -3,9 +3,13 @@ package middleware
|
|||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/containous/traefik/config"
|
||||
"github.com/containous/traefik/server/internal"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -67,10 +71,8 @@ func TestMiddlewaresRegistry_BuildChainNilConfig(t *testing.T) {
|
|||
}
|
||||
middlewaresBuilder := NewBuilder(testConfig, nil)
|
||||
|
||||
chain, err := middlewaresBuilder.BuildChain(context.Background(), []string{"empty"})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = chain.Then(nil)
|
||||
chain := middlewaresBuilder.BuildChain(context.Background(), []string{"empty"})
|
||||
_, err := chain.Then(nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
@ -125,3 +127,254 @@ func TestMiddlewaresRegistry_BuildMiddlewareAddPrefix(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChainWithContext(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
buildChain []string
|
||||
configuration map[string]*config.Middleware
|
||||
expected map[string]string
|
||||
contextProvider string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
desc: "Simple middleware",
|
||||
buildChain: []string{"middleware-1"},
|
||||
configuration: map[string]*config.Middleware{
|
||||
"middleware-1": {
|
||||
Headers: &config.Headers{
|
||||
CustomRequestHeaders: map[string]string{"middleware-1": "value-middleware-1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]string{"middleware-1": "value-middleware-1"},
|
||||
},
|
||||
{
|
||||
desc: "Middleware that references a chain",
|
||||
buildChain: []string{"middleware-chain-1"},
|
||||
configuration: map[string]*config.Middleware{
|
||||
"middleware-1": {
|
||||
Headers: &config.Headers{
|
||||
CustomRequestHeaders: map[string]string{"middleware-1": "value-middleware-1"},
|
||||
},
|
||||
},
|
||||
"middleware-chain-1": {
|
||||
Chain: &config.Chain{
|
||||
Middlewares: []string{"middleware-1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]string{"middleware-1": "value-middleware-1"},
|
||||
},
|
||||
{
|
||||
desc: "Should prefix the middlewareName with the provider in the context",
|
||||
buildChain: []string{"middleware-1"},
|
||||
configuration: map[string]*config.Middleware{
|
||||
"provider-1.middleware-1": {
|
||||
Headers: &config.Headers{
|
||||
CustomRequestHeaders: map[string]string{"provider-1.middleware-1": "value-middleware-1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]string{"provider-1.middleware-1": "value-middleware-1"},
|
||||
contextProvider: "provider-1",
|
||||
},
|
||||
{
|
||||
desc: "Should not prefix a qualified middlewareName with the provider in the context",
|
||||
buildChain: []string{"provider-1.middleware-1"},
|
||||
configuration: map[string]*config.Middleware{
|
||||
"provider-1.middleware-1": {
|
||||
Headers: &config.Headers{
|
||||
CustomRequestHeaders: map[string]string{"provider-1.middleware-1": "value-middleware-1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]string{"provider-1.middleware-1": "value-middleware-1"},
|
||||
contextProvider: "provider-1",
|
||||
},
|
||||
{
|
||||
desc: "Should be context aware if a chain references another middleware",
|
||||
buildChain: []string{"provider-1.middleware-chain-1"},
|
||||
configuration: map[string]*config.Middleware{
|
||||
"provider-1.middleware-1": {
|
||||
Headers: &config.Headers{
|
||||
CustomRequestHeaders: map[string]string{"middleware-1": "value-middleware-1"},
|
||||
},
|
||||
},
|
||||
"provider-1.middleware-chain-1": {
|
||||
Chain: &config.Chain{
|
||||
Middlewares: []string{"middleware-1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]string{"middleware-1": "value-middleware-1"},
|
||||
},
|
||||
{
|
||||
desc: "Should handle nested chains with different context",
|
||||
buildChain: []string{"provider-1.middleware-chain-1", "middleware-chain-1"},
|
||||
configuration: map[string]*config.Middleware{
|
||||
"provider-1.middleware-1": {
|
||||
Headers: &config.Headers{
|
||||
CustomRequestHeaders: map[string]string{"middleware-1": "value-middleware-1"},
|
||||
},
|
||||
},
|
||||
"provider-1.middleware-2": {
|
||||
Headers: &config.Headers{
|
||||
CustomRequestHeaders: map[string]string{"middleware-2": "value-middleware-2"},
|
||||
},
|
||||
},
|
||||
"provider-1.middleware-chain-1": {
|
||||
Chain: &config.Chain{
|
||||
Middlewares: []string{"middleware-1"},
|
||||
},
|
||||
},
|
||||
"provider-1.middleware-chain-2": {
|
||||
Chain: &config.Chain{
|
||||
Middlewares: []string{"middleware-2"},
|
||||
},
|
||||
},
|
||||
"provider-2.middleware-chain-1": {
|
||||
Chain: &config.Chain{
|
||||
Middlewares: []string{"provider-1.middleware-2", "provider-1.middleware-chain-2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]string{"middleware-1": "value-middleware-1", "middleware-2": "value-middleware-2"},
|
||||
contextProvider: "provider-2",
|
||||
},
|
||||
{
|
||||
desc: "Detects recursion in Middleware chain",
|
||||
buildChain: []string{"m1"},
|
||||
configuration: map[string]*config.Middleware{
|
||||
"ok": {
|
||||
Retry: &config.Retry{},
|
||||
},
|
||||
"m1": {
|
||||
Chain: &config.Chain{
|
||||
Middlewares: []string{"m2"},
|
||||
},
|
||||
},
|
||||
"m2": {
|
||||
Chain: &config.Chain{
|
||||
Middlewares: []string{"ok", "m3"},
|
||||
},
|
||||
},
|
||||
"m3": {
|
||||
Chain: &config.Chain{
|
||||
Middlewares: []string{"m1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: errors.New("could not instantiate middleware m1: recursion detected in m1->m2->m3->m1"),
|
||||
},
|
||||
{
|
||||
desc: "Detects recursion in Middleware chain",
|
||||
buildChain: []string{"provider.m1"},
|
||||
configuration: map[string]*config.Middleware{
|
||||
"provider2.ok": {
|
||||
Retry: &config.Retry{},
|
||||
},
|
||||
"provider.m1": {
|
||||
Chain: &config.Chain{
|
||||
Middlewares: []string{"provider2.m2"},
|
||||
},
|
||||
},
|
||||
"provider2.m2": {
|
||||
Chain: &config.Chain{
|
||||
Middlewares: []string{"ok", "provider.m3"},
|
||||
},
|
||||
},
|
||||
"provider.m3": {
|
||||
Chain: &config.Chain{
|
||||
Middlewares: []string{"m1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: errors.New("could not instantiate middleware provider.m1: recursion detected in provider.m1->provider2.m2->provider.m3->provider.m1"),
|
||||
},
|
||||
{
|
||||
buildChain: []string{"ok", "m0"},
|
||||
configuration: map[string]*config.Middleware{
|
||||
"ok": {
|
||||
Retry: &config.Retry{},
|
||||
},
|
||||
"m0": {
|
||||
Chain: &config.Chain{
|
||||
Middlewares: []string{"m0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: errors.New("could not instantiate middleware m0: recursion detected in m0->m0"),
|
||||
},
|
||||
{
|
||||
desc: "Detects MiddlewareChain that references a Chain that references a Chain with a missing middleware",
|
||||
buildChain: []string{"m0"},
|
||||
configuration: map[string]*config.Middleware{
|
||||
"m0": {
|
||||
Chain: &config.Chain{
|
||||
Middlewares: []string{"m1"},
|
||||
},
|
||||
},
|
||||
"m1": {
|
||||
Chain: &config.Chain{
|
||||
Middlewares: []string{"m2"},
|
||||
},
|
||||
},
|
||||
"m2": {
|
||||
Chain: &config.Chain{
|
||||
Middlewares: []string{"m3"},
|
||||
},
|
||||
},
|
||||
"m3": {
|
||||
Chain: &config.Chain{
|
||||
Middlewares: []string{"m2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: errors.New("could not instantiate middleware m2: recursion detected in m0->m1->m2->m3->m2"),
|
||||
},
|
||||
{
|
||||
desc: "--",
|
||||
buildChain: []string{"m0"},
|
||||
configuration: map[string]*config.Middleware{
|
||||
"m0": {
|
||||
Chain: &config.Chain{
|
||||
Middlewares: []string{"m0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: errors.New("could not instantiate middleware m0: recursion detected in m0->m0"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
if len(test.contextProvider) > 0 {
|
||||
ctx = internal.AddProviderInContext(ctx, test.contextProvider+".foobar")
|
||||
}
|
||||
|
||||
builder := NewBuilder(test.configuration, nil)
|
||||
|
||||
result := builder.BuildChain(ctx, test.buildChain)
|
||||
|
||||
handlers, err := result.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }))
|
||||
if test.expectedError != nil {
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, test.expectedError.Error(), err.Error())
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
recorder := httptest.NewRecorder()
|
||||
request, _ := http.NewRequest(http.MethodGet, "http://foo/", nil)
|
||||
handlers.ServeHTTP(recorder, request)
|
||||
|
||||
for key, value := range test.expected {
|
||||
assert.Equal(t, value, request.Header.Get(key))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,13 +15,11 @@ import (
|
|||
|
||||
// chainBuilder The contract of the middleware builder
|
||||
type chainBuilder interface {
|
||||
BuildChain(ctx context.Context, middlewares []string) (*alice.Chain, error)
|
||||
BuildChain(ctx context.Context, middlewares []string) *alice.Chain
|
||||
}
|
||||
|
||||
// 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{}
|
||||
|
||||
if conf.Providers != nil && conf.Providers.Rest != nil {
|
||||
|
@ -29,10 +27,7 @@ func NewRouteAppenderAggregator(ctx context.Context, chainBuilder chainBuilder,
|
|||
}
|
||||
|
||||
if conf.API != nil && conf.API.EntryPoint == entryPointName {
|
||||
chain, err := chainBuilder.BuildChain(ctx, conf.API.Middlewares)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
} else {
|
||||
chain := chainBuilder.BuildChain(ctx, conf.API.Middlewares)
|
||||
aggregator.AddAppender(&WithMiddleware{
|
||||
appender: api.Handler{
|
||||
EntryPoint: conf.API.EntryPoint,
|
||||
|
@ -44,32 +39,24 @@ func NewRouteAppenderAggregator(ctx context.Context, chainBuilder chainBuilder,
|
|||
},
|
||||
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 {
|
||||
chain := chainBuilder.BuildChain(ctx, conf.Ping.Middlewares)
|
||||
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 {
|
||||
chain := chainBuilder.BuildChain(ctx, conf.Metrics.Prometheus.Middlewares)
|
||||
aggregator.AddAppender(&WithMiddleware{
|
||||
appender: metrics.PrometheusHandler{},
|
||||
routerMiddlewares: chain,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return aggregator
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ type ChainBuilderMock struct {
|
|||
middles map[string]alice.Constructor
|
||||
}
|
||||
|
||||
func (c *ChainBuilderMock) BuildChain(ctx context.Context, middles []string) (*alice.Chain, error) {
|
||||
func (c *ChainBuilderMock) BuildChain(ctx context.Context, middles []string) *alice.Chain {
|
||||
chain := alice.New()
|
||||
|
||||
for _, mName := range middles {
|
||||
|
@ -26,7 +26,7 @@ func (c *ChainBuilderMock) BuildChain(ctx context.Context, middles []string) (*a
|
|||
}
|
||||
}
|
||||
|
||||
return &chain, nil
|
||||
return &chain
|
||||
}
|
||||
|
||||
func TestNewRouteAppenderAggregator(t *testing.T) {
|
||||
|
@ -61,7 +61,7 @@ func TestNewRouteAppenderAggregator(t *testing.T) {
|
|||
expected: map[string]int{
|
||||
"/wrong": http.StatusBadGateway,
|
||||
"/ping": http.StatusOK,
|
||||
//"/.well-known/acme-challenge/token": http.StatusNotFound, // FIXME
|
||||
// "/.well-known/acme-challenge/token": http.StatusNotFound, // FIXME
|
||||
"/api/providers": http.StatusUnauthorized,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/containous/traefik/middlewares/recovery"
|
||||
"github.com/containous/traefik/middlewares/tracing"
|
||||
"github.com/containous/traefik/responsemodifiers"
|
||||
"github.com/containous/traefik/server/internal"
|
||||
"github.com/containous/traefik/server/middleware"
|
||||
"github.com/containous/traefik/server/service"
|
||||
)
|
||||
|
@ -104,9 +105,11 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
|
|||
SkipClean(true)
|
||||
|
||||
for routerName, routerConfig := range configs {
|
||||
ctx = log.With(ctx, log.Str(log.RouterName, routerName))
|
||||
ctx := log.With(ctx, log.Str(log.RouterName, routerName))
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
ctx = internal.AddProviderInContext(ctx, routerName)
|
||||
|
||||
handler, err := m.buildRouterHandler(ctx, routerName)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
|
@ -166,10 +169,7 @@ func (m *Manager) buildHandler(ctx context.Context, router *config.Router, route
|
|||
return nil, err
|
||||
}
|
||||
|
||||
mHandler, err := m.middlewaresBuilder.BuildChain(ctx, router.Middlewares)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mHandler := m.middlewaresBuilder.BuildChain(ctx, router.Middlewares)
|
||||
|
||||
alHandler := func(next http.Handler) (http.Handler, error) {
|
||||
return accesslog.NewFieldHandler(next, accesslog.ServiceName, router.Service, accesslog.AddServiceFields), nil
|
||||
|
|
|
@ -209,6 +209,102 @@ func TestRouterManager_Get(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "no middleware with provider name",
|
||||
routersConfig: map[string]*config.Router{
|
||||
"provider-1.foo": {
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "foo-service",
|
||||
Rule: "Host:foo.bar",
|
||||
},
|
||||
},
|
||||
serviceConfig: map[string]*config.Service{
|
||||
"provider-1.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 with specified provider name",
|
||||
routersConfig: map[string]*config.Router{
|
||||
"provider-1.foo": {
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "provider-2.foo-service",
|
||||
Rule: "Host:foo.bar",
|
||||
},
|
||||
},
|
||||
serviceConfig: map[string]*config.Service{
|
||||
"provider-2.foo-service": {
|
||||
LoadBalancer: &config.LoadBalancerService{
|
||||
Servers: []config.Server{
|
||||
{
|
||||
URL: server.URL,
|
||||
Weight: 1,
|
||||
},
|
||||
},
|
||||
Method: "wrr",
|
||||
},
|
||||
},
|
||||
},
|
||||
entryPoints: []string{"web"},
|
||||
expected: ExpectedResult{StatusCode: http.StatusOK},
|
||||
},
|
||||
{
|
||||
desc: "middleware: chain with provider name",
|
||||
routersConfig: map[string]*config.Router{
|
||||
"provider-1.foo": {
|
||||
EntryPoints: []string{"web"},
|
||||
Middlewares: []string{"provider-2.chain-middle", "headers-middle"},
|
||||
Service: "foo-service",
|
||||
Rule: "Host:foo.bar",
|
||||
},
|
||||
},
|
||||
serviceConfig: map[string]*config.Service{
|
||||
"provider-1.foo-service": {
|
||||
LoadBalancer: &config.LoadBalancerService{
|
||||
Servers: []config.Server{
|
||||
{
|
||||
URL: server.URL,
|
||||
Weight: 1,
|
||||
},
|
||||
},
|
||||
Method: "wrr",
|
||||
},
|
||||
},
|
||||
},
|
||||
middlewaresConfig: map[string]*config.Middleware{
|
||||
"provider-2.chain-middle": {
|
||||
Chain: &config.Chain{Middlewares: []string{"auth-middle"}},
|
||||
},
|
||||
"provider-2.auth-middle": {
|
||||
BasicAuth: &config.BasicAuth{
|
||||
Users: []string{"toto:titi"},
|
||||
},
|
||||
},
|
||||
"provider-1.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 {
|
||||
|
|
|
@ -76,28 +76,7 @@ func (s *Server) loadConfig(configurations config.Configurations) (map[string]ht
|
|||
|
||||
ctx := context.TODO()
|
||||
|
||||
// FIXME manage duplicates
|
||||
conf := config.Configuration{
|
||||
Routers: make(map[string]*config.Router),
|
||||
Middlewares: make(map[string]*config.Middleware),
|
||||
Services: make(map[string]*config.Service),
|
||||
}
|
||||
for _, config := range configurations {
|
||||
for key, value := range config.Middlewares {
|
||||
conf.Middlewares[key] = value
|
||||
}
|
||||
|
||||
for key, value := range config.Services {
|
||||
conf.Services[key] = value
|
||||
}
|
||||
|
||||
for key, value := range config.Routers {
|
||||
conf.Routers[key] = value
|
||||
}
|
||||
|
||||
conf.TLS = append(conf.TLS, config.TLS...)
|
||||
}
|
||||
|
||||
conf := mergeConfiguration(configurations)
|
||||
handlers := s.applyConfiguration(ctx, conf)
|
||||
|
||||
// Get new certificates list sorted per entry points
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/containous/traefik/middlewares/emptybackendhandler"
|
||||
"github.com/containous/traefik/old/middlewares/pipelining"
|
||||
"github.com/containous/traefik/server/cookie"
|
||||
"github.com/containous/traefik/server/internal"
|
||||
"github.com/vulcand/oxy/forward"
|
||||
"github.com/vulcand/oxy/roundrobin"
|
||||
)
|
||||
|
@ -58,9 +59,11 @@ type Manager struct {
|
|||
func (m *Manager) Build(rootCtx context.Context, serviceName string, responseModifier func(*http.Response) error) (http.Handler, error) {
|
||||
ctx := log.With(rootCtx, log.Str(log.ServiceName, serviceName))
|
||||
|
||||
// TODO refactor ?
|
||||
serviceName = internal.GetQualifiedName(ctx, serviceName)
|
||||
ctx = internal.AddProviderInContext(ctx, serviceName)
|
||||
|
||||
if conf, ok := m.configs[serviceName]; ok {
|
||||
// FIXME Should handle multiple service types
|
||||
// TODO Should handle multiple service types
|
||||
if conf.LoadBalancer != nil {
|
||||
return m.getLoadBalancerServiceHandler(ctx, serviceName, conf.LoadBalancer, responseModifier)
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/containous/traefik/config"
|
||||
"github.com/containous/traefik/server/internal"
|
||||
"github.com/containous/traefik/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -324,4 +325,59 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestManager_Build(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
serviceName string
|
||||
configs map[string]*config.Service
|
||||
providerName string
|
||||
}{
|
||||
{
|
||||
desc: "Simple service name",
|
||||
serviceName: "serviceName",
|
||||
configs: map[string]*config.Service{
|
||||
"serviceName": {
|
||||
LoadBalancer: &config.LoadBalancerService{Method: "wrr"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Service name with provider",
|
||||
serviceName: "provider-1.serviceName",
|
||||
configs: map[string]*config.Service{
|
||||
"provider-1.serviceName": {
|
||||
LoadBalancer: &config.LoadBalancerService{Method: "wrr"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Service name with provider in context",
|
||||
serviceName: "serviceName",
|
||||
configs: map[string]*config.Service{
|
||||
"provider-1.serviceName": {
|
||||
LoadBalancer: &config.LoadBalancerService{Method: "wrr"},
|
||||
},
|
||||
},
|
||||
providerName: "provider-1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
manager := NewManager(test.configs, http.DefaultTransport)
|
||||
|
||||
ctx := context.Background()
|
||||
if len(test.providerName) > 0 {
|
||||
ctx = internal.AddProviderInContext(ctx, test.providerName+".foobar")
|
||||
}
|
||||
|
||||
_, err := manager.Build(ctx, test.serviceName, nil)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME Add healthcheck tests
|
||||
|
|
Loading…
Reference in a new issue