Merge branch 'v1.3' into merge-v1.3.0-rc2-master
This commit is contained in:
commit
f8ea19d29c
21 changed files with 465 additions and 81 deletions
14
CHANGELOG.md
14
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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Clustering / High Availability
|
||||
# Clustering / High Availability (beta)
|
||||
|
||||
This guide explains how tu use Træfik in high availability mode.
|
||||
In order to deploy and configure multiple Træfik instances, without copying the same configuration file on each instance, we will use a distributed Key-Value store.
|
||||
|
@ -15,5 +15,6 @@ Please refer to [this section](/user-guide/kv-config/#store-configuration-in-key
|
|||
## Deploy a Træfik cluster
|
||||
|
||||
Once your Træfik configuration is uploaded on your KV store, you can start each Træfik instance.
|
||||
A Træfik cluster is based on a master/slave model. When starting, Træfik will elect a master. If this instance fails, another master will be automatically elected.
|
||||
A Træfik cluster is based on a master/slave model.
|
||||
When starting, Træfik will elect a master. If this instance fails, another master will be automatically elected.
|
||||
|
33
middlewares/recover.go
Normal file
33
middlewares/recover.go
Normal 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)
|
||||
}
|
||||
}
|
45
middlewares/recover_test.go
Normal file
45
middlewares/recover_test.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -12,10 +12,7 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
_ http.ResponseWriter = &ResponseRecorder{}
|
||||
_ http.Hijacker = &ResponseRecorder{}
|
||||
_ http.Flusher = &ResponseRecorder{}
|
||||
_ http.CloseNotifier = &ResponseRecorder{}
|
||||
_ Stateful = &ResponseRecorder{}
|
||||
)
|
||||
|
||||
// Retry is a middleware that retries requests
|
||||
|
|
12
middlewares/stateful.go
Normal file
12
middlewares/stateful.go
Normal 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
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
|
|
Loading…
Reference in a new issue