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 # 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) ## [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) [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" 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 ### Priorities
By default, routes will be sorted (in descending order) using rules length (to avoid path overlap): 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. 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. 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 ## Deploy a Træfik cluster
Once your Træfik configuration is uploaded on your KV store, you can start each Træfik instance. 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 ( var (
_ http.ResponseWriter = &ResponseRecorder{} _ Stateful = &ResponseRecorder{}
_ http.Hijacker = &ResponseRecorder{}
_ http.Flusher = &ResponseRecorder{}
_ http.CloseNotifier = &ResponseRecorder{}
) )
// Retry is a middleware that retries requests // 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 package middlewares
import ( import (
"bufio"
"net"
"net/http" "net/http"
"sync" "sync"
"time" "time"
) )
var (
_ Stateful = &responseRecorder{}
)
// StatsRecorder is an optional middleware that records more details statistics // StatsRecorder is an optional middleware that records more details statistics
// about requests and how they are processed. This currently consists of recent // 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 // 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 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 // 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 // is processed. If the response is 4xx or 5xx, add it to the list of 10 most
// recent errors. // recent errors.

View file

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

View file

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

View file

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

View file

@ -19,8 +19,8 @@ import (
// Provider holds configuration of the Provider provider. // Provider holds configuration of the Provider provider.
type Provider struct { type Provider struct {
provider.BaseProvider `mapstructure:",squash"` provider.BaseProvider `mapstructure:",squash"`
Endpoint string Endpoint string `description:"Eureka server endpoint"`
Delay string Delay string `description:"Override default configuration time between refresh"`
} }
// Provide allows the eureka provider to provide configurations to traefik // 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"` TLS *provider.ClientTLS `description:"Enable TLS support"`
Username string `description:"KV Username"` Username string `description:"KV Username"`
Password string `description:"KV Password"` Password string `description:"KV Password"`
StoreType store.Backend storeType store.Backend
Kvclient store.Store kvclient store.Store
} }
// CreateStore create the K/V store // CreateStore create the K/V store
@ -47,15 +47,25 @@ func (p *Provider) CreateStore() (store.Store, error) {
} }
} }
return libkv.NewStore( return libkv.NewStore(
p.StoreType, p.storeType,
strings.Split(p.Endpoint, ","), strings.Split(p.Endpoint, ","),
storeConfig, 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 { func (p *Provider) watchKv(configurationChan chan<- types.ConfigMessage, prefix string, stop chan bool) error {
operation := func() 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 { if err != nil {
return fmt.Errorf("Failed to KV WatchTree: %v", err) 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() configuration := p.loadConfig()
if configuration != nil { if configuration != nil {
configurationChan <- types.ConfigMessage{ configurationChan <- types.ConfigMessage{
ProviderName: string(p.StoreType), ProviderName: string(p.storeType),
Configuration: configuration, 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 { func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error {
p.Constraints = append(p.Constraints, constraints...) p.Constraints = append(p.Constraints, constraints...)
operation := func() error { 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) return fmt.Errorf("Failed to test KV store connection: %v", err)
} }
if p.Watch { if p.Watch {
@ -105,7 +115,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
} }
configuration := p.loadConfig() configuration := p.loadConfig()
configurationChan <- types.ConfigMessage{ configurationChan <- types.ConfigMessage{
ProviderName: string(p.StoreType), ProviderName: string(p.storeType),
Configuration: configuration, Configuration: configuration,
} }
return nil return nil
@ -152,7 +162,7 @@ func (p *Provider) loadConfig() *types.Configuration {
func (p *Provider) list(keys ...string) []string { func (p *Provider) list(keys ...string) []string {
joinedKeys := strings.Join(keys, "") joinedKeys := strings.Join(keys, "")
keysPairs, err := p.Kvclient.List(joinedKeys) keysPairs, err := p.kvclient.List(joinedKeys)
if err != nil { if err != nil {
log.Debugf("Cannot get keys %s %s ", joinedKeys, err) log.Debugf("Cannot get keys %s %s ", joinedKeys, err)
return nil return nil
@ -169,7 +179,7 @@ func (p *Provider) listServers(backend string) []string {
serverNames := p.list(backend, "/servers/") serverNames := p.list(backend, "/servers/")
return fun.Filter(func(serverName string) bool { return fun.Filter(func(serverName string) bool {
key := fmt.Sprint(serverName, "/url") 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 { if err != store.ErrKeyNotFound {
log.Errorf("Failed to retrieve value for key %s: %s", key, err) 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 { func (p *Provider) get(defaultValue string, keys ...string) string {
joinedKeys := strings.Join(keys, "") joinedKeys := strings.Join(keys, "")
keyPair, err := p.Kvclient.Get(strings.TrimPrefix(joinedKeys, "/")) keyPair, err := p.kvclient.Get(strings.TrimPrefix(joinedKeys, "/"))
if err != nil { if err != nil {
log.Debugf("Cannot get key %s %s, setting default %s", joinedKeys, err, defaultValue) log.Debugf("Cannot get key %s %s, setting default %s", joinedKeys, err, defaultValue)
return defaultValue return defaultValue
@ -194,7 +204,7 @@ func (p *Provider) get(defaultValue string, keys ...string) string {
func (p *Provider) splitGet(keys ...string) []string { func (p *Provider) splitGet(keys ...string) []string {
joinedKeys := strings.Join(keys, "") joinedKeys := strings.Join(keys, "")
keyPair, err := p.Kvclient.Get(joinedKeys) keyPair, err := p.kvclient.Get(joinedKeys)
if err != nil { if err != nil {
log.Debugf("Cannot get key %s %s, setting default empty", joinedKeys, err) log.Debugf("Cannot get key %s %s, setting default empty", joinedKeys, err)
return []string{} return []string{}
@ -212,7 +222,7 @@ func (p *Provider) last(key string) string {
func (p *Provider) checkConstraints(keys ...string) bool { func (p *Provider) checkConstraints(keys ...string) bool {
joinedKeys := strings.Join(keys, "") joinedKeys := strings.Join(keys, "")
keyPair, err := p.Kvclient.Get(joinedKeys) keyPair, err := p.kvclient.Get(joinedKeys)
value := "" value := ""
if err == nil && keyPair != nil && keyPair.Value != nil { if err == nil && keyPair != nil && keyPair.Value != nil {

View file

@ -20,21 +20,21 @@ func TestKvList(t *testing.T) {
}{ }{
{ {
provider: &Provider{ provider: &Provider{
Kvclient: &Mock{}, kvclient: &Mock{},
}, },
keys: []string{}, keys: []string{},
expected: []string{}, expected: []string{},
}, },
{ {
provider: &Provider{ provider: &Provider{
Kvclient: &Mock{}, kvclient: &Mock{},
}, },
keys: []string{"traefik"}, keys: []string{"traefik"},
expected: []string{}, expected: []string{},
}, },
{ {
provider: &Provider{ provider: &Provider{
Kvclient: &Mock{ kvclient: &Mock{
KVPairs: []*store.KVPair{ KVPairs: []*store.KVPair{
{ {
Key: "foo", Key: "foo",
@ -48,7 +48,7 @@ func TestKvList(t *testing.T) {
}, },
{ {
provider: &Provider{ provider: &Provider{
Kvclient: &Mock{ kvclient: &Mock{
KVPairs: []*store.KVPair{ KVPairs: []*store.KVPair{
{ {
Key: "foo", Key: "foo",
@ -62,7 +62,7 @@ func TestKvList(t *testing.T) {
}, },
{ {
provider: &Provider{ provider: &Provider{
Kvclient: &Mock{ kvclient: &Mock{
KVPairs: []*store.KVPair{ KVPairs: []*store.KVPair{
{ {
Key: "foo/baz/1", Key: "foo/baz/1",
@ -95,7 +95,7 @@ func TestKvList(t *testing.T) {
// Error case // Error case
provider := &Provider{ provider := &Provider{
Kvclient: &Mock{ kvclient: &Mock{
Error: KvError{ Error: KvError{
List: store.ErrKeyNotFound, List: store.ErrKeyNotFound,
}, },
@ -115,21 +115,21 @@ func TestKvGet(t *testing.T) {
}{ }{
{ {
provider: &Provider{ provider: &Provider{
Kvclient: &Mock{}, kvclient: &Mock{},
}, },
keys: []string{}, keys: []string{},
expected: "", expected: "",
}, },
{ {
provider: &Provider{ provider: &Provider{
Kvclient: &Mock{}, kvclient: &Mock{},
}, },
keys: []string{"traefik"}, keys: []string{"traefik"},
expected: "", expected: "",
}, },
{ {
provider: &Provider{ provider: &Provider{
Kvclient: &Mock{ kvclient: &Mock{
KVPairs: []*store.KVPair{ KVPairs: []*store.KVPair{
{ {
Key: "foo", Key: "foo",
@ -143,7 +143,7 @@ func TestKvGet(t *testing.T) {
}, },
{ {
provider: &Provider{ provider: &Provider{
Kvclient: &Mock{ kvclient: &Mock{
KVPairs: []*store.KVPair{ KVPairs: []*store.KVPair{
{ {
Key: "foo", Key: "foo",
@ -157,7 +157,7 @@ func TestKvGet(t *testing.T) {
}, },
{ {
provider: &Provider{ provider: &Provider{
Kvclient: &Mock{ kvclient: &Mock{
KVPairs: []*store.KVPair{ KVPairs: []*store.KVPair{
{ {
Key: "foo/baz/1", Key: "foo/baz/1",
@ -188,7 +188,7 @@ func TestKvGet(t *testing.T) {
// Error case // Error case
provider := &Provider{ provider := &Provider{
Kvclient: &Mock{ kvclient: &Mock{
Error: KvError{ Error: KvError{
Get: store.ErrKeyNotFound, Get: store.ErrKeyNotFound,
}, },
@ -249,7 +249,7 @@ func TestKvWatchTree(t *testing.T) {
returnedChans := make(chan chan []*store.KVPair) returnedChans := make(chan chan []*store.KVPair)
provider := &KvMock{ provider := &KvMock{
Provider{ Provider{
Kvclient: &Mock{ kvclient: &Mock{
WatchTreeMethod: func() <-chan []*store.KVPair { WatchTreeMethod: func() <-chan []*store.KVPair {
c := make(chan []*store.KVPair, 10) c := make(chan []*store.KVPair, 10)
returnedChans <- c returnedChans <- c
@ -378,7 +378,7 @@ func (s *Mock) Close() {
func TestKVLoadConfig(t *testing.T) { func TestKVLoadConfig(t *testing.T) {
provider := &Provider{ provider := &Provider{
Prefix: "traefik", Prefix: "traefik",
Kvclient: &Mock{ kvclient: &Mock{
KVPairs: []*store.KVPair{ KVPairs: []*store.KVPair{
{ {
Key: "traefik/frontends/frontend.with.dot", 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"` 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"` KeepAlive flaeg.Duration `description:"Set a non-default TCP Keep Alive time in seconds"`
ForceTaskHostname bool `description:"Force to use the task's hostname."` ForceTaskHostname bool `description:"Force to use the task's hostname."`
Basic *Basic Basic *Basic `description:"Enable basic authentication"`
marathonClient marathon.Marathon marathonClient marathon.Marathon
} }
// Basic holds basic authentication specific configurations // Basic holds basic authentication specific configurations
type Basic struct { type Basic struct {
HTTPBasicAuthUser string HTTPBasicAuthUser string `description:"Basic authentication User"`
HTTPBasicPassword string HTTPBasicPassword string `description:"Basic authentication Password"`
} }
type lightMarathonClient interface { type lightMarathonClient interface {

View file

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

View file

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

View file

@ -177,7 +177,7 @@ func (server *Server) startHTTPServers() {
server.serverEntryPoints = server.buildEntryPoints(server.globalConfiguration) server.serverEntryPoints = server.buildEntryPoints(server.globalConfiguration)
for newServerEntryPointName, newServerEntryPoint := range server.serverEntryPoints { 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 != nil && server.globalConfiguration.Web.Metrics != nil {
if server.globalConfiguration.Web.Metrics.Prometheus != nil { if server.globalConfiguration.Web.Metrics.Prometheus != nil {
metricsMiddleware := middlewares.NewMetricsWrapper(middlewares.NewPrometheus(newServerEntryPointName, server.globalConfiguration.Web.Metrics.Prometheus)) 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 { if configuration == nil || configuration.Frontends == nil {
return return
} }
for _, frontend := range configuration.Frontends { server.configureFrontends(configuration.Frontends)
// default endpoints if not defined in frontends server.configureBackends(configuration.Backends)
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"}
}
}
} }
func (server *Server) listenConfigurations(stop chan bool) { 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) log.Errorf("Skipping frontend %s...", frontendName)
continue frontend 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 { if hcOpts != nil {
log.Debugf("Setting up backend health check %s", *hcOpts) log.Debugf("Setting up backend health check %s", *hcOpts)
backendsHealthcheck[frontend.Backend] = healthcheck.NewBackendHealthCheck(*hcOpts) backendsHealthcheck[frontend.Backend] = healthcheck.NewBackendHealthCheck(*hcOpts)
@ -689,7 +678,7 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
continue frontend 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 { if hcOpts != nil {
log.Debugf("Setting up backend health check %s", *hcOpts) log.Debugf("Setting up backend health check %s", *hcOpts)
backendsHealthcheck[frontend.Backend] = healthcheck.NewBackendHealthCheck(*hcOpts) backendsHealthcheck[frontend.Backend] = healthcheck.NewBackendHealthCheck(*hcOpts)
@ -739,9 +728,10 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
} }
authMiddleware, err := middlewares.NewAuthenticator(auth) authMiddleware, err := middlewares.NewAuthenticator(auth)
if err != nil { 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 { if configuration.Backends[frontend.Backend].CircuitBreaker != nil {
log.Debugf("Creating circuit breaker %s", configuration.Backends[frontend.Backend].CircuitBreaker.Expression) 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) { 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 { if len(serverRoute.addPrefix) > 0 {
handler = &middlewares.AddPrefix{ handler = &middlewares.AddPrefix{
Prefix: serverRoute.addPrefix, Prefix: serverRoute.addPrefix,
@ -802,14 +802,6 @@ func (server *Server) wireFrontendBackend(serverRoute *serverRoute, handler http
handler = middlewares.NewStripPrefixRegex(handler, serverRoute.stripPrefixesRegex) 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) serverRoute.route.Handler(handler)
} }
@ -849,8 +841,8 @@ func (server *Server) buildDefaultHTTPRouter() *mux.Router {
return router return router
} }
func parseHealthCheckOptions(lb healthcheck.LoadBalancer, backend string, hc *types.HealthCheck, hcConfig HealthCheckConfig) *healthcheck.Options { func parseHealthCheckOptions(lb healthcheck.LoadBalancer, backend string, hc *types.HealthCheck, hcConfig *HealthCheckConfig) *healthcheck.Options {
if hc == nil || hc.Path == "" { if hc == nil || hc.Path == "" || hcConfig == nil {
return nil return nil
} }
@ -893,3 +885,29 @@ func sortedFrontendNamesForConfig(configuration *types.Configuration) []string {
sort.Strings(keys) sort.Strings(keys)
return 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 ( import (
"fmt" "fmt"
"net/http"
"net/url" "net/url"
"reflect" "reflect"
"testing" "testing"
"time" "time"
"github.com/containous/flaeg" "github.com/containous/flaeg"
"github.com/containous/mux"
"github.com/containous/traefik/healthcheck" "github.com/containous/traefik/healthcheck"
"github.com/containous/traefik/testhelpers"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
"github.com/davecgh/go-spew/spew"
"github.com/vulcand/oxy/roundrobin" "github.com/vulcand/oxy/roundrobin"
) )
@ -27,6 +31,85 @@ func (lb *testLoadBalancer) Servers() []*url.URL {
return []*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) { func TestServerLoadConfigHealthCheckOptions(t *testing.T) {
healthChecks := []*types.HealthCheck{ healthChecks := []*types.HealthCheck{
nil, nil,
@ -151,10 +234,125 @@ func TestServerParseHealthCheckOptions(t *testing.T) {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() 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) { if !reflect.DeepEqual(gotOpts, test.wantOpts) {
t.Errorf("got health check options %+v, want %+v", 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 package testhelpers
import (
"fmt"
"io"
"net/http"
)
// Intp returns a pointer to the given integer value. // Intp returns a pointer to the given integer value.
func Intp(i int) *int { func Intp(i int) *int {
return &i 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. // NewLoadBalancerMethod create a new LoadBalancerMethod from a given LoadBalancer.
func NewLoadBalancerMethod(loadBalancer *LoadBalancer) (LoadBalancerMethod, error) { func NewLoadBalancerMethod(loadBalancer *LoadBalancer) (LoadBalancerMethod, error) {
var method string
if loadBalancer != nil { if loadBalancer != nil {
method = loadBalancer.Method
for i, name := range loadBalancerMethodNames { for i, name := range loadBalancerMethodNames {
if strings.EqualFold(name, loadBalancer.Method) { if strings.EqualFold(name, method) {
return LoadBalancerMethod(i), nil 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. // Configuration of a provider.
type Configuration struct { type Configuration struct {
Backends map[string]*Backend `json:"backends,omitempty"` Backends map[string]*Backend `json:"backends,omitempty"`