diff --git a/CHANGELOG.md b/CHANGELOG.md index 7af7981dd..280acb691 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/docs/basics.md b/docs/basics.md index 29cea3c44..c7b0f6043 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -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): diff --git a/docs/user-guide/cluster.md b/docs/user-guide/cluster.md index fb422cb49..3a33dc001 100644 --- a/docs/user-guide/cluster.md +++ b/docs/user-guide/cluster.md @@ -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. \ No newline at end of file diff --git a/middlewares/recover.go b/middlewares/recover.go new file mode 100644 index 000000000..fff7c2e0b --- /dev/null +++ b/middlewares/recover.go @@ -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) + } +} diff --git a/middlewares/recover_test.go b/middlewares/recover_test.go new file mode 100644 index 000000000..31cf098ca --- /dev/null +++ b/middlewares/recover_test.go @@ -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) + } +} diff --git a/middlewares/retry.go b/middlewares/retry.go index 21568eb97..031490045 100644 --- a/middlewares/retry.go +++ b/middlewares/retry.go @@ -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 diff --git a/middlewares/stateful.go b/middlewares/stateful.go new file mode 100644 index 000000000..4762d97a1 --- /dev/null +++ b/middlewares/stateful.go @@ -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 +} diff --git a/middlewares/stats.go b/middlewares/stats.go index c166799fc..faac75eba 100644 --- a/middlewares/stats.go +++ b/middlewares/stats.go @@ -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. diff --git a/provider/boltdb/boltdb.go b/provider/boltdb/boltdb.go index d2a92079c..190bea638 100644 --- a/provider/boltdb/boltdb.go +++ b/provider/boltdb/boltdb.go @@ -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() } diff --git a/provider/consul/consul.go b/provider/consul/consul.go index 5de063c68..d7a31f28d 100644 --- a/provider/consul/consul.go +++ b/provider/consul/consul.go @@ -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() } diff --git a/provider/etcd/etcd.go b/provider/etcd/etcd.go index a4b8bb3e5..488959e3a 100644 --- a/provider/etcd/etcd.go +++ b/provider/etcd/etcd.go @@ -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() } diff --git a/provider/eureka/eureka.go b/provider/eureka/eureka.go index b724f6025..a848280b2 100644 --- a/provider/eureka/eureka.go +++ b/provider/eureka/eureka.go @@ -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 diff --git a/provider/kv/kv.go b/provider/kv/kv.go index fc234fa4b..9503c0f8e 100644 --- a/provider/kv/kv.go +++ b/provider/kv/kv.go @@ -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 { diff --git a/provider/kv/kv_test.go b/provider/kv/kv_test.go index 72a5ed6fd..39ba6b0bb 100644 --- a/provider/kv/kv_test.go +++ b/provider/kv/kv_test.go @@ -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", diff --git a/provider/marathon/marathon.go b/provider/marathon/marathon.go index 217b986dc..de1d5bbdf 100644 --- a/provider/marathon/marathon.go +++ b/provider/marathon/marathon.go @@ -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 { diff --git a/provider/zk/zk.go b/provider/zk/zk.go index 3adb82dc2..2089d44f5 100644 --- a/provider/zk/zk.go +++ b/provider/zk/zk.go @@ -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() } diff --git a/script/deploy.sh b/script/deploy.sh index c04fcfa43..0f445f28f 100755 --- a/script/deploy.sh +++ b/script/deploy.sh @@ -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" diff --git a/server/server.go b/server/server.go index fdfd59d01..3c80249a2 100644 --- a/server/server.go +++ b/server/server.go @@ -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,9 +728,10 @@ 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) } - negroni.Use(authMiddleware) } if configuration.Backends[frontend.Backend].CircuitBreaker != nil { log.Debugf("Creating circuit breaker %s", configuration.Backends[frontend.Backend].CircuitBreaker.Expression) @@ -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, + } + } + } +} diff --git a/server/server_test.go b/server/server_test.go index d74949c2c..cb4c2d6d9 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -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)) + } + }) + } +} diff --git a/testhelpers/helpers.go b/testhelpers/helpers.go index e75be547e..0d2bb9802 100644 --- a/testhelpers/helpers.go +++ b/testhelpers/helpers.go @@ -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 +} diff --git a/types/types.go b/types/types.go index 05e704adc..3831847e0 100644 --- a/types/types.go +++ b/types/types.go @@ -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"`