Compare commits

...

29 Commits

Author SHA1 Message Date
Emile Vauge
f521e72f15 Merge pull request #1612 from containous/fix-deploy
Fix deploy script, removes Docker version check
2017-05-16 17:53:47 +02:00
Emile Vauge
88ea0a037b Fix deploy script, removes Docker version check 2017-05-16 17:24:08 +02:00
Emile Vauge
c963cee3c8 Merge pull request #1606 from containous/prepare-release-v1.3.0-rc2
Prepare release v1.3.0-rc2
2017-05-16 16:15:05 +02:00
Emile Vauge
0be353d435 Merge pull request #1610 from ldez/beta-cluster
doc: Traefik cluster in beta.
2017-05-16 15:50:03 +02:00
Emile Vauge
6afff2d403 Merge pull request #1610 from ldez/beta-cluster
doc: Traefik cluster in beta.
2017-05-16 15:47:11 +02:00
Fernandez Ludovic
12fa144f2f doc: Traefik cluster in beta. 2017-05-16 15:28:18 +02:00
Emile Vauge
ac0e48b48c Merge pull request #1608 from ldez/feat-semaphoreci
SemaphoreCI on 1.3 branch
2017-05-16 15:08:39 +02:00
Attilio Borello
64aa37858b added retry function to validate script 2017-05-16 14:33:06 +02:00
Attilio Borello
5348d4dccd added retry function to tests script 2017-05-16 14:33:06 +02:00
Attilio Borello
c3c599241f removed unit and integration tests from travis 2017-05-16 14:33:06 +02:00
Attilio Borello
c19432f95c clean up apt-cache in webui/Dockerfile 2017-05-16 14:33:06 +02:00
Attilio Borello
bdf4f48d78 replaced docker images with alpine if available (nginx, rabbitmq) 2017-05-16 14:33:06 +02:00
Attilio Borello
21aa0ea2da added DOCKER_VERSION variable 2017-05-16 14:33:06 +02:00
Timo Reimann
f8e7b5595b Merge pull request #1585 from timoreimann/1-3-maintain-sticky-flag-on-lb-method-validation-failure
Maintain sticky flag on LB method validation failure.
2017-05-16 00:41:15 +02:00
Timo Reimann
f9839f7b1d Turn configureBackends into method. 2017-05-16 00:06:42 +02:00
Timo Reimann
2c45428c8a 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.
2017-05-16 00:06:42 +02:00
Emile Vauge
30aa5a82b3 Merge pull request #1577 from aantono/Issue1569
Fixed ReplacePath rule executing out of order, when combined with PathPrefixStrip
2017-05-15 23:21:53 +02:00
Alex Antonov
3f68e382fd Fixed ReplacePath rule executing out of order, when combined with PathPrefixStrip #1569 2017-05-15 10:08:18 -05:00
Emile Vauge
9e57a283d7 Merge pull request #1601 from containous/fix-fatal-auth
Fix empty basic auth
2017-05-15 17:00:52 +02:00
Emile Vauge
eaedc1b924 Fix empty basic auth
Signed-off-by: Emile Vauge <emile@vauge.com>
2017-05-15 16:03:46 +02:00
Emile Vauge
e3ab4e4d63 Merge pull request #1598 from containous/fix-stats-hijack
Fix stats hijack
2017-05-15 15:04:23 +02:00
Emile Vauge
48a91d05b5 Add Recover tests 2017-05-15 09:17:33 +02:00
Emile Vauge
111251da05 Adds Panic Recover middleware
Signed-off-by: Emile Vauge <emile@vauge.com>
2017-05-15 09:17:33 +02:00
Emile Vauge
71cec1580b Fix stats responseRecorder Hijacker
Signed-off-by: Emile Vauge <emile@vauge.com>
2017-05-15 09:17:33 +02:00
Emile Vauge
ffe1104851 Merge pull request #1588 from containous/fix-exported-fields-providers
Fix exported fields providers
2017-05-11 22:58:00 +02:00
Emile Vauge
aa4ed088bb Unexport Kvclient & StoreType from kv Provider
Signed-off-by: Emile Vauge <emile@vauge.com>
2017-05-11 19:33:32 +02:00
Emile Vauge
3a4ec19817 Add missing description tag
Signed-off-by: Emile Vauge <emile@vauge.com>
2017-05-11 19:33:32 +02:00
Timo Reimann
d2b204a075 Merge pull request #1581 from timoreimann/1-3-kubernetes-ignore-missing-pass-host-header-annotation
[Kubernetes] Ignore missing pass host header annotation. [v1.3 - CHERRY-PICK]
2017-05-11 17:43:14 +02:00
Timo Reimann
fe6c35bc6b [Kubernetes] Ignore missing pass host header annotation.
A missing annotation would previously be handled in the default error
case, causing a noisy warning-level log message to be generated each
time.

We add another case statement to ignore the case where the annotation is
missing from the annotations map.

Also piggybacking a minor improvement to the log message.
2017-05-10 19:32:45 +02:00
31 changed files with 522 additions and 118 deletions

View File

@@ -1,7 +1,11 @@
#!/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
make validate
ci_retry make validate

View File

@@ -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

View File

@@ -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
@@ -15,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

View File

@@ -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 .;

View File

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

View File

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

View File

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

View File

@@ -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"

View File

@@ -12,6 +12,6 @@ consul:
- "8302"
- "8302/udp"
nginx:
image: nginx
image: nginx:alpine
ports:
- "8881:80"

View File

@@ -12,6 +12,6 @@ consul:
- "8302"
- "8302/udp"
nginx:
image: nginx
image: nginx:alpine
ports:
- "8881:80"

View File

@@ -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"

33
middlewares/recover.go Normal file
View File

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

View File

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

View File

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

12
middlewares/stateful.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -156,14 +156,16 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
PassHostHeader := p.getPassHostHeader()
passHostHeaderAnnotation := i.Annotations["traefik.frontend.passHostHeader"]
switch passHostHeaderAnnotation {
case "true":
PassHostHeader = true
case "false":
passHostHeaderAnnotation, ok := i.Annotations["traefik.frontend.passHostHeader"]
switch {
case !ok:
// No op.
case passHostHeaderAnnotation == "false":
PassHostHeader = false
case passHostHeaderAnnotation == "true":
PassHostHeader = true
default:
log.Warnf("Unknown value of %s for traefik.frontend.passHostHeader, falling back to %s", passHostHeaderAnnotation, PassHostHeader)
log.Warnf("Unknown value '%s' for traefik.frontend.passHostHeader, falling back to %s", passHostHeaderAnnotation, PassHostHeader)
}
if realm := i.Annotations["ingress.kubernetes.io/auth-realm"]; realm != "" && realm != traefikDefaultRealm {
return nil, errors.New("no realm customization supported")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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))
@@ -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)
server.configureBackends(configuration.Backends)
}
func (server *Server) listenConfigurations(stop chan bool) {
@@ -655,7 +644,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 +672,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 +722,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)
@@ -775,7 +765,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,
@@ -796,14 +796,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)
}
@@ -843,8 +835,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
}
@@ -887,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 (*Server) configureBackends(backends map[string]*types.Backend) {
for backendName, backend := range backends {
_, err := types.NewLoadBalancerMethod(backend.LoadBalancer)
if err != nil {
log.Debugf("Validation of load balancer method for backend %s failed: %s. Using default method wrr.", backendName, err)
var sticky bool
if backend.LoadBalancer != nil {
sticky = backend.LoadBalancer.Sticky
}
backend.LoadBalancer = &types.LoadBalancer{
Method: "wrr",
Sticky: sticky,
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -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/