From 3a4ec19817feec3b1321bd9d7351bd876daf2385 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Thu, 11 May 2017 19:07:45 +0200 Subject: [PATCH 01/17] Add missing description tag Signed-off-by: Emile Vauge --- provider/eureka/eureka.go | 4 ++-- provider/marathon/marathon.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) 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/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 { From aa4ed088bbf72f7677da81dcaa1fe1b702f2c5f7 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Thu, 11 May 2017 19:09:06 +0200 Subject: [PATCH 02/17] Unexport Kvclient & StoreType from kv Provider Signed-off-by: Emile Vauge --- provider/boltdb/boltdb.go | 4 ++-- provider/consul/consul.go | 4 ++-- provider/etcd/etcd.go | 4 ++-- provider/kv/kv.go | 34 ++++++++++++++++++++++------------ provider/kv/kv_test.go | 28 ++++++++++++++-------------- provider/zk/zk.go | 4 ++-- 6 files changed, 44 insertions(+), 34 deletions(-) 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/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/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() } From 71cec1580b049594879d17f7eb9e97c22c58c38f Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Sat, 13 May 2017 19:02:06 +0200 Subject: [PATCH 03/17] Fix stats responseRecorder Hijacker Signed-off-by: Emile Vauge --- middlewares/retry.go | 5 +---- middlewares/stateful.go | 12 ++++++++++++ middlewares/stats.go | 23 +++++++++++++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 middlewares/stateful.go 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. From 111251da05a9434a201729e42ac4aea4b80f50f7 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Sat, 13 May 2017 19:02:52 +0200 Subject: [PATCH 04/17] Adds Panic Recover middleware Signed-off-by: Emile Vauge --- middlewares/recover.go | 32 ++++++++++++++++++++++++++++++++ server/server.go | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 middlewares/recover.go diff --git a/middlewares/recover.go b/middlewares/recover.go new file mode 100644 index 000000000..3b8475d2a --- /dev/null +++ b/middlewares/recover.go @@ -0,0 +1,32 @@ +package middlewares + +import ( + "github.com/codegangsta/negroni" + "github.com/containous/traefik/log" + "net/http" +) + +// 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/server/server.go b/server/server.go index da99e366d..008699651 100644 --- a/server/server.go +++ b/server/server.go @@ -173,7 +173,7 @@ func (server *Server) startHTTPServers() { server.serverEntryPoints = server.buildEntryPoints(server.globalConfiguration) for newServerEntryPointName, newServerEntryPoint := range server.serverEntryPoints { - serverMiddlewares := []negroni.Handler{server.loggerMiddleware, metrics} + serverMiddlewares := []negroni.Handler{middlewares.NegroniRecoverHandler(), 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)) From 48a91d05b5adc3cbb55da81596d7e3ddab1834dd Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Sat, 13 May 2017 19:36:37 +0200 Subject: [PATCH 05/17] Add Recover tests --- middlewares/recover.go | 3 ++- middlewares/recover_test.go | 45 +++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 middlewares/recover_test.go diff --git a/middlewares/recover.go b/middlewares/recover.go index 3b8475d2a..fff7c2e0b 100644 --- a/middlewares/recover.go +++ b/middlewares/recover.go @@ -1,9 +1,10 @@ package middlewares import ( + "net/http" + "github.com/codegangsta/negroni" "github.com/containous/traefik/log" - "net/http" ) // RecoverHandler recovers from a panic in http handlers 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) + } +} From eaedc1b924ca76dfe5b2df93fecda58e0aa86bf1 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Mon, 15 May 2017 09:02:32 +0200 Subject: [PATCH 06/17] Fix empty basic auth Signed-off-by: Emile Vauge --- server/server.go | 13 +++++++------ server/server_test.go | 39 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/server/server.go b/server/server.go index 008699651..1547270f6 100644 --- a/server/server.go +++ b/server/server.go @@ -655,7 +655,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) @@ -683,7 +683,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) @@ -733,9 +733,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) @@ -843,8 +844,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 } diff --git a/server/server_test.go b/server/server_test.go index d74949c2c..bc41bdd30 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -151,10 +151,47 @@ 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) + } +} From 3f68e382fdf35ef6312f0a8270a518223d393cf7 Mon Sep 17 00:00:00 2001 From: Alex Antonov Date: Tue, 9 May 2017 15:31:16 -0500 Subject: [PATCH 07/17] Fixed ReplacePath rule executing out of order, when combined with PathPrefixStrip #1569 --- docs/basics.md | 19 ++++++++++ server/server.go | 20 ++++++----- server/server_test.go | 82 ++++++++++++++++++++++++++++++++++++++++++ testhelpers/helpers.go | 15 ++++++++ 4 files changed, 127 insertions(+), 9 deletions(-) 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/server/server.go b/server/server.go index 1547270f6..5630ffc2a 100644 --- a/server/server.go +++ b/server/server.go @@ -776,7 +776,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, @@ -797,14 +807,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) } diff --git a/server/server_test.go b/server/server_test.go index bc41bdd30..96c4daae6 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -2,13 +2,16 @@ 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/vulcand/oxy/roundrobin" ) @@ -27,6 +30,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, 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 +} From 2c45428c8a7cf9edf540628b07ce1633bcbf2490 Mon Sep 17 00:00:00 2001 From: Timo Reimann Date: Thu, 11 May 2017 00:34:47 +0200 Subject: [PATCH 08/17] Maintain sticky flag on LB method validation failure. We previously did not copy the sticky flag if the load-balancer method validation failed, causing enabled stickiness to be dropped in case of a validation error (which, technically, for us is the same as a load-balancer configuration without an explicitly set method). This change fixes that. A few refactorings and improvements along the way: - Move the frontend and backend configuration steps into separate methods/functions for better testability. - Include the invalid method name in the error value and avoid log duplication. - Add tests for the backend configuration part. --- server/server.go | 41 +++++++++++++++-------- server/server_test.go | 78 +++++++++++++++++++++++++++++++++++++++++++ types/types.go | 9 +++-- 3 files changed, 110 insertions(+), 18 deletions(-) diff --git a/server/server.go b/server/server.go index 5630ffc2a..e1465db2c 100644 --- a/server/server.go +++ b/server/server.go @@ -254,19 +254,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) + configureBackends(configuration.Backends) } func (server *Server) listenConfigurations(stop chan bool) { @@ -890,3 +879,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 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 96c4daae6..9c4d6bdf9 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -13,6 +13,7 @@ import ( "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" ) @@ -277,3 +278,80 @@ func TestServerLoadConfigEmptyBasicAuth(t *testing.T) { 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, + } + + 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/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"` From f9839f7b1d1bfd215dcd105681d35c4fdf5d6c91 Mon Sep 17 00:00:00 2001 From: Timo Reimann Date: Mon, 15 May 2017 23:53:35 +0200 Subject: [PATCH 09/17] Turn configureBackends into method. --- server/server.go | 4 ++-- server/server_test.go | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/server/server.go b/server/server.go index e1465db2c..ad7b64876 100644 --- a/server/server.go +++ b/server/server.go @@ -255,7 +255,7 @@ func (server *Server) defaultConfigurationValues(configuration *types.Configurat return } server.configureFrontends(configuration.Frontends) - configureBackends(configuration.Backends) + server.configureBackends(configuration.Backends) } func (server *Server) listenConfigurations(stop chan bool) { @@ -889,7 +889,7 @@ func (server *Server) configureFrontends(frontends map[string]*types.Frontend) { } } -func configureBackends(backends map[string]*types.Backend) { +func (*Server) configureBackends(backends map[string]*types.Backend) { for backendName, backend := range backends { _, err := types.NewLoadBalancerMethod(backend.LoadBalancer) if err != nil { diff --git a/server/server_test.go b/server/server_test.go index 9c4d6bdf9..cb4c2d6d9 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -341,7 +341,8 @@ func TestConfigureBackends(t *testing.T) { LoadBalancer: test.lb, } - configureBackends(map[string]*types.Backend{ + srv := Server{} + srv.configureBackends(map[string]*types.Backend{ "backend": backend, }) From 21aa0ea2dab8ce8edccc0f3c1cd424d42435573a Mon Sep 17 00:00:00 2001 From: Attilio Borello Date: Tue, 9 May 2017 10:34:30 +0200 Subject: [PATCH 10/17] added DOCKER_VERSION variable --- .semaphoreci/setup.sh | 4 ++++ .semaphoreci/vars | 2 ++ 2 files changed, 6 insertions(+) diff --git a/.semaphoreci/setup.sh b/.semaphoreci/setup.sh index 52f253d1d..56bb0169d 100755 --- a/.semaphoreci/setup.sh +++ b/.semaphoreci/setup.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash set -e +sudo -E apt-get -yq update +sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install docker-engine=${DOCKER_VERSION}* +docker version + pip install --user -r requirements.txt make pull-images diff --git a/.semaphoreci/vars b/.semaphoreci/vars index 15a5e501d..a17eabb76 100644 --- a/.semaphoreci/vars +++ b/.semaphoreci/vars @@ -5,6 +5,8 @@ export secure='btt4r13t09gQlHb6gYrvGC2yGCMMHfnp1Mz1RQedc4Mpf/FfT8aE6xmK2a2i9CCvs export REPO='containous/traefik' +export DOCKER_VERSION=1.12.6 + if VERSION=$(git describe --exact-match --abbrev=0 --tags); then export VERSION From bdf4f48d781c8ed611056426f3f770d56e7e162c Mon Sep 17 00:00:00 2001 From: Attilio Borello Date: Wed, 10 May 2017 08:54:38 +0200 Subject: [PATCH 11/17] replaced docker images with alpine if available (nginx, rabbitmq) --- integration/resources/compose/boulder.yml | 2 +- integration/resources/compose/constraints.yml | 2 +- integration/resources/compose/consul_catalog.yml | 2 +- integration/resources/compose/file.yml | 10 +++++----- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/integration/resources/compose/boulder.yml b/integration/resources/compose/boulder.yml index 8320c6a92..f1f954377 100644 --- a/integration/resources/compose/boulder.yml +++ b/integration/resources/compose/boulder.yml @@ -39,6 +39,6 @@ bmysql: MYSQL_ALLOW_EMPTY_PASSWORD: "yes" log_driver: none brabbitmq: - image: rabbitmq:3 + image: rabbitmq:3-alpine environment: RABBITMQ_NODE_IP_ADDRESS: "0.0.0.0" diff --git a/integration/resources/compose/constraints.yml b/integration/resources/compose/constraints.yml index 9a2688904..1d5395281 100644 --- a/integration/resources/compose/constraints.yml +++ b/integration/resources/compose/constraints.yml @@ -12,6 +12,6 @@ consul: - "8302" - "8302/udp" nginx: - image: nginx + image: nginx:alpine ports: - "8881:80" diff --git a/integration/resources/compose/consul_catalog.yml b/integration/resources/compose/consul_catalog.yml index 9a2688904..1d5395281 100644 --- a/integration/resources/compose/consul_catalog.yml +++ b/integration/resources/compose/consul_catalog.yml @@ -12,6 +12,6 @@ consul: - "8302" - "8302/udp" nginx: - image: nginx + image: nginx:alpine ports: - "8881:80" diff --git a/integration/resources/compose/file.yml b/integration/resources/compose/file.yml index acd082616..1d3dddd1d 100644 --- a/integration/resources/compose/file.yml +++ b/integration/resources/compose/file.yml @@ -1,20 +1,20 @@ nginx1: - image: nginx + image: nginx:alpine ports: - "8881:80" nginx2: - image: nginx + image: nginx:alpine ports: - "8882:80" nginx3: - image: nginx + image: nginx:alpine ports: - "8883:80" nginx4: - image: nginx + image: nginx:alpine ports: - "8884:80" nginx5: - image: nginx + image: nginx:alpine ports: - "8885:80" From c19432f95c0f710a957e1bb901ee897b1ef4b600 Mon Sep 17 00:00:00 2001 From: Attilio Borello Date: Wed, 10 May 2017 09:33:41 +0200 Subject: [PATCH 12/17] clean up apt-cache in webui/Dockerfile --- webui/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webui/Dockerfile b/webui/Dockerfile index 5102a3969..c78d2bfd1 100644 --- a/webui/Dockerfile +++ b/webui/Dockerfile @@ -7,7 +7,8 @@ RUN apt-get -yq update \ && apt-get -yq --no-install-suggests --no-install-recommends --force-yes install apt-transport-https \ && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ -&& apt-get -yq update && apt-get -yq --no-install-suggests --no-install-recommends --force-yes install yarn +&& apt-get -yq update && apt-get -yq --no-install-suggests --no-install-recommends --force-yes install yarn \ +&& rm -fr /var/lib/apt/lists/ COPY package.json $WEBUI_DIR/ COPY yarn.lock $WEBUI_DIR/ From c3c599241f1b41a98aa50054e7ec6bb9526a5754 Mon Sep 17 00:00:00 2001 From: Attilio Borello Date: Fri, 12 May 2017 16:14:45 +0200 Subject: [PATCH 13/17] removed unit and integration tests from travis --- .travis.yml | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/.travis.yml b/.travis.yml index fdcdab568..919533465 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,32 +11,20 @@ env: - VERSION: $TRAVIS_TAG - CODENAME: raclette - N_MAKE_JOBS: 2 + - DOCKER_VERSION: 1.12.6 -matrix: - fast_finish: true - include: - - env: DOCKER_VERSION=1.10.3 - - env: DOCKER_VERSION=1.12.6 - -before_install: - - sudo -E apt-get -yq update - - sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install docker-engine=${DOCKER_VERSION}* -install: - - docker version - - pip install --user -r requirements.txt - - make pull-images -before_script: - - make validate script: - - make test-unit && travis_retry make test-integration - - make -j${N_MAKE_JOBS} crossbinary-default-parallel -after_failure: - - docker ps +- echo "Skipping tests... (Tests are executed on SemaphoreCI)" + before_deploy: - > if ! [ "$BEFORE_DEPLOY_RUN" ]; then export BEFORE_DEPLOY_RUN=1; - make -j${N_MAKE_JOBS} crossbinary-others-parallel; + sudo -E apt-get -yq update; + sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install docker-engine=${DOCKER_VERSION}*; + docker version; + pip install --user -r requirements.txt; + make -j${N_MAKE_JOBS} crossbinary-parallel; make image; mkdocs build --clean; tar cfz dist/traefik-${VERSION}.src.tar.gz --exclude-vcs --exclude dist .; From 5348d4dccd61366dc0041bbd35c58ed6bd53a988 Mon Sep 17 00:00:00 2001 From: Attilio Borello Date: Tue, 9 May 2017 09:19:12 +0200 Subject: [PATCH 14/17] added retry function to tests script --- .semaphoreci/tests.sh | 3 ++- .semaphoreci/vars | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.semaphoreci/tests.sh b/.semaphoreci/tests.sh index 15bb0fa6b..2a586ccf6 100755 --- a/.semaphoreci/tests.sh +++ b/.semaphoreci/tests.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash set -e -make test-unit && make test-integration +make test-unit +ci_retry make test-integration make -j${N_MAKE_JOBS} crossbinary-default-parallel diff --git a/.semaphoreci/vars b/.semaphoreci/vars index a17eabb76..5d2eb94ed 100644 --- a/.semaphoreci/vars +++ b/.semaphoreci/vars @@ -17,3 +17,25 @@ fi export CODENAME=raclette export N_MAKE_JOBS=2 + + +function ci_retry { + + local NRETRY=3 + local NSLEEP=5 + local n=0 + + until [ $n -ge $NRETRY ] + do + "$@" && break + n=$[$n+1] + echo "$@ failed, attempt ${n}/${NRETRY}" + sleep $NSLEEP + done + + [ $n -lt $NRETRY ] + +} + +export -f ci_retry + From 64aa37858b66cf8e83d17254187dade75af72565 Mon Sep 17 00:00:00 2001 From: Attilio Borello Date: Fri, 12 May 2017 16:27:16 +0200 Subject: [PATCH 15/17] added retry function to validate script --- .semaphoreci/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.semaphoreci/setup.sh b/.semaphoreci/setup.sh index 56bb0169d..e8c6e1e2e 100755 --- a/.semaphoreci/setup.sh +++ b/.semaphoreci/setup.sh @@ -8,4 +8,4 @@ docker version pip install --user -r requirements.txt make pull-images -make validate +ci_retry make validate From 12fa144f2fed6f6b31d709ac51d33d2154769e00 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Tue, 16 May 2017 15:28:18 +0200 Subject: [PATCH 16/17] doc: Traefik cluster in beta. --- docs/user-guide/cluster.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 From 88ea0a037b1ba6fa7ac8518c48de34102f803d66 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Tue, 16 May 2017 17:24:08 +0200 Subject: [PATCH 17/17] Fix deploy script, removes Docker version check --- script/deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"