Merge pull request #1613 from containous/merge-v1.3.0-rc2-master

Merge v1.3.0-rc2 master
This commit is contained in:
Ludovic Fernandez 2017-05-17 12:41:26 +02:00 committed by GitHub
commit ff3481f06b
21 changed files with 465 additions and 81 deletions

View file

@ -1,5 +1,19 @@
# Change Log
## [v1.3.0-rc2](https://github.com/containous/traefik/tree/v1.3.0-rc2) (2017-05-16)
[Full Changelog](https://github.com/containous/traefik/compare/v1.3.0-rc1...v1.3.0-rc2)
**Merged pull requests:**
- doc: Traefik cluster in beta. [\#1610](https://github.com/containous/traefik/pull/1610) ([ldez](https://github.com/ldez))
- SemaphoreCI on 1.3 branch [\#1608](https://github.com/containous/traefik/pull/1608) ([ldez](https://github.com/ldez))
- Fix empty basic auth [\#1601](https://github.com/containous/traefik/pull/1601) ([emilevauge](https://github.com/emilevauge))
- Fix stats hijack [\#1598](https://github.com/containous/traefik/pull/1598) ([emilevauge](https://github.com/emilevauge))
- Fix exported fields providers [\#1588](https://github.com/containous/traefik/pull/1588) ([emilevauge](https://github.com/emilevauge))
- Maintain sticky flag on LB method validation failure. [\#1585](https://github.com/containous/traefik/pull/1585) ([timoreimann](https://github.com/timoreimann))
- \[Kubernetes\] Ignore missing pass host header annotation. [\#1581](https://github.com/containous/traefik/pull/1581) ([timoreimann](https://github.com/timoreimann))
- Fixed ReplacePath rule executing out of order, when combined with PathPrefixStrip [\#1577](https://github.com/containous/traefik/pull/1577) ([aantono](https://github.com/aantono))
## [v1.3.0-rc1](https://github.com/containous/traefik/tree/v1.3.0-rc1) (2017-05-04)
[Full Changelog](https://github.com/containous/traefik/compare/v1.2.3...v1.3.0-rc1)

View file

@ -191,6 +191,25 @@ backend = "backend2"
rule = "Path:/test1,/test2"
```
### Rules Order
When combining `Modifier` rules with `Matcher` rules, it is important to remember that `Modifier` rules **ALWAYS** apply after the `Matcher` rules.
The following rules are both `Matchers` and `Modifiers`, so the `Matcher` portion of the rule will apply first, and the `Modifier` will apply later.
- `PathStrip`
- `PathStripRegex`
- `PathPrefixStrip`
- `PathPrefixStripRegex`
`Modifiers` will be applied in a pre-determined order regardless of their order in the `rule` configuration section.
1. `PathStrip`
2. `PathPrefixStrip`
3. `PathStripRegex`
4. `PathPrefixStripRegex`
5. `AddPrefix`
6. `ReplacePath`
### Priorities
By default, routes will be sorted (in descending order) using rules length (to avoid path overlap):

View file

@ -1,4 +1,4 @@
# Clustering / High Availability
# Clustering / High Availability (beta)
This guide explains how tu use Træfik in high availability mode.
In order to deploy and configure multiple Træfik instances, without copying the same configuration file on each instance, we will use a distributed Key-Value store.
@ -15,5 +15,6 @@ Please refer to [this section](/user-guide/kv-config/#store-configuration-in-key
## Deploy a Træfik cluster
Once your Træfik configuration is uploaded on your KV store, you can start each Træfik instance.
A Træfik cluster is based on a master/slave model. When starting, Træfik will elect a master. If this instance fails, another master will be automatically elected.
A Træfik cluster is based on a master/slave model.
When starting, Træfik will elect a master. If this instance fails, another master will be automatically elected.

33
middlewares/recover.go Normal file
View file

@ -0,0 +1,33 @@
package middlewares
import (
"net/http"
"github.com/codegangsta/negroni"
"github.com/containous/traefik/log"
)
// RecoverHandler recovers from a panic in http handlers
func RecoverHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
defer recoverFunc(w)
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
// NegroniRecoverHandler recovers from a panic in negroni handlers
func NegroniRecoverHandler() negroni.Handler {
fn := func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
defer recoverFunc(w)
next.ServeHTTP(w, r)
}
return negroni.HandlerFunc(fn)
}
func recoverFunc(w http.ResponseWriter) {
if err := recover(); err != nil {
log.Errorf("Recovered from panic in http handler: %+v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
}

View file

@ -0,0 +1,45 @@
package middlewares
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/codegangsta/negroni"
)
func TestRecoverHandler(t *testing.T) {
fn := func(w http.ResponseWriter, r *http.Request) {
panic("I love panicing!")
}
recoverHandler := RecoverHandler(http.HandlerFunc(fn))
server := httptest.NewServer(recoverHandler)
defer server.Close()
resp, err := http.Get(server.URL)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusInternalServerError {
t.Fatalf("Received non-%d response: %d\n", http.StatusInternalServerError, resp.StatusCode)
}
}
func TestNegroniRecoverHandler(t *testing.T) {
n := negroni.New()
n.Use(NegroniRecoverHandler())
panicHandler := func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
panic("I love panicing!")
}
n.UseFunc(negroni.HandlerFunc(panicHandler))
server := httptest.NewServer(n)
defer server.Close()
resp, err := http.Get(server.URL)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusInternalServerError {
t.Fatalf("Received non-%d response: %d\n", http.StatusInternalServerError, resp.StatusCode)
}
}

View file

@ -12,10 +12,7 @@ import (
)
var (
_ http.ResponseWriter = &ResponseRecorder{}
_ http.Hijacker = &ResponseRecorder{}
_ http.Flusher = &ResponseRecorder{}
_ http.CloseNotifier = &ResponseRecorder{}
_ Stateful = &ResponseRecorder{}
)
// Retry is a middleware that retries requests

12
middlewares/stateful.go Normal file
View file

@ -0,0 +1,12 @@
package middlewares
import "net/http"
// Stateful interface groups all http interfaces that must be
// implemented by a stateful middleware (ie: recorders)
type Stateful interface {
http.ResponseWriter
http.Hijacker
http.Flusher
http.CloseNotifier
}

View file

@ -1,11 +1,17 @@
package middlewares
import (
"bufio"
"net"
"net/http"
"sync"
"time"
)
var (
_ Stateful = &responseRecorder{}
)
// StatsRecorder is an optional middleware that records more details statistics
// about requests and how they are processed. This currently consists of recent
// requests that have caused errors (4xx and 5xx status codes), making it easy
@ -51,6 +57,23 @@ func (r *responseRecorder) WriteHeader(status int) {
r.statusCode = status
}
// Hijack hijacks the connection
func (r *responseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return r.ResponseWriter.(http.Hijacker).Hijack()
}
// CloseNotify returns a channel that receives at most a
// single value (true) when the client connection has gone
// away.
func (r *responseRecorder) CloseNotify() <-chan bool {
return r.ResponseWriter.(http.CloseNotifier).CloseNotify()
}
// Flush sends any buffered data to the client.
func (r *responseRecorder) Flush() {
r.ResponseWriter.(http.Flusher).Flush()
}
// ServeHTTP silently extracts information from the request and response as it
// is processed. If the response is 4xx or 5xx, add it to the list of 10 most
// recent errors.

View file

@ -25,13 +25,13 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
if err != nil {
return fmt.Errorf("Failed to Connect to KV store: %v", err)
}
p.Kvclient = store
p.SetKVClient(store)
return p.Provider.Provide(configurationChan, pool, constraints)
}
// CreateStore creates the KV store
func (p *Provider) CreateStore() (store.Store, error) {
p.StoreType = store.BOLTDB
p.SetStoreType(store.BOLTDB)
boltdb.Register()
return p.Provider.CreateStore()
}

View file

@ -25,13 +25,13 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
if err != nil {
return fmt.Errorf("Failed to Connect to KV store: %v", err)
}
p.Kvclient = store
p.SetKVClient(store)
return p.Provider.Provide(configurationChan, pool, constraints)
}
// CreateStore creates the KV store
func (p *Provider) CreateStore() (store.Store, error) {
p.StoreType = store.CONSUL
p.SetStoreType(store.CONSUL)
consul.Register()
return p.Provider.CreateStore()
}

View file

@ -25,13 +25,13 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
if err != nil {
return fmt.Errorf("Failed to Connect to KV store: %v", err)
}
p.Kvclient = store
p.SetKVClient(store)
return p.Provider.Provide(configurationChan, pool, constraints)
}
// CreateStore creates the KV store
func (p *Provider) CreateStore() (store.Store, error) {
p.StoreType = store.ETCD
p.SetStoreType(store.ETCD)
etcd.Register()
return p.Provider.CreateStore()
}

View file

@ -19,8 +19,8 @@ import (
// Provider holds configuration of the Provider provider.
type Provider struct {
provider.BaseProvider `mapstructure:",squash"`
Endpoint string
Delay string
Endpoint string `description:"Eureka server endpoint"`
Delay string `description:"Override default configuration time between refresh"`
}
// Provide allows the eureka provider to provide configurations to traefik

View file

@ -26,8 +26,8 @@ type Provider struct {
TLS *provider.ClientTLS `description:"Enable TLS support"`
Username string `description:"KV Username"`
Password string `description:"KV Password"`
StoreType store.Backend
Kvclient store.Store
storeType store.Backend
kvclient store.Store
}
// CreateStore create the K/V store
@ -47,15 +47,25 @@ func (p *Provider) CreateStore() (store.Store, error) {
}
}
return libkv.NewStore(
p.StoreType,
p.storeType,
strings.Split(p.Endpoint, ","),
storeConfig,
)
}
// SetStoreType storeType setter
func (p *Provider) SetStoreType(storeType store.Backend) {
p.storeType = storeType
}
// SetKVClient kvclient setter
func (p *Provider) SetKVClient(kvClient store.Store) {
p.kvclient = kvClient
}
func (p *Provider) watchKv(configurationChan chan<- types.ConfigMessage, prefix string, stop chan bool) error {
operation := func() error {
events, err := p.Kvclient.WatchTree(p.Prefix, make(chan struct{}))
events, err := p.kvclient.WatchTree(p.Prefix, make(chan struct{}))
if err != nil {
return fmt.Errorf("Failed to KV WatchTree: %v", err)
}
@ -70,7 +80,7 @@ func (p *Provider) watchKv(configurationChan chan<- types.ConfigMessage, prefix
configuration := p.loadConfig()
if configuration != nil {
configurationChan <- types.ConfigMessage{
ProviderName: string(p.StoreType),
ProviderName: string(p.storeType),
Configuration: configuration,
}
}
@ -92,7 +102,7 @@ func (p *Provider) watchKv(configurationChan chan<- types.ConfigMessage, prefix
func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error {
p.Constraints = append(p.Constraints, constraints...)
operation := func() error {
if _, err := p.Kvclient.Exists("qmslkjdfmqlskdjfmqlksjazçueznbvbwzlkajzebvkwjdcqmlsfj"); err != nil {
if _, err := p.kvclient.Exists("qmslkjdfmqlskdjfmqlksjazçueznbvbwzlkajzebvkwjdcqmlsfj"); err != nil {
return fmt.Errorf("Failed to test KV store connection: %v", err)
}
if p.Watch {
@ -105,7 +115,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
}
configuration := p.loadConfig()
configurationChan <- types.ConfigMessage{
ProviderName: string(p.StoreType),
ProviderName: string(p.storeType),
Configuration: configuration,
}
return nil
@ -152,7 +162,7 @@ func (p *Provider) loadConfig() *types.Configuration {
func (p *Provider) list(keys ...string) []string {
joinedKeys := strings.Join(keys, "")
keysPairs, err := p.Kvclient.List(joinedKeys)
keysPairs, err := p.kvclient.List(joinedKeys)
if err != nil {
log.Debugf("Cannot get keys %s %s ", joinedKeys, err)
return nil
@ -169,7 +179,7 @@ func (p *Provider) listServers(backend string) []string {
serverNames := p.list(backend, "/servers/")
return fun.Filter(func(serverName string) bool {
key := fmt.Sprint(serverName, "/url")
if _, err := p.Kvclient.Get(key); err != nil {
if _, err := p.kvclient.Get(key); err != nil {
if err != store.ErrKeyNotFound {
log.Errorf("Failed to retrieve value for key %s: %s", key, err)
}
@ -181,7 +191,7 @@ func (p *Provider) listServers(backend string) []string {
func (p *Provider) get(defaultValue string, keys ...string) string {
joinedKeys := strings.Join(keys, "")
keyPair, err := p.Kvclient.Get(strings.TrimPrefix(joinedKeys, "/"))
keyPair, err := p.kvclient.Get(strings.TrimPrefix(joinedKeys, "/"))
if err != nil {
log.Debugf("Cannot get key %s %s, setting default %s", joinedKeys, err, defaultValue)
return defaultValue
@ -194,7 +204,7 @@ func (p *Provider) get(defaultValue string, keys ...string) string {
func (p *Provider) splitGet(keys ...string) []string {
joinedKeys := strings.Join(keys, "")
keyPair, err := p.Kvclient.Get(joinedKeys)
keyPair, err := p.kvclient.Get(joinedKeys)
if err != nil {
log.Debugf("Cannot get key %s %s, setting default empty", joinedKeys, err)
return []string{}
@ -212,7 +222,7 @@ func (p *Provider) last(key string) string {
func (p *Provider) checkConstraints(keys ...string) bool {
joinedKeys := strings.Join(keys, "")
keyPair, err := p.Kvclient.Get(joinedKeys)
keyPair, err := p.kvclient.Get(joinedKeys)
value := ""
if err == nil && keyPair != nil && keyPair.Value != nil {

View file

@ -20,21 +20,21 @@ func TestKvList(t *testing.T) {
}{
{
provider: &Provider{
Kvclient: &Mock{},
kvclient: &Mock{},
},
keys: []string{},
expected: []string{},
},
{
provider: &Provider{
Kvclient: &Mock{},
kvclient: &Mock{},
},
keys: []string{"traefik"},
expected: []string{},
},
{
provider: &Provider{
Kvclient: &Mock{
kvclient: &Mock{
KVPairs: []*store.KVPair{
{
Key: "foo",
@ -48,7 +48,7 @@ func TestKvList(t *testing.T) {
},
{
provider: &Provider{
Kvclient: &Mock{
kvclient: &Mock{
KVPairs: []*store.KVPair{
{
Key: "foo",
@ -62,7 +62,7 @@ func TestKvList(t *testing.T) {
},
{
provider: &Provider{
Kvclient: &Mock{
kvclient: &Mock{
KVPairs: []*store.KVPair{
{
Key: "foo/baz/1",
@ -95,7 +95,7 @@ func TestKvList(t *testing.T) {
// Error case
provider := &Provider{
Kvclient: &Mock{
kvclient: &Mock{
Error: KvError{
List: store.ErrKeyNotFound,
},
@ -115,21 +115,21 @@ func TestKvGet(t *testing.T) {
}{
{
provider: &Provider{
Kvclient: &Mock{},
kvclient: &Mock{},
},
keys: []string{},
expected: "",
},
{
provider: &Provider{
Kvclient: &Mock{},
kvclient: &Mock{},
},
keys: []string{"traefik"},
expected: "",
},
{
provider: &Provider{
Kvclient: &Mock{
kvclient: &Mock{
KVPairs: []*store.KVPair{
{
Key: "foo",
@ -143,7 +143,7 @@ func TestKvGet(t *testing.T) {
},
{
provider: &Provider{
Kvclient: &Mock{
kvclient: &Mock{
KVPairs: []*store.KVPair{
{
Key: "foo",
@ -157,7 +157,7 @@ func TestKvGet(t *testing.T) {
},
{
provider: &Provider{
Kvclient: &Mock{
kvclient: &Mock{
KVPairs: []*store.KVPair{
{
Key: "foo/baz/1",
@ -188,7 +188,7 @@ func TestKvGet(t *testing.T) {
// Error case
provider := &Provider{
Kvclient: &Mock{
kvclient: &Mock{
Error: KvError{
Get: store.ErrKeyNotFound,
},
@ -249,7 +249,7 @@ func TestKvWatchTree(t *testing.T) {
returnedChans := make(chan chan []*store.KVPair)
provider := &KvMock{
Provider{
Kvclient: &Mock{
kvclient: &Mock{
WatchTreeMethod: func() <-chan []*store.KVPair {
c := make(chan []*store.KVPair, 10)
returnedChans <- c
@ -378,7 +378,7 @@ func (s *Mock) Close() {
func TestKVLoadConfig(t *testing.T) {
provider := &Provider{
Prefix: "traefik",
Kvclient: &Mock{
kvclient: &Mock{
KVPairs: []*store.KVPair{
{
Key: "traefik/frontends/frontend.with.dot",

View file

@ -45,14 +45,14 @@ type Provider struct {
DialerTimeout flaeg.Duration `description:"Set a non-default connection timeout for Marathon"`
KeepAlive flaeg.Duration `description:"Set a non-default TCP Keep Alive time in seconds"`
ForceTaskHostname bool `description:"Force to use the task's hostname."`
Basic *Basic
Basic *Basic `description:"Enable basic authentication"`
marathonClient marathon.Marathon
}
// Basic holds basic authentication specific configurations
type Basic struct {
HTTPBasicAuthUser string
HTTPBasicPassword string
HTTPBasicAuthUser string `description:"Basic authentication User"`
HTTPBasicPassword string `description:"Basic authentication Password"`
}
type lightMarathonClient interface {

View file

@ -25,13 +25,13 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
if err != nil {
return fmt.Errorf("Failed to Connect to KV store: %v", err)
}
p.Kvclient = store
p.SetKVClient(store)
return p.Provider.Provide(configurationChan, pool, constraints)
}
// CreateStore creates the KV store
func (p *Provider) CreateStore() (store.Store, error) {
p.StoreType = store.ZK
p.SetStoreType(store.ZK)
zookeeper.Register()
return p.Provider.CreateStore()
}

View file

@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -e
if [ -n "$TRAVIS_TAG" ] && [ "$DOCKER_VERSION" = "1.10.3" ]; then
if [ -n "$TRAVIS_TAG" ]; then
echo "Deploying..."
else
echo "Skipping deploy"

View file

@ -177,7 +177,7 @@ func (server *Server) startHTTPServers() {
server.serverEntryPoints = server.buildEntryPoints(server.globalConfiguration)
for newServerEntryPointName, newServerEntryPoint := range server.serverEntryPoints {
serverMiddlewares := []negroni.Handler{server.accessLoggerMiddleware, server.loggerMiddleware, metrics}
serverMiddlewares := []negroni.Handler{middlewares.NegroniRecoverHandler(), server.accessLoggerMiddleware, server.loggerMiddleware, metrics}
if server.globalConfiguration.Web != nil && server.globalConfiguration.Web.Metrics != nil {
if server.globalConfiguration.Web.Metrics.Prometheus != nil {
metricsMiddleware := middlewares.NewMetricsWrapper(middlewares.NewPrometheus(newServerEntryPointName, server.globalConfiguration.Web.Metrics.Prometheus))
@ -258,19 +258,8 @@ func (server *Server) defaultConfigurationValues(configuration *types.Configurat
if configuration == nil || configuration.Frontends == nil {
return
}
for _, frontend := range configuration.Frontends {
// default endpoints if not defined in frontends
if len(frontend.EntryPoints) == 0 {
frontend.EntryPoints = server.globalConfiguration.DefaultEntryPoints
}
}
for backendName, backend := range configuration.Backends {
_, err := types.NewLoadBalancerMethod(backend.LoadBalancer)
if err != nil {
log.Debugf("Load balancer method '%+v' for backend %s: %v. Using default wrr.", backend.LoadBalancer, backendName, err)
backend.LoadBalancer = &types.LoadBalancer{Method: "wrr"}
}
}
server.configureFrontends(configuration.Frontends)
server.configureBackends(configuration.Backends)
}
func (server *Server) listenConfigurations(stop chan bool) {
@ -661,7 +650,7 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
log.Errorf("Skipping frontend %s...", frontendName)
continue frontend
}
hcOpts := parseHealthCheckOptions(rebalancer, frontend.Backend, configuration.Backends[frontend.Backend].HealthCheck, *globalConfiguration.HealthCheck)
hcOpts := parseHealthCheckOptions(rebalancer, frontend.Backend, configuration.Backends[frontend.Backend].HealthCheck, globalConfiguration.HealthCheck)
if hcOpts != nil {
log.Debugf("Setting up backend health check %s", *hcOpts)
backendsHealthcheck[frontend.Backend] = healthcheck.NewBackendHealthCheck(*hcOpts)
@ -689,7 +678,7 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
continue frontend
}
}
hcOpts := parseHealthCheckOptions(rr, frontend.Backend, configuration.Backends[frontend.Backend].HealthCheck, *globalConfiguration.HealthCheck)
hcOpts := parseHealthCheckOptions(rr, frontend.Backend, configuration.Backends[frontend.Backend].HealthCheck, globalConfiguration.HealthCheck)
if hcOpts != nil {
log.Debugf("Setting up backend health check %s", *hcOpts)
backendsHealthcheck[frontend.Backend] = healthcheck.NewBackendHealthCheck(*hcOpts)
@ -739,10 +728,11 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
}
authMiddleware, err := middlewares.NewAuthenticator(auth)
if err != nil {
log.Fatal("Error creating Auth: ", err)
}
log.Errorf("Error creating Auth: %s", err)
} else {
negroni.Use(authMiddleware)
}
}
if configuration.Backends[frontend.Backend].CircuitBreaker != nil {
log.Debugf("Creating circuit breaker %s", configuration.Backends[frontend.Backend].CircuitBreaker.Expression)
cbreaker, err := middlewares.NewCircuitBreaker(lb, configuration.Backends[frontend.Backend].CircuitBreaker.Expression, cbreaker.Logger(oxyLogger))
@ -781,7 +771,17 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
}
func (server *Server) wireFrontendBackend(serverRoute *serverRoute, handler http.Handler) {
// add prefix
// path replace - This needs to always be the very last on the handler chain (first in the order in this function)
// -- Replacing Path should happen at the very end of the Modifier chain, after all the Matcher+Modifiers ran
if len(serverRoute.replacePath) > 0 {
handler = &middlewares.ReplacePath{
Path: serverRoute.replacePath,
Handler: handler,
}
}
// add prefix - This needs to always be right before ReplacePath on the chain (second in order in this function)
// -- Adding Path Prefix should happen after all *Strip Matcher+Modifiers ran, but before Replace (in case it's configured)
if len(serverRoute.addPrefix) > 0 {
handler = &middlewares.AddPrefix{
Prefix: serverRoute.addPrefix,
@ -802,14 +802,6 @@ func (server *Server) wireFrontendBackend(serverRoute *serverRoute, handler http
handler = middlewares.NewStripPrefixRegex(handler, serverRoute.stripPrefixesRegex)
}
// path replace
if len(serverRoute.replacePath) > 0 {
handler = &middlewares.ReplacePath{
Path: serverRoute.replacePath,
Handler: handler,
}
}
serverRoute.route.Handler(handler)
}
@ -849,8 +841,8 @@ func (server *Server) buildDefaultHTTPRouter() *mux.Router {
return router
}
func parseHealthCheckOptions(lb healthcheck.LoadBalancer, backend string, hc *types.HealthCheck, hcConfig HealthCheckConfig) *healthcheck.Options {
if hc == nil || hc.Path == "" {
func parseHealthCheckOptions(lb healthcheck.LoadBalancer, backend string, hc *types.HealthCheck, hcConfig *HealthCheckConfig) *healthcheck.Options {
if hc == nil || hc.Path == "" || hcConfig == nil {
return nil
}
@ -893,3 +885,29 @@ func sortedFrontendNamesForConfig(configuration *types.Configuration) []string {
sort.Strings(keys)
return keys
}
func (server *Server) configureFrontends(frontends map[string]*types.Frontend) {
for _, frontend := range frontends {
// default endpoints if not defined in frontends
if len(frontend.EntryPoints) == 0 {
frontend.EntryPoints = server.globalConfiguration.DefaultEntryPoints
}
}
}
func (*Server) configureBackends(backends map[string]*types.Backend) {
for backendName, backend := range backends {
_, err := types.NewLoadBalancerMethod(backend.LoadBalancer)
if err != nil {
log.Debugf("Validation of load balancer method for backend %s failed: %s. Using default method wrr.", backendName, err)
var sticky bool
if backend.LoadBalancer != nil {
sticky = backend.LoadBalancer.Sticky
}
backend.LoadBalancer = &types.LoadBalancer{
Method: "wrr",
Sticky: sticky,
}
}
}
}

View file

@ -2,14 +2,18 @@ package server
import (
"fmt"
"net/http"
"net/url"
"reflect"
"testing"
"time"
"github.com/containous/flaeg"
"github.com/containous/mux"
"github.com/containous/traefik/healthcheck"
"github.com/containous/traefik/testhelpers"
"github.com/containous/traefik/types"
"github.com/davecgh/go-spew/spew"
"github.com/vulcand/oxy/roundrobin"
)
@ -27,6 +31,85 @@ func (lb *testLoadBalancer) Servers() []*url.URL {
return []*url.URL{}
}
func TestServerMultipleFrontendRules(t *testing.T) {
cases := []struct {
expression string
requestURL string
expectedURL string
}{
{
expression: "Host:foo.bar",
requestURL: "http://foo.bar",
expectedURL: "http://foo.bar",
},
{
expression: "PathPrefix:/management;ReplacePath:/health",
requestURL: "http://foo.bar/management",
expectedURL: "http://foo.bar/health",
},
{
expression: "Host:foo.bar;AddPrefix:/blah",
requestURL: "http://foo.bar/baz",
expectedURL: "http://foo.bar/blah/baz",
},
{
expression: "PathPrefixStripRegex:/one/{two}/{three:[0-9]+}",
requestURL: "http://foo.bar/one/some/12345/four",
expectedURL: "http://foo.bar/four",
},
{
expression: "PathPrefixStripRegex:/one/{two}/{three:[0-9]+};AddPrefix:/zero",
requestURL: "http://foo.bar/one/some/12345/four",
expectedURL: "http://foo.bar/zero/four",
},
{
expression: "AddPrefix:/blah;ReplacePath:/baz",
requestURL: "http://foo.bar/hello",
expectedURL: "http://foo.bar/baz",
},
{
expression: "PathPrefixStrip:/management;ReplacePath:/health",
requestURL: "http://foo.bar/management",
expectedURL: "http://foo.bar/health",
},
}
for _, test := range cases {
test := test
t.Run(test.expression, func(t *testing.T) {
t.Parallel()
router := mux.NewRouter()
route := router.NewRoute()
serverRoute := &serverRoute{route: route}
rules := &Rules{route: serverRoute}
expression := test.expression
routeResult, err := rules.Parse(expression)
if err != nil {
t.Fatalf("Error while building route for %s: %+v", expression, err)
}
request := testhelpers.MustNewRequest(http.MethodGet, test.requestURL, nil)
routeMatch := routeResult.Match(request, &mux.RouteMatch{Route: routeResult})
if !routeMatch {
t.Fatalf("Rule %s doesn't match", expression)
}
server := new(Server)
server.wireFrontendBackend(serverRoute, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.String() != test.expectedURL {
t.Fatalf("got URL %s, expected %s", r.URL.String(), test.expectedURL)
}
}))
serverRoute.route.GetHandler().ServeHTTP(nil, request)
})
}
}
func TestServerLoadConfigHealthCheckOptions(t *testing.T) {
healthChecks := []*types.HealthCheck{
nil,
@ -151,10 +234,125 @@ func TestServerParseHealthCheckOptions(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
gotOpts := parseHealthCheckOptions(lb, "backend", test.hc, HealthCheckConfig{Interval: flaeg.Duration(globalInterval)})
gotOpts := parseHealthCheckOptions(lb, "backend", test.hc, &HealthCheckConfig{Interval: flaeg.Duration(globalInterval)})
if !reflect.DeepEqual(gotOpts, test.wantOpts) {
t.Errorf("got health check options %+v, want %+v", gotOpts, test.wantOpts)
}
})
}
}
func TestServerLoadConfigEmptyBasicAuth(t *testing.T) {
globalConfig := GlobalConfiguration{
EntryPoints: EntryPoints{
"http": &EntryPoint{},
},
}
dynamicConfigs := configs{
"config": &types.Configuration{
Frontends: map[string]*types.Frontend{
"frontend": {
EntryPoints: []string{"http"},
Backend: "backend",
BasicAuth: []string{""},
},
},
Backends: map[string]*types.Backend{
"backend": {
Servers: map[string]types.Server{
"server": {
URL: "http://localhost",
},
},
LoadBalancer: &types.LoadBalancer{
Method: "Wrr",
},
},
},
},
}
srv := NewServer(globalConfig)
if _, err := srv.loadConfig(dynamicConfigs, globalConfig); err != nil {
t.Fatalf("got error: %s", err)
}
}
func TestConfigureBackends(t *testing.T) {
validMethod := "Drr"
defaultMethod := "wrr"
tests := []struct {
desc string
lb *types.LoadBalancer
wantMethod string
wantSticky bool
}{
{
desc: "valid load balancer method with sticky enabled",
lb: &types.LoadBalancer{
Method: validMethod,
Sticky: true,
},
wantMethod: validMethod,
wantSticky: true,
},
{
desc: "valid load balancer method with sticky disabled",
lb: &types.LoadBalancer{
Method: validMethod,
Sticky: false,
},
wantMethod: validMethod,
wantSticky: false,
},
{
desc: "invalid load balancer method with sticky enabled",
lb: &types.LoadBalancer{
Method: "Invalid",
Sticky: true,
},
wantMethod: defaultMethod,
wantSticky: true,
},
{
desc: "invalid load balancer method with sticky disabled",
lb: &types.LoadBalancer{
Method: "Invalid",
Sticky: false,
},
wantMethod: defaultMethod,
wantSticky: false,
},
{
desc: "missing load balancer",
lb: nil,
wantMethod: defaultMethod,
wantSticky: false,
},
}
for _, test := range tests {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
backend := &types.Backend{
LoadBalancer: test.lb,
}
srv := Server{}
srv.configureBackends(map[string]*types.Backend{
"backend": backend,
})
wantLB := types.LoadBalancer{
Method: test.wantMethod,
Sticky: test.wantSticky,
}
if !reflect.DeepEqual(*backend.LoadBalancer, wantLB) {
t.Errorf("got backend load-balancer\n%v\nwant\n%v\n", spew.Sdump(backend.LoadBalancer), spew.Sdump(wantLB))
}
})
}
}

View file

@ -1,6 +1,21 @@
package testhelpers
import (
"fmt"
"io"
"net/http"
)
// Intp returns a pointer to the given integer value.
func Intp(i int) *int {
return &i
}
// MustNewRequest creates a new http get request or panics if it can't
func MustNewRequest(method, urlStr string, body io.Reader) *http.Request {
request, err := http.NewRequest(method, urlStr, body)
if err != nil {
panic(fmt.Sprintf("failed to create HTTP %s Request for '%s': %s", method, urlStr, err))
}
return request
}

View file

@ -81,19 +81,18 @@ var loadBalancerMethodNames = []string{
// NewLoadBalancerMethod create a new LoadBalancerMethod from a given LoadBalancer.
func NewLoadBalancerMethod(loadBalancer *LoadBalancer) (LoadBalancerMethod, error) {
var method string
if loadBalancer != nil {
method = loadBalancer.Method
for i, name := range loadBalancerMethodNames {
if strings.EqualFold(name, loadBalancer.Method) {
if strings.EqualFold(name, method) {
return LoadBalancerMethod(i), nil
}
}
}
return Wrr, ErrInvalidLoadBalancerMethod
return Wrr, fmt.Errorf("invalid load-balancing method '%s'", method)
}
// ErrInvalidLoadBalancerMethod is thrown when the specified load balancing method is invalid.
var ErrInvalidLoadBalancerMethod = errors.New("Invalid method, using default")
// Configuration of a provider.
type Configuration struct {
Backends map[string]*Backend `json:"backends,omitempty"`