From dbc796359fd5d6ca82329c051e4807e8887a31ac Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Thu, 12 Oct 2017 11:10:03 +0200 Subject: [PATCH 01/13] Fix Proxy Protocol documentation --- docs/configuration/entrypoints.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/configuration/entrypoints.md b/docs/configuration/entrypoints.md index 3defd7fd5..c626352e7 100644 --- a/docs/configuration/entrypoints.md +++ b/docs/configuration/entrypoints.md @@ -188,11 +188,14 @@ To enable IP whitelisting at the entrypoint level. whiteListSourceRange = ["127.0.0.1/32", "192.168.1.7"] ``` -## ProxyProtocol Support +## ProxyProtocol To enable [ProxyProtocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) support. -Only IPs in `trustedIPs` will lead to remote client address replacement: you should declare your load-balancer IP or CIDR range here. +Only IPs in `trustedIPs` will lead to remote client address replacement: you should declare your load-balancer IP or CIDR range here (in testing environment, you can trust everyone using `0.0.0.0/0`). +!!! danger + When queuing Træfik behind another load-balancer, be sure to carefully configure Proxy Protocol on both sides. + Otherwise, it could introduce a security risk in your system by forging requests. ```toml [entryPoints] @@ -201,4 +204,3 @@ Only IPs in `trustedIPs` will lead to remote client address replacement: you sho [entryPoints.http.proxyProtocol] trustedIPs = ["127.0.0.1/32", "192.168.1.7"] ``` -² \ No newline at end of file From 7f2582e3b65d9b39980af019300148e7183a78c2 Mon Sep 17 00:00:00 2001 From: SALLEYRON Julien Date: Thu, 12 Oct 2017 15:10:04 +0200 Subject: [PATCH 02/13] Nil body retries --- glide.lock | 4 ++-- glide.yaml | 2 +- vendor/github.com/vulcand/oxy/forward/fwd.go | 6 ++++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/glide.lock b/glide.lock index 94577fae0..990a41429 100644 --- a/glide.lock +++ b/glide.lock @@ -1,4 +1,4 @@ -hash: 9d176906cf25eaa3224c750b1dd3a5c8ce3340a58df3c562e57572c6294e16f0 +hash: d87c01b4b8f802c81e1f3ae34a09c7001dc392654703b53fe0e6722041183abc updated: 2017-09-30T18:32:16.848940186+02:00 imports: - name: cloud.google.com/go @@ -481,7 +481,7 @@ imports: - name: github.com/urfave/negroni version: 490e6a555d47ca891a89a150d0c1ef3922dfffe9 - name: github.com/vulcand/oxy - version: 648088ee0902cf8d8337826ae2a82444008720e2 + version: c024a22700b56debed9a9c8dbb297210a7ece02d repo: https://github.com/containous/oxy.git vcs: git subpackages: diff --git a/glide.yaml b/glide.yaml index e6817d4d8..d48639846 100644 --- a/glide.yaml +++ b/glide.yaml @@ -12,7 +12,7 @@ import: - package: github.com/cenk/backoff - package: github.com/containous/flaeg - package: github.com/vulcand/oxy - version: 648088ee0902cf8d8337826ae2a82444008720e2 + version: c024a22700b56debed9a9c8dbb297210a7ece02d repo: https://github.com/containous/oxy.git vcs: git subpackages: diff --git a/vendor/github.com/vulcand/oxy/forward/fwd.go b/vendor/github.com/vulcand/oxy/forward/fwd.go index a30c76ba0..09225f4c6 100644 --- a/vendor/github.com/vulcand/oxy/forward/fwd.go +++ b/vendor/github.com/vulcand/oxy/forward/fwd.go @@ -249,6 +249,12 @@ func (f *httpForwarder) copyRequest(req *http.Request, u *url.URL) *http.Request if f.rewriter != nil { f.rewriter.Rewrite(outReq) } + + if req.ContentLength == 0 { + // https://github.com/golang/go/issues/16036: nil Body for http.Transport retries + outReq.Body = nil + } + return outReq } From 8d158402f3a72e89c090159fa70a260efdd06db7 Mon Sep 17 00:00:00 2001 From: Timo Reimann Date: Thu, 12 Oct 2017 15:48:03 +0200 Subject: [PATCH 03/13] Continue processing on invalid auth-realm annotation. --- provider/kubernetes/kubernetes.go | 4 +++- provider/kubernetes/kubernetes_test.go | 28 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index e5f5a65b5..a62f0c107 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -180,7 +180,9 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) log.Warnf("Unknown value '%s' for %s, falling back to %s", passHostHeaderAnnotation, types.LabelFrontendPassHostHeader, PassHostHeader) } if realm := i.Annotations[annotationKubernetesAuthRealm]; realm != "" && realm != traefikDefaultRealm { - return nil, errors.New("no realm customization supported") + log.Errorf("Value for annotation %q on ingress %s/%s invalid: no realm customization supported", annotationKubernetesAuthRealm, i.ObjectMeta.Namespace, i.ObjectMeta.Name) + delete(templateObjects.Backends, r.Host+pa.Path) + continue } witelistSourceRangeAnnotation := i.Annotations[annotationKubernetesWhitelistSourceRange] diff --git a/provider/kubernetes/kubernetes_test.go b/provider/kubernetes/kubernetes_test.go index e8df03ea1..3ac0905fe 100644 --- a/provider/kubernetes/kubernetes_test.go +++ b/provider/kubernetes/kubernetes_test.go @@ -1531,6 +1531,34 @@ func TestIngressAnnotations(t *testing.T) { }, }, }, + { + ObjectMeta: v1.ObjectMeta{ + Namespace: "testing", + Annotations: map[string]string{ + "ingress.kubernetes.io/auth-realm": "customized", + }, + }, + Spec: v1beta1.IngressSpec{ + Rules: []v1beta1.IngressRule{ + { + Host: "auth-realm-customized", + IngressRuleValue: v1beta1.IngressRuleValue{ + HTTP: &v1beta1.HTTPIngressRuleValue{ + Paths: []v1beta1.HTTPIngressPath{ + { + Path: "/auth-realm-customized", + Backend: v1beta1.IngressBackend{ + ServiceName: "service1", + ServicePort: intstr.FromInt(80), + }, + }, + }, + }, + }, + }, + }, + }, + }, } services := []*v1.Service{ { From cba0898e4fc6f5546e1199655d13291e94c69b57 Mon Sep 17 00:00:00 2001 From: Manuel Zapf Date: Thu, 12 Oct 2017 16:26:03 +0200 Subject: [PATCH 04/13] fix seconds to really be seconds --- provider/rancher/metadata.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provider/rancher/metadata.go b/provider/rancher/metadata.go index 24c9c585b..37c470ecc 100644 --- a/provider/rancher/metadata.go +++ b/provider/rancher/metadata.go @@ -85,7 +85,7 @@ func (p *Provider) intervalPoll(client rancher.Client, updateConfiguration func( _, cancel := context.WithCancel(context.Background()) defer cancel() - ticker := time.NewTicker(time.Duration(p.RefreshSeconds)) + ticker := time.NewTicker(time.Second * time.Duration(p.RefreshSeconds)) defer ticker.Stop() var version string From 8cb3f0835a0579b3506840edb845a7fa32e29d22 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 12 Oct 2017 17:50:03 +0200 Subject: [PATCH 05/13] Stickiness cookie name. --- provider/consul/consul_catalog.go | 2 +- provider/consul/consul_catalog_test.go | 67 +++++++++ provider/docker/docker.go | 12 +- provider/docker/docker_test.go | 78 ++++++++++ provider/ecs/ecs.go | 2 +- provider/kubernetes/kubernetes.go | 9 +- provider/kubernetes/kubernetes_test.go | 2 +- provider/kv/kv.go | 16 ++- provider/kv/kv_mock_test.go | 110 ++++++++++++++ provider/kv/kv_test.go | 189 ++++++++++++------------- provider/marathon/marathon.go | 12 +- provider/marathon/marathon_test.go | 37 +++-- provider/rancher/rancher.go | 12 +- provider/rancher/rancher_test.go | 95 +++++++++++++ server/server.go | 18 ++- templates/consul_catalog.tmpl | 2 +- templates/docker.tmpl | 2 +- templates/ecs.tmpl | 2 +- templates/kubernetes.tmpl | 2 +- templates/marathon.tmpl | 2 +- templates/rancher.tmpl | 2 +- 21 files changed, 525 insertions(+), 148 deletions(-) create mode 100644 provider/kv/kv_mock_test.go diff --git a/provider/consul/consul_catalog.go b/provider/consul/consul_catalog.go index dd2c75156..02230a621 100644 --- a/provider/consul/consul_catalog.go +++ b/provider/consul/consul_catalog.go @@ -398,7 +398,7 @@ func (p *CatalogProvider) hasStickinessLabel(tags []string) bool { stickyTag := p.getTag(types.LabelBackendLoadbalancerSticky, tags, "") if len(stickyTag) > 0 { - log.Warn("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) + log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) } stickiness := len(stickinessTag) > 0 && strings.EqualFold(strings.TrimSpace(stickinessTag), "true") diff --git a/provider/consul/consul_catalog_test.go b/provider/consul/consul_catalog_test.go index 7cd3c25eb..7b8ce4a7b 100644 --- a/provider/consul/consul_catalog_test.go +++ b/provider/consul/consul_catalog_test.go @@ -9,6 +9,7 @@ import ( "github.com/BurntSushi/ty/fun" "github.com/containous/traefik/types" "github.com/hashicorp/consul/api" + "github.com/stretchr/testify/assert" ) func TestConsulCatalogGetFrontendRule(t *testing.T) { @@ -891,3 +892,69 @@ func TestConsulCatalogGetBasicAuth(t *testing.T) { }) } } + +func TestConsulCatalogHasStickinessLabel(t *testing.T) { + testCases := []struct { + desc string + tags []string + expected bool + }{ + { + desc: "label missing", + tags: []string{}, + expected: false, + }, + { + desc: "sticky=true", + tags: []string{ + "traefik.backend.loadbalancer.sticky=true", + }, + expected: true, + }, + { + desc: "stickiness=true", + tags: []string{ + "traefik.backend.loadbalancer.stickiness=true", + }, + expected: true, + }, + { + desc: "sticky=true and stickiness=true", + tags: []string{ + "traefik.backend.loadbalancer.sticky=true", + "traefik.backend.loadbalancer.stickiness=true", + }, + expected: true, + }, + { + desc: "sticky=false and stickiness=true", + tags: []string{ + "traefik.backend.loadbalancer.sticky=true", + "traefik.backend.loadbalancer.stickiness=false", + }, + expected: true, + }, + { + desc: "sticky=true and stickiness=false", + tags: []string{ + "traefik.backend.loadbalancer.sticky=true", + "traefik.backend.loadbalancer.stickiness=false", + }, + expected: true, + }, + } + + provider := &CatalogProvider{ + Prefix: "traefik", + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := provider.hasStickinessLabel(test.tags) + assert.Equal(t, actual, test.expected) + }) + } +} diff --git a/provider/docker/docker.go b/provider/docker/docker.go index ec0b7f707..2b1b8d61a 100644 --- a/provider/docker/docker.go +++ b/provider/docker/docker.go @@ -645,14 +645,16 @@ func (p *Provider) getWeight(container dockerData) string { } func (p *Provider) hasStickinessLabel(container dockerData) bool { - _, errStickiness := getLabel(container, types.LabelBackendLoadbalancerStickiness) + labelStickiness, errStickiness := getLabel(container, types.LabelBackendLoadbalancerStickiness) - label, errSticky := getLabel(container, types.LabelBackendLoadbalancerSticky) - if len(label) > 0 { - log.Warn("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) + labelSticky, errSticky := getLabel(container, types.LabelBackendLoadbalancerSticky) + if len(labelSticky) > 0 { + log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) } - return errStickiness == nil || (errSticky == nil && strings.EqualFold(strings.TrimSpace(label), "true")) + stickiness := errStickiness == nil && len(labelStickiness) > 0 && strings.EqualFold(strings.TrimSpace(labelStickiness), "true") + sticky := errSticky == nil && len(labelSticky) > 0 && strings.EqualFold(strings.TrimSpace(labelSticky), "true") + return stickiness || sticky } func (p *Provider) getStickinessCookieName(container dockerData) string { diff --git a/provider/docker/docker_test.go b/provider/docker/docker_test.go index a8f3a0bf0..0701dc2e2 100644 --- a/provider/docker/docker_test.go +++ b/provider/docker/docker_test.go @@ -10,6 +10,7 @@ import ( docker "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/go-connections/nat" + "github.com/stretchr/testify/assert" ) func TestDockerGetFrontendName(t *testing.T) { @@ -1051,3 +1052,80 @@ func TestDockerLoadDockerConfig(t *testing.T) { }) } } + +func TestDockerHasStickinessLabel(t *testing.T) { + testCases := []struct { + desc string + container docker.ContainerJSON + expected bool + }{ + { + desc: "no sticky/stickiness-label", + container: containerJSON(), + expected: false, + }, + { + desc: "sticky true", + container: containerJSON(labels(map[string]string{ + types.LabelBackendLoadbalancerSticky: "true", + })), + expected: true, + }, + { + desc: "sticky false", + container: containerJSON(labels(map[string]string{ + types.LabelBackendLoadbalancerSticky: "false", + })), + expected: false, + }, + { + desc: "stickiness true", + container: containerJSON(labels(map[string]string{ + types.LabelBackendLoadbalancerStickiness: "true", + })), + expected: true, + }, + { + desc: "stickiness false", + container: containerJSON(labels(map[string]string{ + types.LabelBackendLoadbalancerStickiness: "false", + })), + expected: false, + }, + { + desc: "sticky true + stickiness false", + container: containerJSON(labels(map[string]string{ + types.LabelBackendLoadbalancerSticky: "true", + types.LabelBackendLoadbalancerStickiness: "false", + })), + expected: true, + }, + { + desc: "sticky false + stickiness true", + container: containerJSON(labels(map[string]string{ + types.LabelBackendLoadbalancerSticky: "false", + types.LabelBackendLoadbalancerStickiness: "true", + })), + expected: true, + }, + { + desc: "sticky false + stickiness false", + container: containerJSON(labels(map[string]string{ + types.LabelBackendLoadbalancerSticky: "false", + types.LabelBackendLoadbalancerStickiness: "false", + })), + expected: false, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + dockerData := parseContainer(test.container) + provider := &Provider{} + actual := provider.hasStickinessLabel(dockerData) + assert.Equal(t, actual, test.expected) + }) + } +} diff --git a/provider/ecs/ecs.go b/provider/ecs/ecs.go index a8bb03365..d76e3f939 100644 --- a/provider/ecs/ecs.go +++ b/provider/ecs/ecs.go @@ -490,7 +490,7 @@ func (p *Provider) hasStickinessLabel(instances []ecsInstance) bool { stickyLabel := getFirstInstanceLabel(instances, types.LabelBackendLoadbalancerSticky) if len(stickyLabel) > 0 { - log.Warn("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) + log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) } stickiness := len(stickinessLabel) > 0 && strings.EqualFold(strings.TrimSpace(stickinessLabel), "true") sticky := len(stickyLabel) > 0 && strings.EqualFold(strings.TrimSpace(stickyLabel), "true") diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index a62f0c107..5d38b5af7 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -18,7 +18,6 @@ import ( "github.com/containous/traefik/log" "github.com/containous/traefik/provider" "github.com/containous/traefik/safe" - "github.com/containous/traefik/server/cookie" "github.com/containous/traefik/types" "k8s.io/client-go/pkg/api/v1" "k8s.io/client-go/pkg/apis/extensions/v1beta1" @@ -185,8 +184,8 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) continue } - witelistSourceRangeAnnotation := i.Annotations[annotationKubernetesWhitelistSourceRange] - whitelistSourceRange := provider.SplitAndTrimString(witelistSourceRangeAnnotation) + whitelistSourceRangeAnnotation := i.Annotations[annotationKubernetesWhitelistSourceRange] + whitelistSourceRange := provider.SplitAndTrimString(whitelistSourceRangeAnnotation) if _, exists := templateObjects.Frontends[r.Host+pa.Path]; !exists { basicAuthCreds, err := handleBasicAuthConfig(i, k8sClient) @@ -250,12 +249,12 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) } if len(service.Annotations[types.LabelBackendLoadbalancerSticky]) > 0 { - log.Warn("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) + log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) } if service.Annotations[types.LabelBackendLoadbalancerSticky] == "true" || service.Annotations[types.LabelBackendLoadbalancerStickiness] == "true" { templateObjects.Backends[r.Host+pa.Path].LoadBalancer.Stickiness = &types.Stickiness{ - CookieName: cookie.GenerateName(r.Host + pa.Path), + CookieName: r.Host + pa.Path, } } diff --git a/provider/kubernetes/kubernetes_test.go b/provider/kubernetes/kubernetes_test.go index 3ac0905fe..5e0a66bf9 100644 --- a/provider/kubernetes/kubernetes_test.go +++ b/provider/kubernetes/kubernetes_test.go @@ -1326,7 +1326,7 @@ func TestServiceAnnotations(t *testing.T) { LoadBalancer: &types.LoadBalancer{ Method: "wrr", Stickiness: &types.Stickiness{ - CookieName: "_4155f", + CookieName: "bar", }, }, }, diff --git a/provider/kv/kv.go b/provider/kv/kv.go index 36beb8650..c75d2f231 100644 --- a/provider/kv/kv.go +++ b/provider/kv/kv.go @@ -243,13 +243,17 @@ func (p *Provider) checkConstraints(keys ...string) bool { } func (p *Provider) hasStickinessLabel(rootPath string) bool { - stickiness, err := p.kvclient.Exists(rootPath + "/loadbalancer/stickiness") - if err != nil { - log.Debugf("Error occurs when check stickiness: %v", err) - } - sticky := p.get("false", rootPath, "/loadbalancer", "/sticky") + stickyValue := p.get("false", rootPath, "/loadbalancer", "/sticky") - return stickiness || (len(sticky) != 0 && strings.EqualFold(strings.TrimSpace(sticky), "true")) + sticky := len(stickyValue) != 0 && strings.EqualFold(strings.TrimSpace(stickyValue), "true") + if sticky { + log.Warnf("Deprecated configuration found: %s. Please use %s.", "loadbalancer/sticky", "loadbalancer/stickiness") + } + + stickinessValue := p.get("false", rootPath, "/loadbalancer", "/stickiness") + stickiness := len(stickinessValue) > 0 && strings.EqualFold(strings.TrimSpace(stickinessValue), "true") + + return stickiness || sticky } func (p *Provider) getStickinessCookieName(rootPath string) string { diff --git a/provider/kv/kv_mock_test.go b/provider/kv/kv_mock_test.go new file mode 100644 index 000000000..fddb2192e --- /dev/null +++ b/provider/kv/kv_mock_test.go @@ -0,0 +1,110 @@ +package kv + +import ( + "errors" + "strings" + + "github.com/containous/traefik/types" + "github.com/docker/libkv/store" +) + +type KvMock struct { + Provider +} + +func (provider *KvMock) loadConfig() *types.Configuration { + return nil +} + +// Override Get/List to return a error +type KvError struct { + Get error + List error +} + +// Extremely limited mock store so we can test initialization +type Mock struct { + Error KvError + KVPairs []*store.KVPair + WatchTreeMethod func() <-chan []*store.KVPair +} + +func (s *Mock) Put(key string, value []byte, opts *store.WriteOptions) error { + return errors.New("Put not supported") +} + +func (s *Mock) Get(key string) (*store.KVPair, error) { + if err := s.Error.Get; err != nil { + return nil, err + } + for _, kvPair := range s.KVPairs { + if kvPair.Key == key { + return kvPair, nil + } + } + return nil, store.ErrKeyNotFound +} + +func (s *Mock) Delete(key string) error { + return errors.New("Delete not supported") +} + +// Exists mock +func (s *Mock) Exists(key string) (bool, error) { + if err := s.Error.Get; err != nil { + return false, err + } + for _, kvPair := range s.KVPairs { + if strings.HasPrefix(kvPair.Key, key) { + return true, nil + } + } + return false, store.ErrKeyNotFound +} + +// Watch mock +func (s *Mock) Watch(key string, stopCh <-chan struct{}) (<-chan *store.KVPair, error) { + return nil, errors.New("Watch not supported") +} + +// WatchTree mock +func (s *Mock) WatchTree(prefix string, stopCh <-chan struct{}) (<-chan []*store.KVPair, error) { + return s.WatchTreeMethod(), nil +} + +// NewLock mock +func (s *Mock) NewLock(key string, options *store.LockOptions) (store.Locker, error) { + return nil, errors.New("NewLock not supported") +} + +// List mock +func (s *Mock) List(prefix string) ([]*store.KVPair, error) { + if err := s.Error.List; err != nil { + return nil, err + } + kv := []*store.KVPair{} + for _, kvPair := range s.KVPairs { + if strings.HasPrefix(kvPair.Key, prefix) && !strings.ContainsAny(strings.TrimPrefix(kvPair.Key, prefix), "/") { + kv = append(kv, kvPair) + } + } + return kv, nil +} + +// DeleteTree mock +func (s *Mock) DeleteTree(prefix string) error { + return errors.New("DeleteTree not supported") +} + +// AtomicPut mock +func (s *Mock) AtomicPut(key string, value []byte, previous *store.KVPair, opts *store.WriteOptions) (bool, *store.KVPair, error) { + return false, nil, errors.New("AtomicPut not supported") +} + +// AtomicDelete mock +func (s *Mock) AtomicDelete(key string, previous *store.KVPair) (bool, error) { + return false, errors.New("AtomicDelete not supported") +} + +// Close mock +func (s *Mock) Close() {} diff --git a/provider/kv/kv_test.go b/provider/kv/kv_test.go index 3daf2d98d..91aa64de8 100644 --- a/provider/kv/kv_test.go +++ b/provider/kv/kv_test.go @@ -1,10 +1,8 @@ package kv import ( - "errors" "reflect" "sort" - "strings" "testing" "time" @@ -237,14 +235,6 @@ func TestKvLast(t *testing.T) { } } -type KvMock struct { - Provider -} - -func (provider *KvMock) loadConfig() *types.Configuration { - return nil -} - func TestKvWatchTree(t *testing.T) { returnedChans := make(chan chan []*store.KVPair) provider := &KvMock{ @@ -288,91 +278,6 @@ func TestKvWatchTree(t *testing.T) { } } -// Override Get/List to return a error -type KvError struct { - Get error - List error -} - -// Extremely limited mock store so we can test initialization -type Mock struct { - Error KvError - KVPairs []*store.KVPair - WatchTreeMethod func() <-chan []*store.KVPair -} - -func (s *Mock) Put(key string, value []byte, opts *store.WriteOptions) error { - return errors.New("Put not supported") -} - -func (s *Mock) Get(key string) (*store.KVPair, error) { - if err := s.Error.Get; err != nil { - return nil, err - } - for _, kvPair := range s.KVPairs { - if kvPair.Key == key { - return kvPair, nil - } - } - return nil, store.ErrKeyNotFound -} - -func (s *Mock) Delete(key string) error { - return errors.New("Delete not supported") -} - -// Exists mock -func (s *Mock) Exists(key string) (bool, error) { - return false, errors.New("Exists not supported") -} - -// Watch mock -func (s *Mock) Watch(key string, stopCh <-chan struct{}) (<-chan *store.KVPair, error) { - return nil, errors.New("Watch not supported") -} - -// WatchTree mock -func (s *Mock) WatchTree(prefix string, stopCh <-chan struct{}) (<-chan []*store.KVPair, error) { - return s.WatchTreeMethod(), nil -} - -// NewLock mock -func (s *Mock) NewLock(key string, options *store.LockOptions) (store.Locker, error) { - return nil, errors.New("NewLock not supported") -} - -// List mock -func (s *Mock) List(prefix string) ([]*store.KVPair, error) { - if err := s.Error.List; err != nil { - return nil, err - } - kv := []*store.KVPair{} - for _, kvPair := range s.KVPairs { - if strings.HasPrefix(kvPair.Key, prefix) && !strings.ContainsAny(strings.TrimPrefix(kvPair.Key, prefix), "/") { - kv = append(kv, kvPair) - } - } - return kv, nil -} - -// DeleteTree mock -func (s *Mock) DeleteTree(prefix string) error { - return errors.New("DeleteTree not supported") -} - -// AtomicPut mock -func (s *Mock) AtomicPut(key string, value []byte, previous *store.KVPair, opts *store.WriteOptions) (bool, *store.KVPair, error) { - return false, nil, errors.New("AtomicPut not supported") -} - -// AtomicDelete mock -func (s *Mock) AtomicDelete(key string, previous *store.KVPair) (bool, error) { - return false, errors.New("AtomicDelete not supported") -} - -// Close mock -func (s *Mock) Close() {} - func TestKVLoadConfig(t *testing.T) { provider := &Provider{ Prefix: "traefik", @@ -463,3 +368,97 @@ func TestKVLoadConfig(t *testing.T) { t.Fatalf("expected %+v, got %+v", expected.Frontends, actual.Frontends) } } + +func TestKVHasStickinessLabel(t *testing.T) { + testCases := []struct { + desc string + KVPairs []*store.KVPair + expected bool + }{ + { + desc: "without option", + expected: false, + }, + { + desc: "with cookie name without stickiness=true", + KVPairs: []*store.KVPair{ + { + Key: "loadbalancer/stickiness/cookiename", + Value: []byte("foo"), + }, + }, + expected: false, + }, + { + desc: "stickiness=true", + KVPairs: []*store.KVPair{ + { + Key: "loadbalancer/stickiness", + Value: []byte("true"), + }, + }, + expected: true, + }, + { + desc: "stickiness=true and sticky=true", + KVPairs: []*store.KVPair{ + { + Key: "loadbalancer/stickiness", + Value: []byte("true"), + }, + { + Key: "loadbalancer/sticky", + Value: []byte("true"), + }, + }, + expected: true, + }, + { + desc: "stickiness=false and sticky=true", + KVPairs: []*store.KVPair{ + { + Key: "loadbalancer/stickiness", + Value: []byte("false"), + }, + { + Key: "loadbalancer/sticky", + Value: []byte("true"), + }, + }, + expected: true, + }, + { + desc: "stickiness=true and sticky=false", + KVPairs: []*store.KVPair{ + { + Key: "loadbalancer/stickiness", + Value: []byte("true"), + }, + { + Key: "loadbalancer/sticky", + Value: []byte("false"), + }, + }, + expected: true, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + p := &Provider{ + kvclient: &Mock{ + KVPairs: test.KVPairs, + }, + } + + actual := p.hasStickinessLabel("") + + if actual != test.expected { + t.Fatalf("expected %v, got %v", test.expected, actual) + } + }) + } +} diff --git a/provider/marathon/marathon.go b/provider/marathon/marathon.go index d8639b13c..982306bf7 100644 --- a/provider/marathon/marathon.go +++ b/provider/marathon/marathon.go @@ -430,14 +430,16 @@ func (p *Provider) getProtocol(application marathon.Application, serviceName str } func (p *Provider) hasStickinessLabel(application marathon.Application) bool { - _, okStickiness := p.getAppLabel(application, types.LabelBackendLoadbalancerStickiness) + labelStickiness, okStickiness := p.getAppLabel(application, types.LabelBackendLoadbalancerStickiness) - label, okSticky := p.getAppLabel(application, types.LabelBackendLoadbalancerSticky) - if len(label) > 0 { - log.Warn("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) + labelSticky, okSticky := p.getAppLabel(application, types.LabelBackendLoadbalancerSticky) + if len(labelSticky) > 0 { + log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) } - return okStickiness || (okSticky && strings.EqualFold(strings.TrimSpace(label), "true")) + stickiness := okStickiness && len(labelStickiness) > 0 && strings.EqualFold(strings.TrimSpace(labelStickiness), "true") + sticky := okSticky && len(labelSticky) > 0 && strings.EqualFold(strings.TrimSpace(labelSticky), "true") + return stickiness || sticky } func (p *Provider) getStickinessCookieName(application marathon.Application) string { diff --git a/provider/marathon/marathon_test.go b/provider/marathon/marathon_test.go index e78fd3f0b..145cefe5e 100644 --- a/provider/marathon/marathon_test.go +++ b/provider/marathon/marathon_test.go @@ -856,7 +856,7 @@ func TestMarathonGetProtocol(t *testing.T) { } func TestMarathonHasStickinessLabel(t *testing.T) { - cases := []struct { + testCases := []struct { desc string application marathon.Application expected bool @@ -867,35 +867,50 @@ func TestMarathonHasStickinessLabel(t *testing.T) { expected: false, }, { - desc: "label existing and value equals true (deprecated)", + desc: "sticky=true (deprecated)", application: application(label(types.LabelBackendLoadbalancerSticky, "true")), expected: true, }, { - desc: "label existing and value equals false (deprecated)", + desc: "sticky=false (deprecated)", application: application(label(types.LabelBackendLoadbalancerSticky, "false")), expected: false, }, { - desc: "label existing and value equals true", + desc: "stickiness=true", application: application(label(types.LabelBackendLoadbalancerStickiness, "true")), expected: true, }, { - desc: "label existing and value equals false ", + desc: "stickiness=false ", application: application(label(types.LabelBackendLoadbalancerStickiness, "true")), expected: true, }, + { + desc: "sticky=false stickiness=true ", + application: application( + label(types.LabelBackendLoadbalancerStickiness, "true"), + label(types.LabelBackendLoadbalancerSticky, "false")), + expected: true, + }, + { + desc: "sticky=true stickiness=false ", + application: application( + label(types.LabelBackendLoadbalancerStickiness, "false"), + label(types.LabelBackendLoadbalancerSticky, "true")), + expected: true, + }, } - for _, c := range cases { - c := c - t.Run(c.desc, func(t *testing.T) { + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { t.Parallel() + provider := &Provider{} - actual := provider.hasStickinessLabel(c.application) - if actual != c.expected { - t.Errorf("actual %q, expected %q", actual, c.expected) + actual := provider.hasStickinessLabel(test.application) + if actual != test.expected { + t.Errorf("actual %q, expected %q", actual, test.expected) } }) } diff --git a/provider/rancher/rancher.go b/provider/rancher/rancher.go index 6327f460b..948c3c30b 100644 --- a/provider/rancher/rancher.go +++ b/provider/rancher/rancher.go @@ -113,14 +113,16 @@ func (p *Provider) getCircuitBreakerExpression(service rancherData) string { } func (p *Provider) hasStickinessLabel(service rancherData) bool { - _, errStickiness := getServiceLabel(service, types.LabelBackendLoadbalancerStickiness) + labelStickiness, errStickiness := getServiceLabel(service, types.LabelBackendLoadbalancerStickiness) - label, errSticky := getServiceLabel(service, types.LabelBackendLoadbalancerSticky) - if len(label) > 0 { - log.Warn("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) + labelSticky, errSticky := getServiceLabel(service, types.LabelBackendLoadbalancerSticky) + if len(labelSticky) > 0 { + log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) } - return errStickiness == nil || (errSticky == nil && strings.EqualFold(strings.TrimSpace(label), "true")) + stickiness := errStickiness == nil && len(labelStickiness) > 0 && strings.EqualFold(strings.TrimSpace(labelStickiness), "true") + sticky := errSticky == nil && len(labelSticky) > 0 && strings.EqualFold(strings.TrimSpace(labelSticky), "true") + return stickiness || sticky } func (p *Provider) getStickinessCookieName(service rancherData, backendName string) string { diff --git a/provider/rancher/rancher_test.go b/provider/rancher/rancher_test.go index 7fcf03568..d56b552e9 100644 --- a/provider/rancher/rancher_test.go +++ b/provider/rancher/rancher_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/containous/traefik/types" + "github.com/stretchr/testify/assert" ) func TestRancherServiceFilter(t *testing.T) { @@ -597,3 +598,97 @@ func TestRancherLoadRancherConfig(t *testing.T) { } } } + +func TestRancherHasStickinessLabel(t *testing.T) { + provider := &Provider{ + Domain: "rancher.localhost", + } + + testCases := []struct { + desc string + service rancherData + expected bool + }{ + { + desc: "no labels", + service: rancherData{ + Name: "test-service", + }, + expected: false, + }, + { + desc: "sticky=true", + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + types.LabelBackendLoadbalancerSticky: "true", + }, + }, + expected: true, + }, + { + desc: "stickiness=true", + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + types.LabelBackendLoadbalancerStickiness: "true", + }, + }, + expected: true, + }, + { + desc: "sticky=true and stickiness=true", + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + types.LabelBackendLoadbalancerSticky: "true", + types.LabelBackendLoadbalancerStickiness: "true", + }, + }, + expected: true, + }, + { + desc: "sticky=false and stickiness=false", + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + types.LabelBackendLoadbalancerSticky: "false", + types.LabelBackendLoadbalancerStickiness: "false", + }, + }, + expected: false, + }, + { + desc: "sticky=true and stickiness=false", + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + types.LabelBackendLoadbalancerSticky: "true", + types.LabelBackendLoadbalancerStickiness: "false", + }, + }, + expected: true, + }, + { + desc: "sticky=false and stickiness=true", + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + types.LabelBackendLoadbalancerSticky: "false", + types.LabelBackendLoadbalancerStickiness: "true", + }, + }, + expected: true, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := provider.hasStickinessLabel(test.service) + assert.Equal(t, actual, test.expected) + }) + } +} diff --git a/server/server.go b/server/server.go index 24360d967..3a5a76cb9 100644 --- a/server/server.go +++ b/server/server.go @@ -1166,25 +1166,29 @@ func (server *Server) configureFrontends(frontends map[string]*types.Frontend) { func (*Server) configureBackends(backends map[string]*types.Backend) { for backendName, backend := range backends { if backend.LoadBalancer != nil && backend.LoadBalancer.Sticky { - log.Warn("Deprecated configuration found: %s. Please use %s.", "backend.LoadBalancer.Sticky", "backend.LoadBalancer.Stickiness") + log.Warnf("Deprecated configuration found: %s. Please use %s.", "backend.LoadBalancer.Sticky", "backend.LoadBalancer.Stickiness") } _, err := types.NewLoadBalancerMethod(backend.LoadBalancer) if err == nil { if backend.LoadBalancer != nil && backend.LoadBalancer.Stickiness == nil && backend.LoadBalancer.Sticky { - backend.LoadBalancer.Stickiness = &types.Stickiness{} + backend.LoadBalancer.Stickiness = &types.Stickiness{ + CookieName: "_TRAEFIK_BACKEND", + } } } else { log.Debugf("Validation of load balancer method for backend %s failed: %s. Using default method wrr.", backendName, err) var stickiness *types.Stickiness if backend.LoadBalancer != nil { - if backend.LoadBalancer.Stickiness != nil { - stickiness = backend.LoadBalancer.Stickiness - } else if backend.LoadBalancer.Sticky { - if backend.LoadBalancer.Stickiness == nil { - stickiness = &types.Stickiness{} + if backend.LoadBalancer.Stickiness == nil { + if backend.LoadBalancer.Sticky { + stickiness = &types.Stickiness{ + CookieName: "_TRAEFIK_BACKEND", + } } + } else { + stickiness = backend.LoadBalancer.Stickiness } } backend.LoadBalancer = &types.LoadBalancer{ diff --git a/templates/consul_catalog.tmpl b/templates/consul_catalog.tmpl index b055a4aa4..93777c55c 100644 --- a/templates/consul_catalog.tmpl +++ b/templates/consul_catalog.tmpl @@ -21,7 +21,7 @@ sticky = {{getAttribute "backend.loadbalancer.sticky" .Attributes "false"}} {{if hasStickinessLabel .Attributes}} [Backends."backend-{{$service}}".LoadBalancer.Stickiness] - cookieName = {{getStickinessCookieName .Attributes}} + cookieName = "{{getStickinessCookieName .Attributes}}" {{end}} {{if hasMaxconnAttributes .Attributes}} diff --git a/templates/docker.tmpl b/templates/docker.tmpl index 6e18a9904..ef6f69526 100644 --- a/templates/docker.tmpl +++ b/templates/docker.tmpl @@ -10,7 +10,7 @@ method = "{{getLoadBalancerMethod $backend}}" {{if hasStickinessLabel $backend}} [Backends."{{$backendName}}".LoadBalancer.Stickiness] - cookieName = {{getStickinessCookieName $backend}} + cookieName = "{{getStickinessCookieName $backend}}" {{end}} {{end}} diff --git a/templates/ecs.tmpl b/templates/ecs.tmpl index a325f7539..f42642ad4 100644 --- a/templates/ecs.tmpl +++ b/templates/ecs.tmpl @@ -3,7 +3,7 @@ method = "{{ getLoadBalancerMethod $instances}}" {{if hasStickinessLabel $instances}} [Backends.backend-{{ $serviceName }}.LoadBalancer.Stickiness] - cookieName = {{getStickinessCookieName $instances}} + cookieName = "{{getStickinessCookieName $instances}}" {{end}} {{range $index, $i := $instances}} diff --git a/templates/kubernetes.tmpl b/templates/kubernetes.tmpl index 47fec081c..bab94ae19 100644 --- a/templates/kubernetes.tmpl +++ b/templates/kubernetes.tmpl @@ -8,7 +8,7 @@ method = "{{$backend.LoadBalancer.Method}}" {{if $backend.LoadBalancer.Stickiness}} [Backends."{{$backendName}}".LoadBalancer.Stickiness] - cookieName = {{$backend.LoadBalancer.Stickiness.CookieName}} + cookieName = "{{$backend.LoadBalancer.Stickiness.CookieName}}" {{end}} {{range $serverName, $server := $backend.Servers}} [backends."{{$backendName}}".servers."{{$serverName}}"] diff --git a/templates/marathon.tmpl b/templates/marathon.tmpl index e5c802a63..aaa7f2e6a 100644 --- a/templates/marathon.tmpl +++ b/templates/marathon.tmpl @@ -22,7 +22,7 @@ method = "{{getLoadBalancerMethod $app }}" {{if hasStickinessLabel $app}} [Backends."backend{{getBackend $app $serviceName }}".LoadBalancer.Stickiness] - cookieName = {{getStickinessCookieName $app}} + cookieName = "{{getStickinessCookieName $app}}" {{end}} {{end}} {{ if hasCircuitBreakerLabels $app }} diff --git a/templates/rancher.tmpl b/templates/rancher.tmpl index 90a10ac56..1f911e5b2 100644 --- a/templates/rancher.tmpl +++ b/templates/rancher.tmpl @@ -10,7 +10,7 @@ method = "{{getLoadBalancerMethod $backend}}" {{if hasStickinessLabel $backend}} [Backends."{{$backendName}}".LoadBalancer.Stickiness] - cookieName = {{getStickinessCookieName $backend}} + cookieName = "{{getStickinessCookieName $backend}}" {{end}} {{end}} From 8af39bdaf73d349fe674cd9cfc12f4b6e859a47a Mon Sep 17 00:00:00 2001 From: Sergey Kirillov Date: Fri, 13 Oct 2017 13:00:03 +0300 Subject: [PATCH 06/13] Changed Docker network filter to allow any swarm network --- provider/docker/docker.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provider/docker/docker.go b/provider/docker/docker.go index 2b1b8d61a..b1570e81c 100644 --- a/provider/docker/docker.go +++ b/provider/docker/docker.go @@ -825,7 +825,7 @@ func (p *Provider) listServices(ctx context.Context, dockerClient client.APIClie return []dockerData{}, err } networkListArgs := filters.NewArgs() - networkListArgs.Add("driver", "overlay") + networkListArgs.Add("scope", "swarm") networkList, err := dockerClient.NetworkList(ctx, dockertypes.NetworkListOptions{Filters: networkListArgs}) From 9598f646f516e8e650c51d48435154a92f0a91c9 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Fri, 13 Oct 2017 15:04:02 +0200 Subject: [PATCH 07/13] New entry point parser. --- configuration/configuration.go | 66 ++++++++++++------------- configuration/configuration_test.go | 74 +++++++++++++++++++---------- 2 files changed, 82 insertions(+), 58 deletions(-) diff --git a/configuration/configuration.go b/configuration/configuration.go index b112af7e6..7e489a008 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -5,7 +5,6 @@ import ( "fmt" "io/ioutil" "os" - "regexp" "strings" "time" @@ -193,55 +192,52 @@ func (ep *EntryPoints) String() string { // Set's argument is a string to be parsed to set the flag. // It's a comma-separated list, so we split it. func (ep *EntryPoints) Set(value string) error { - result, err := parseEntryPointsConfiguration(value) - if err != nil { - return err - } + result := parseEntryPointsConfiguration(value) var configTLS *TLS - if len(result["TLS"]) > 0 { + if len(result["tls"]) > 0 { certs := Certificates{} - if err := certs.Set(result["TLS"]); err != nil { + if err := certs.Set(result["tls"]); err != nil { return err } configTLS = &TLS{ Certificates: certs, } - } else if len(result["TLSACME"]) > 0 { + } else if len(result["tls_acme"]) > 0 { configTLS = &TLS{ Certificates: Certificates{}, } } - if len(result["CA"]) > 0 { - files := strings.Split(result["CA"], ",") + if len(result["ca"]) > 0 { + files := strings.Split(result["ca"], ",") configTLS.ClientCAFiles = files } var redirect *Redirect - if len(result["RedirectEntryPoint"]) > 0 || len(result["RedirectRegex"]) > 0 || len(result["RedirectReplacement"]) > 0 { + if len(result["redirect_entrypoint"]) > 0 || len(result["redirect_regex"]) > 0 || len(result["redirect_replacement"]) > 0 { redirect = &Redirect{ - EntryPoint: result["RedirectEntryPoint"], - Regex: result["RedirectRegex"], - Replacement: result["RedirectReplacement"], + EntryPoint: result["redirect_entrypoint"], + Regex: result["redirect_regex"], + Replacement: result["redirect_replacement"], } } whiteListSourceRange := []string{} - if len(result["WhiteListSourceRange"]) > 0 { - whiteListSourceRange = strings.Split(result["WhiteListSourceRange"], ",") + if len(result["whitelistsourcerange"]) > 0 { + whiteListSourceRange = strings.Split(result["whitelistsourcerange"], ",") } - compress := toBool(result, "Compress") + compress := toBool(result, "compress") var proxyProtocol *ProxyProtocol - if len(result["ProxyProtocol"]) > 0 { - trustedIPs := strings.Split(result["ProxyProtocol"], ",") + if len(result["proxyprotocol_trustedips"]) > 0 { + trustedIPs := strings.Split(result["proxyprotocol_trustedips"], ",") proxyProtocol = &ProxyProtocol{ TrustedIPs: trustedIPs, } } - (*ep)[result["Name"]] = &EntryPoint{ - Address: result["Address"], + (*ep)[result["name"]] = &EntryPoint{ + Address: result["address"], TLS: configTLS, Redirect: redirect, Compress: compress, @@ -252,20 +248,24 @@ func (ep *EntryPoints) Set(value string) error { return nil } -func parseEntryPointsConfiguration(value string) (map[string]string, error) { - regex := regexp.MustCompile(`(?:Name:(?P\S*))\s*(?:Address:(?P
\S*))?\s*(?:TLS:(?P\S*))?\s*(?PTLS)?\s*(?:CA:(?P\S*))?\s*(?:Redirect\.EntryPoint:(?P\S*))?\s*(?:Redirect\.Regex:(?P\S*))?\s*(?:Redirect\.Replacement:(?P\S*))?\s*(?:Compress:(?P\S*))?\s*(?:WhiteListSourceRange:(?P\S*))?\s*(?:ProxyProtocol\.TrustedIPs:(?P\S*))?`) - match := regex.FindAllStringSubmatch(value, -1) - if match == nil { - return nil, fmt.Errorf("bad EntryPoints format: %s", value) - } - matchResult := match[0] - result := make(map[string]string) - for i, name := range regex.SubexpNames() { - if i != 0 && len(matchResult[i]) != 0 { - result[name] = matchResult[i] +func parseEntryPointsConfiguration(raw string) map[string]string { + sections := strings.Fields(raw) + + config := make(map[string]string) + for _, part := range sections { + field := strings.SplitN(part, ":", 2) + name := strings.ToLower(strings.Replace(field[0], ".", "_", -1)) + if len(field) > 1 { + config[name] = field[1] + } else { + if strings.EqualFold(name, "TLS") { + config["tls_acme"] = "TLS" + } else { + config[name] = "" + } } } - return result, nil + return config } func toBool(conf map[string]string, key string) bool { diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index 591e32d91..5e7ac0ad7 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -16,36 +16,36 @@ func Test_parseEntryPointsConfiguration(t *testing.T) { }{ { name: "all parameters", - value: "Name:foo Address:bar TLS:goo TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:WhiteListSourceRange ProxyProtocol.TrustedIPs:192.168.0.1", + value: "Name:foo TLS:goo TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:WhiteListSourceRange ProxyProtocol.TrustedIPs:192.168.0.1 Address::8000", expectedResult: map[string]string{ - "Name": "foo", - "Address": "bar", - "CA": "car", - "TLS": "goo", - "TLSACME": "TLS", - "RedirectEntryPoint": "RedirectEntryPoint", - "RedirectRegex": "RedirectRegex", - "RedirectReplacement": "RedirectReplacement", - "WhiteListSourceRange": "WhiteListSourceRange", - "ProxyProtocol": "192.168.0.1", - "Compress": "true", + "name": "foo", + "address": ":8000", + "ca": "car", + "tls": "goo", + "tls_acme": "TLS", + "redirect_entrypoint": "RedirectEntryPoint", + "redirect_regex": "RedirectRegex", + "redirect_replacement": "RedirectReplacement", + "whitelistsourcerange": "WhiteListSourceRange", + "proxyprotocol_trustedips": "192.168.0.1", + "compress": "true", }, }, { name: "compress on", - value: "Name:foo Compress:on", + value: "name:foo Compress:on", expectedResult: map[string]string{ - "Name": "foo", - "Compress": "on", + "name": "foo", + "compress": "on", }, }, { name: "TLS", value: "Name:foo TLS:goo TLS", expectedResult: map[string]string{ - "Name": "foo", - "TLS": "goo", - "TLSACME": "TLS", + "name": "foo", + "tls": "goo", + "tls_acme": "TLS", }, }, } @@ -55,10 +55,7 @@ func Test_parseEntryPointsConfiguration(t *testing.T) { t.Run(test.name, func(t *testing.T) { t.Parallel() - conf, err := parseEntryPointsConfiguration(test.value) - if err != nil { - t.Error(err) - } + conf := parseEntryPointsConfiguration(test.value) for key, value := range conf { fmt.Println(key, value) @@ -133,11 +130,38 @@ func TestEntryPoints_Set(t *testing.T) { expectedEntryPoint *EntryPoint }{ { - name: "all parameters", - expression: "Name:foo Address:bar TLS:goo,gii TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:Range ProxyProtocol.TrustedIPs:192.168.0.1", + name: "all parameters camelcase", + expression: "Name:foo Address::8000 TLS:goo,gii TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:Range ProxyProtocol.TrustedIPs:192.168.0.1", expectedEntryPointName: "foo", expectedEntryPoint: &EntryPoint{ - Address: "bar", + Address: ":8000", + Redirect: &Redirect{ + EntryPoint: "RedirectEntryPoint", + Regex: "RedirectRegex", + Replacement: "RedirectReplacement", + }, + Compress: true, + ProxyProtocol: &ProxyProtocol{ + TrustedIPs: []string{"192.168.0.1"}, + }, + WhitelistSourceRange: []string{"Range"}, + TLS: &TLS{ + ClientCAFiles: []string{"car"}, + Certificates: Certificates{ + { + CertFile: FileOrContent("goo"), + KeyFile: FileOrContent("gii"), + }, + }, + }, + }, + }, + { + name: "all parameters lowercase", + expression: "name:foo address::8000 tls:goo,gii tls ca:car redirect.entryPoint:RedirectEntryPoint redirect.regex:RedirectRegex redirect.replacement:RedirectReplacement compress:true whiteListSourceRange:Range proxyProtocol.TrustedIPs:192.168.0.1", + expectedEntryPointName: "foo", + expectedEntryPoint: &EntryPoint{ + Address: ":8000", Redirect: &Redirect{ EntryPoint: "RedirectEntryPoint", Regex: "RedirectRegex", From aa308b7a3a25673f2c30a421ebba3178396da070 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 16 Oct 2017 12:46:03 +0200 Subject: [PATCH 08/13] Add TrustForwardHeader options. --- cmd/traefik/traefik.go | 13 ++++- configuration/configuration.go | 40 +++++++++++-- configuration/configuration_test.go | 90 ++++++++++++++++++++++++++--- docs/configuration/entrypoints.md | 41 +++++++++++-- middlewares/ip_whitelister.go | 2 +- server/header_rewriter.go | 51 ++++++++++++++++ server/server.go | 10 +++- server/server_test.go | 19 ++++-- whitelist/ip.go | 33 +++++++---- whitelist/ip_test.go | 6 +- 10 files changed, 265 insertions(+), 40 deletions(-) create mode 100644 server/header_rewriter.go diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index d9c703e8d..969a03ee9 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -229,7 +229,10 @@ func run(globalConfiguration *configuration.GlobalConfiguration) { http.DefaultTransport.(*http.Transport).Proxy = http.ProxyFromEnvironment if len(globalConfiguration.EntryPoints) == 0 { - globalConfiguration.EntryPoints = map[string]*configuration.EntryPoint{"http": {Address: ":80"}} + globalConfiguration.EntryPoints = map[string]*configuration.EntryPoint{"http": { + Address: ":80", + ForwardedHeaders: &configuration.ForwardedHeaders{Insecure: true}, + }} globalConfiguration.DefaultEntryPoints = []string{"http"} } @@ -259,6 +262,14 @@ func run(globalConfiguration *configuration.GlobalConfiguration) { globalConfiguration.LogLevel = "DEBUG" } + // ForwardedHeaders must be remove in the next breaking version + for entryPointName := range globalConfiguration.EntryPoints { + entryPoint := globalConfiguration.EntryPoints[entryPointName] + if entryPoint.ForwardedHeaders == nil { + entryPoint.ForwardedHeaders = &configuration.ForwardedHeaders{Insecure: true} + } + } + // logging level, err := logrus.ParseLevel(strings.ToLower(globalConfiguration.LogLevel)) if err != nil { diff --git a/configuration/configuration.go b/configuration/configuration.go index 7e489a008..6d7732dbe 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -10,6 +10,7 @@ import ( "github.com/containous/flaeg" "github.com/containous/traefik/acme" + "github.com/containous/traefik/log" "github.com/containous/traefik/provider/boltdb" "github.com/containous/traefik/provider/consul" "github.com/containous/traefik/provider/docker" @@ -229,11 +230,31 @@ func (ep *EntryPoints) Set(value string) error { compress := toBool(result, "compress") var proxyProtocol *ProxyProtocol - if len(result["proxyprotocol_trustedips"]) > 0 { - trustedIPs := strings.Split(result["proxyprotocol_trustedips"], ",") + ppTrustedIPs := result["proxyprotocol_trustedips"] + if len(result["proxyprotocol_insecure"]) > 0 || len(ppTrustedIPs) > 0 { proxyProtocol = &ProxyProtocol{ - TrustedIPs: trustedIPs, + Insecure: toBool(result, "proxyprotocol_insecure"), } + if len(ppTrustedIPs) > 0 { + proxyProtocol.TrustedIPs = strings.Split(ppTrustedIPs, ",") + } + } + + // TODO must be changed to false by default in the next breaking version. + forwardedHeaders := &ForwardedHeaders{Insecure: true} + if _, ok := result["forwardedheaders_insecure"]; ok { + forwardedHeaders.Insecure = toBool(result, "forwardedheaders_insecure") + } + + fhTrustedIPs := result["forwardedheaders_trustedips"] + if len(fhTrustedIPs) > 0 { + // TODO must be removed in the next breaking version. + forwardedHeaders.Insecure = toBool(result, "forwardedheaders_insecure") + forwardedHeaders.TrustedIPs = strings.Split(fhTrustedIPs, ",") + } + + if proxyProtocol != nil && proxyProtocol.Insecure { + log.Warn("ProxyProtocol.Insecure:true is dangerous. Please use 'ProxyProtocol.TrustedIPs:IPs' and remove 'ProxyProtocol.Insecure:true'") } (*ep)[result["name"]] = &EntryPoint{ @@ -243,6 +264,7 @@ func (ep *EntryPoints) Set(value string) error { Compress: compress, WhitelistSourceRange: whiteListSourceRange, ProxyProtocol: proxyProtocol, + ForwardedHeaders: forwardedHeaders, } return nil @@ -300,8 +322,9 @@ type EntryPoint struct { Redirect *Redirect `export:"true"` Auth *types.Auth `export:"true"` WhitelistSourceRange []string - Compress bool `export:"true"` - ProxyProtocol *ProxyProtocol `export:"true"` + Compress bool `export:"true"` + ProxyProtocol *ProxyProtocol `export:"true"` + ForwardedHeaders *ForwardedHeaders `export:"true"` } // Redirect configures a redirection of an entry point to another, or to an URL @@ -453,5 +476,12 @@ type ForwardingTimeouts struct { // ProxyProtocol contains Proxy-Protocol configuration type ProxyProtocol struct { + Insecure bool + TrustedIPs []string +} + +// ForwardedHeaders Trust client forwarding headers +type ForwardedHeaders struct { + Insecure bool TrustedIPs []string } diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index 5e7ac0ad7..432685855 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -1,7 +1,6 @@ package configuration import ( - "fmt" "testing" "github.com/stretchr/testify/assert" @@ -16,7 +15,7 @@ func Test_parseEntryPointsConfiguration(t *testing.T) { }{ { name: "all parameters", - value: "Name:foo TLS:goo TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:WhiteListSourceRange ProxyProtocol.TrustedIPs:192.168.0.1 Address::8000", + value: "Name:foo TLS:goo TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:WhiteListSourceRange ProxyProtocol.TrustedIPs:192.168.0.1 ProxyProtocol.Insecure:false Address::8000", expectedResult: map[string]string{ "name": "foo", "address": ":8000", @@ -28,6 +27,7 @@ func Test_parseEntryPointsConfiguration(t *testing.T) { "redirect_replacement": "RedirectReplacement", "whitelistsourcerange": "WhiteListSourceRange", "proxyprotocol_trustedips": "192.168.0.1", + "proxyprotocol_insecure": "false", "compress": "true", }, }, @@ -57,10 +57,6 @@ func Test_parseEntryPointsConfiguration(t *testing.T) { conf := parseEntryPointsConfiguration(test.value) - for key, value := range conf { - fmt.Println(key, value) - } - assert.Len(t, conf, len(test.expectedResult)) assert.Equal(t, test.expectedResult, conf) }) @@ -131,7 +127,7 @@ func TestEntryPoints_Set(t *testing.T) { }{ { name: "all parameters camelcase", - expression: "Name:foo Address::8000 TLS:goo,gii TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:Range ProxyProtocol.TrustedIPs:192.168.0.1", + expression: "Name:foo Address::8000 TLS:goo,gii TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:Range ProxyProtocol.TrustedIPs:192.168.0.1 ForwardedHeaders.TrustedIPs:10.0.0.3/24,20.0.0.3/24", expectedEntryPointName: "foo", expectedEntryPoint: &EntryPoint{ Address: ":8000", @@ -144,6 +140,9 @@ func TestEntryPoints_Set(t *testing.T) { ProxyProtocol: &ProxyProtocol{ TrustedIPs: []string{"192.168.0.1"}, }, + ForwardedHeaders: &ForwardedHeaders{ + TrustedIPs: []string{"10.0.0.3/24", "20.0.0.3/24"}, + }, WhitelistSourceRange: []string{"Range"}, TLS: &TLS{ ClientCAFiles: []string{"car"}, @@ -158,7 +157,7 @@ func TestEntryPoints_Set(t *testing.T) { }, { name: "all parameters lowercase", - expression: "name:foo address::8000 tls:goo,gii tls ca:car redirect.entryPoint:RedirectEntryPoint redirect.regex:RedirectRegex redirect.replacement:RedirectReplacement compress:true whiteListSourceRange:Range proxyProtocol.TrustedIPs:192.168.0.1", + expression: "name:foo address::8000 tls:goo,gii tls ca:car redirect.entryPoint:RedirectEntryPoint redirect.regex:RedirectRegex redirect.replacement:RedirectReplacement compress:true whiteListSourceRange:Range proxyProtocol.trustedIPs:192.168.0.1 forwardedHeaders.trustedIPs:10.0.0.3/24,20.0.0.3/24", expectedEntryPointName: "foo", expectedEntryPoint: &EntryPoint{ Address: ":8000", @@ -171,6 +170,9 @@ func TestEntryPoints_Set(t *testing.T) { ProxyProtocol: &ProxyProtocol{ TrustedIPs: []string{"192.168.0.1"}, }, + ForwardedHeaders: &ForwardedHeaders{ + TrustedIPs: []string{"10.0.0.3/24", "20.0.0.3/24"}, + }, WhitelistSourceRange: []string{"Range"}, TLS: &TLS{ ClientCAFiles: []string{"car"}, @@ -183,6 +185,76 @@ func TestEntryPoints_Set(t *testing.T) { }, }, }, + { + name: "default", + expression: "Name:foo", + expectedEntryPointName: "foo", + expectedEntryPoint: &EntryPoint{ + WhitelistSourceRange: []string{}, + ForwardedHeaders: &ForwardedHeaders{Insecure: true}, + }, + }, + { + name: "ForwardedHeaders insecure true", + expression: "Name:foo ForwardedHeaders.Insecure:true", + expectedEntryPointName: "foo", + expectedEntryPoint: &EntryPoint{ + WhitelistSourceRange: []string{}, + ForwardedHeaders: &ForwardedHeaders{Insecure: true}, + }, + }, + { + name: "ForwardedHeaders insecure false", + expression: "Name:foo ForwardedHeaders.Insecure:false", + expectedEntryPointName: "foo", + expectedEntryPoint: &EntryPoint{ + WhitelistSourceRange: []string{}, + ForwardedHeaders: &ForwardedHeaders{Insecure: false}, + }, + }, + { + name: "ForwardedHeaders TrustedIPs", + expression: "Name:foo ForwardedHeaders.TrustedIPs:10.0.0.3/24,20.0.0.3/24", + expectedEntryPointName: "foo", + expectedEntryPoint: &EntryPoint{ + WhitelistSourceRange: []string{}, + ForwardedHeaders: &ForwardedHeaders{ + TrustedIPs: []string{"10.0.0.3/24", "20.0.0.3/24"}, + }, + }, + }, + { + name: "ProxyProtocol insecure true", + expression: "Name:foo ProxyProtocol.Insecure:true", + expectedEntryPointName: "foo", + expectedEntryPoint: &EntryPoint{ + WhitelistSourceRange: []string{}, + ForwardedHeaders: &ForwardedHeaders{Insecure: true}, + ProxyProtocol: &ProxyProtocol{Insecure: true}, + }, + }, + { + name: "ProxyProtocol insecure false", + expression: "Name:foo ProxyProtocol.Insecure:false", + expectedEntryPointName: "foo", + expectedEntryPoint: &EntryPoint{ + WhitelistSourceRange: []string{}, + ForwardedHeaders: &ForwardedHeaders{Insecure: true}, + ProxyProtocol: &ProxyProtocol{}, + }, + }, + { + name: "ProxyProtocol TrustedIPs", + expression: "Name:foo ProxyProtocol.TrustedIPs:10.0.0.3/24,20.0.0.3/24", + expectedEntryPointName: "foo", + expectedEntryPoint: &EntryPoint{ + WhitelistSourceRange: []string{}, + ForwardedHeaders: &ForwardedHeaders{Insecure: true}, + ProxyProtocol: &ProxyProtocol{ + TrustedIPs: []string{"10.0.0.3/24", "20.0.0.3/24"}, + }, + }, + }, { name: "compress on", expression: "Name:foo Compress:on", @@ -190,6 +262,7 @@ func TestEntryPoints_Set(t *testing.T) { expectedEntryPoint: &EntryPoint{ Compress: true, WhitelistSourceRange: []string{}, + ForwardedHeaders: &ForwardedHeaders{Insecure: true}, }, }, { @@ -199,6 +272,7 @@ func TestEntryPoints_Set(t *testing.T) { expectedEntryPoint: &EntryPoint{ Compress: true, WhitelistSourceRange: []string{}, + ForwardedHeaders: &ForwardedHeaders{Insecure: true}, }, }, } diff --git a/docs/configuration/entrypoints.md b/docs/configuration/entrypoints.md index c626352e7..7bb55c8f3 100644 --- a/docs/configuration/entrypoints.md +++ b/docs/configuration/entrypoints.md @@ -191,7 +191,7 @@ To enable IP whitelisting at the entrypoint level. ## ProxyProtocol To enable [ProxyProtocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) support. -Only IPs in `trustedIPs` will lead to remote client address replacement: you should declare your load-balancer IP or CIDR range here (in testing environment, you can trust everyone using `0.0.0.0/0`). +Only IPs in `trustedIPs` will lead to remote client address replacement: you should declare your load-balancer IP or CIDR range here (in testing environment, you can trust everyone using `insecure = true`). !!! danger When queuing Træfik behind another load-balancer, be sure to carefully configure Proxy Protocol on both sides. @@ -200,7 +200,40 @@ Only IPs in `trustedIPs` will lead to remote client address replacement: you sho ```toml [entryPoints] [entryPoints.http] - address = ":80" - [entryPoints.http.proxyProtocol] - trustedIPs = ["127.0.0.1/32", "192.168.1.7"] + address = ":80" + + # Enable ProxyProtocol + [entryPoints.http.proxyProtocol] + # List of trusted IPs + # + # Required + # Default: [] + # + trustedIPs = ["127.0.0.1/32", "192.168.1.7"] + + # Insecure mode FOR TESTING ENVIRONNEMENT ONLY + # + # Optional + # Default: false + # + # insecure = true +``` + +## Forwarded Header + +Only IPs in `trustedIPs` will be authorize to trust the client forwarded headers (`X-Forwarded-*`). + +```toml +[entryPoints] + [entryPoints.http] + address = ":80" + + # Enable Forwarded Headers + [entryPoints.http.forwardedHeaders] + # List of trusted IPs + # + # Required + # Default: [] + # + trustedIPs = ["127.0.0.1/32", "192.168.1.7"] ``` diff --git a/middlewares/ip_whitelister.go b/middlewares/ip_whitelister.go index 490986c29..29eb76063 100644 --- a/middlewares/ip_whitelister.go +++ b/middlewares/ip_whitelister.go @@ -26,7 +26,7 @@ func NewIPWhitelister(whitelistStrings []string) (*IPWhiteLister, error) { whiteLister := IPWhiteLister{} - ip, err := whitelist.NewIP(whitelistStrings) + ip, err := whitelist.NewIP(whitelistStrings, false) if err != nil { return nil, fmt.Errorf("parsing CIDR whitelist %s: %v", whitelistStrings, err) } diff --git a/server/header_rewriter.go b/server/header_rewriter.go new file mode 100644 index 000000000..aebd9fcb7 --- /dev/null +++ b/server/header_rewriter.go @@ -0,0 +1,51 @@ +package server + +import ( + "net" + "net/http" + "os" + + "github.com/containous/traefik/whitelist" + "github.com/vulcand/oxy/forward" +) + +// NewHeaderRewriter Create a header rewriter +func NewHeaderRewriter(trustedIPs []string, insecure bool) (forward.ReqRewriter, error) { + IPs, err := whitelist.NewIP(trustedIPs, insecure) + if err != nil { + return nil, err + } + + h, err := os.Hostname() + if err != nil { + h = "localhost" + } + + return &headerRewriter{ + secureRewriter: &forward.HeaderRewriter{TrustForwardHeader: true, Hostname: h}, + insecureRewriter: &forward.HeaderRewriter{TrustForwardHeader: false, Hostname: h}, + ips: IPs, + insecure: insecure, + }, nil +} + +type headerRewriter struct { + secureRewriter forward.ReqRewriter + insecureRewriter forward.ReqRewriter + insecure bool + ips *whitelist.IP +} + +func (h *headerRewriter) Rewrite(req *http.Request) { + clientIP, _, err := net.SplitHostPort(req.RemoteAddr) + if err != nil { + h.secureRewriter.Rewrite(req) + } + + authorized, _, err := h.ips.Contains(clientIP) + if h.insecure || authorized { + h.secureRewriter.Rewrite(req) + } else { + h.insecureRewriter.Rewrite(req) + } +} diff --git a/server/server.go b/server/server.go index 3a5a76cb9..cfcd91411 100644 --- a/server/server.go +++ b/server/server.go @@ -655,7 +655,7 @@ func (server *Server) prepareServer(entryPointName string, entryPoint *configura } if entryPoint.ProxyProtocol != nil { - IPs, err := whitelist.NewIP(entryPoint.ProxyProtocol.TrustedIPs) + IPs, err := whitelist.NewIP(entryPoint.ProxyProtocol.TrustedIPs, entryPoint.ProxyProtocol.Insecure) if err != nil { return nil, nil, fmt.Errorf("Error creating whitelist: %s", err) } @@ -806,11 +806,19 @@ func (server *Server) loadConfig(configurations types.Configurations, globalConf continue frontend } + rewriter, err := NewHeaderRewriter(entryPoint.ForwardedHeaders.TrustedIPs, entryPoint.ForwardedHeaders.Insecure) + if err != nil { + log.Errorf("Error creating rewriter for frontend %s: %v", frontendName, err) + log.Errorf("Skipping frontend %s...", frontendName) + continue frontend + } + fwd, err := forward.New( forward.Logger(oxyLogger), forward.PassHostHeader(frontend.PassHostHeader), forward.RoundTripper(roundTripper), forward.ErrorHandler(errorHandler), + forward.Rewriter(rewriter), ) if err != nil { diff --git a/server/server_test.go b/server/server_test.go index a5ec9b091..f661a44a3 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -96,7 +96,10 @@ func TestPrepareServerTimeouts(t *testing.T) { t.Parallel() entryPointName := "http" - entryPoint := &configuration.EntryPoint{Address: "localhost:0"} + entryPoint := &configuration.EntryPoint{ + Address: "localhost:0", + ForwardedHeaders: &configuration.ForwardedHeaders{Insecure: true}, + } router := middlewares.NewHandlerSwitcher(mux.NewRouter()) srv := NewServer(test.globalConfig) @@ -210,7 +213,9 @@ func TestServerLoadConfigHealthCheckOptions(t *testing.T) { t.Run(fmt.Sprintf("%s/hc=%t", lbMethod, healthCheck != nil), func(t *testing.T) { globalConfig := configuration.GlobalConfiguration{ EntryPoints: configuration.EntryPoints{ - "http": &configuration.EntryPoint{}, + "http": &configuration.EntryPoint{ + ForwardedHeaders: &configuration.ForwardedHeaders{Insecure: true}, + }, }, HealthCheck: &configuration.HealthCheckConfig{Interval: flaeg.Duration(5 * time.Second)}, } @@ -383,7 +388,7 @@ func TestNewServerWithWhitelistSourceRange(t *testing.T) { func TestServerLoadConfigEmptyBasicAuth(t *testing.T) { globalConfig := configuration.GlobalConfiguration{ EntryPoints: configuration.EntryPoints{ - "http": &configuration.EntryPoint{}, + "http": &configuration.EntryPoint{ForwardedHeaders: &configuration.ForwardedHeaders{Insecure: true}}, }, } @@ -492,7 +497,7 @@ func TestConfigureBackends(t *testing.T) { } } -func TestServerEntrypointWhitelistConfig(t *testing.T) { +func TestServerEntryPointWhitelistConfig(t *testing.T) { tests := []struct { desc string entrypoint *configuration.EntryPoint @@ -501,7 +506,8 @@ func TestServerEntrypointWhitelistConfig(t *testing.T) { { desc: "no whitelist middleware if no config on entrypoint", entrypoint: &configuration.EntryPoint{ - Address: ":0", + Address: ":0", + ForwardedHeaders: &configuration.ForwardedHeaders{Insecure: true}, }, wantMiddleware: false, }, @@ -512,6 +518,7 @@ func TestServerEntrypointWhitelistConfig(t *testing.T) { WhitelistSourceRange: []string{ "127.0.0.1/32", }, + ForwardedHeaders: &configuration.ForwardedHeaders{Insecure: true}, }, wantMiddleware: true, }, @@ -633,7 +640,7 @@ func TestServerResponseEmptyBackend(t *testing.T) { globalConfig := configuration.GlobalConfiguration{ EntryPoints: configuration.EntryPoints{ - "http": &configuration.EntryPoint{}, + "http": &configuration.EntryPoint{ForwardedHeaders: &configuration.ForwardedHeaders{Insecure: true}}, }, } dynamicConfigs := types.Configurations{"config": test.dynamicConfig(testServer.URL)} diff --git a/whitelist/ip.go b/whitelist/ip.go index af3b691e7..322404fab 100644 --- a/whitelist/ip.go +++ b/whitelist/ip.go @@ -11,26 +11,29 @@ import ( type IP struct { whiteListsIPs []*net.IP whiteListsNet []*net.IPNet + insecure bool } // NewIP builds a new IP given a list of CIDR-Strings to whitelist -func NewIP(whitelistStrings []string) (*IP, error) { - if len(whitelistStrings) == 0 { +func NewIP(whitelistStrings []string, insecure bool) (*IP, error) { + if len(whitelistStrings) == 0 && !insecure { return nil, errors.New("no whiteListsNet provided") } ip := IP{} - for _, whitelistString := range whitelistStrings { - ipAddr := net.ParseIP(whitelistString) - if ipAddr != nil { - ip.whiteListsIPs = append(ip.whiteListsIPs, &ipAddr) - } else { - _, whitelist, err := net.ParseCIDR(whitelistString) - if err != nil { - return nil, fmt.Errorf("parsing CIDR whitelist %s: %v", whitelist, err) + if !insecure { + for _, whitelistString := range whitelistStrings { + ipAddr := net.ParseIP(whitelistString) + if ipAddr != nil { + ip.whiteListsIPs = append(ip.whiteListsIPs, &ipAddr) + } else { + _, whitelist, err := net.ParseCIDR(whitelistString) + if err != nil { + return nil, fmt.Errorf("parsing CIDR whitelist %s: %v", whitelist, err) + } + ip.whiteListsNet = append(ip.whiteListsNet, whitelist) } - ip.whiteListsNet = append(ip.whiteListsNet, whitelist) } } @@ -39,6 +42,10 @@ func NewIP(whitelistStrings []string) (*IP, error) { // Contains checks if provided address is in the white list func (ip *IP) Contains(addr string) (bool, net.IP, error) { + if ip.insecure { + return true, nil, nil + } + ipAddr, err := ipFromRemoteAddr(addr) if err != nil { return false, nil, fmt.Errorf("unable to parse address: %s: %s", addr, err) @@ -50,6 +57,10 @@ func (ip *IP) Contains(addr string) (bool, net.IP, error) { // ContainsIP checks if provided address is in the white list func (ip *IP) ContainsIP(addr net.IP) (bool, error) { + if ip.insecure { + return true, nil + } + for _, whiteListIP := range ip.whiteListsIPs { if whiteListIP.Equal(addr) { return true, nil diff --git a/whitelist/ip_test.go b/whitelist/ip_test.go index 46ab7142f..abd65f297 100644 --- a/whitelist/ip_test.go +++ b/whitelist/ip_test.go @@ -75,7 +75,7 @@ func TestNew(t *testing.T) { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() - whitelister, err := NewIP(test.whitelistStrings) + whitelister, err := NewIP(test.whitelistStrings, false) if test.errMessage != "" { require.EqualError(t, err, test.errMessage) } else { @@ -275,7 +275,7 @@ func TestIsAllowed(t *testing.T) { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() - whiteLister, err := NewIP(test.whitelistStrings) + whiteLister, err := NewIP(test.whitelistStrings, false) require.NoError(t, err) require.NotNil(t, whiteLister) @@ -306,7 +306,7 @@ func TestBrokenIPs(t *testing.T) { "\\&$§&/(", } - whiteLister, err := NewIP([]string{"1.2.3.4/24"}) + whiteLister, err := NewIP([]string{"1.2.3.4/24"}, false) require.NoError(t, err) for _, testIP := range brokenIPs { From 3afd6024b5b11b796f968769d6c252f3455ca1b8 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 16 Oct 2017 16:58:03 +0200 Subject: [PATCH 09/13] Fix consul catalog retry --- integration/consul_catalog_test.go | 76 ++++++++++++++++++++++++++---- provider/consul/consul_catalog.go | 15 ++++-- 2 files changed, 78 insertions(+), 13 deletions(-) diff --git a/integration/consul_catalog_test.go b/integration/consul_catalog_test.go index 1ec14f6a1..fdd132122 100644 --- a/integration/consul_catalog_test.go +++ b/integration/consul_catalog_test.go @@ -24,19 +24,20 @@ func (s *ConsulCatalogSuite) SetUpSuite(c *check.C) { s.composeProject.Start(c) consul := s.composeProject.Container(c, "consul") - s.consulIP = consul.NetworkSettings.IPAddress config := api.DefaultConfig() config.Address = s.consulIP + ":8500" - consulClient, err := api.NewClient(config) - if err != nil { - c.Fatalf("Error creating consul client. %v", err) - } - s.consulClient = consulClient + s.createConsulClient(config, c) // Wait for consul to elect itself leader - err = try.Do(3*time.Second, func() error { - leader, err := consulClient.Status().Leader() + err := s.waitToElectConsulLeader() + c.Assert(err, checker.IsNil) + +} + +func (s *ConsulCatalogSuite) waitToElectConsulLeader() error { + return try.Do(3*time.Second, func() error { + leader, err := s.consulClient.Status().Leader() if err != nil || len(leader) == 0 { return fmt.Errorf("Leader not found. %v", err) @@ -44,7 +45,17 @@ func (s *ConsulCatalogSuite) SetUpSuite(c *check.C) { return nil }) - c.Assert(err, checker.IsNil) +} +func (s *ConsulCatalogSuite) createConsulClient(config *api.Config, c *check.C) *api.Client { + consulClient, err := api.NewClient(config) + if err != nil { + c.Fatalf("Error creating consul client. %v", err) + } + s.consulClient = consulClient + return consulClient +} +func (s *ConsulCatalogSuite) startConsulService(c *check.C) { + } func (s *ConsulCatalogSuite) registerService(name string, address string, port int, tags []string) error { @@ -332,3 +343,50 @@ func (s *ConsulCatalogSuite) TestBasicAuthSimpleService(c *check.C) { err = try.Request(req, 5*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) c.Assert(err, checker.IsNil) } + +func (s *ConsulCatalogSuite) TestRetryWithConsulServer(c *check.C) { + + //Scale consul to 0 to be able to start traefik before and test retry + s.composeProject.Scale(c, "consul", 0) + + cmd, display := s.traefikCmd( + withConfigFile("fixtures/consul_catalog/simple.toml"), + "--consulCatalog", + "--consulCatalog.watch=false", + "--consulCatalog.exposedByDefault=true", + "--consulCatalog.endpoint="+s.consulIP+":8500", + "--consulCatalog.domain=consul.localhost") + defer display(c) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + // Wait for Traefik to turn ready. + err = try.GetRequest("http://127.0.0.1:8000/", 2*time.Second, try.StatusCodeIs(http.StatusNotFound)) + c.Assert(err, checker.IsNil) + + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) + req.Host = "test.consul.localhost" + + // Request should fail + err = try.Request(req, 2*time.Second, try.StatusCodeIs(http.StatusNotFound), try.HasBody()) + c.Assert(err, checker.IsNil) + + // Scale consul to 1 + s.composeProject.Scale(c, "consul", 1) + s.waitToElectConsulLeader() + + nginx := s.composeProject.Container(c, "nginx1") + // Register service + err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{}) + c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) + + // Provider consul catalog should be present + err = try.GetRequest("http://127.0.0.1:8080/api/providers", 10*time.Second, try.BodyContains("consul_catalog")) + c.Assert(err, checker.IsNil) + + // Should be ok + err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) + c.Assert(err, checker.IsNil) +} diff --git a/provider/consul/consul_catalog.go b/provider/consul/consul_catalog.go index 02230a621..868bcd6b4 100644 --- a/provider/consul/consul_catalog.go +++ b/provider/consul/consul_catalog.go @@ -110,7 +110,7 @@ func getChangedHealthyKeys(currState []string, prevState []string) ([]string, [] return fun.Keys(addedKeys).([]string), fun.Keys(removedKeys).([]string) } -func (p *CatalogProvider) watchHealthState(stopCh <-chan struct{}, watchCh chan<- map[string][]string) { +func (p *CatalogProvider) watchHealthState(stopCh <-chan struct{}, watchCh chan<- map[string][]string, errorCh chan<- error) { health := p.client.Health() catalog := p.client.Catalog() @@ -131,6 +131,7 @@ func (p *CatalogProvider) watchHealthState(stopCh <-chan struct{}, watchCh chan< healthyState, meta, err := health.State("passing", options) if err != nil { log.WithError(err).Error("Failed to retrieve health checks") + errorCh <- err return } @@ -154,6 +155,7 @@ func (p *CatalogProvider) watchHealthState(stopCh <-chan struct{}, watchCh chan< data, _, err := catalog.Services(&api.QueryOptions{}) if err != nil { log.Errorf("Failed to list services: %s", err) + errorCh <- err return } @@ -186,7 +188,7 @@ type Service struct { Nodes []string } -func (p *CatalogProvider) watchCatalogServices(stopCh <-chan struct{}, watchCh chan<- map[string][]string) { +func (p *CatalogProvider) watchCatalogServices(stopCh <-chan struct{}, watchCh chan<- map[string][]string, errorCh chan<- error) { catalog := p.client.Catalog() safe.Go(func() { @@ -205,6 +207,7 @@ func (p *CatalogProvider) watchCatalogServices(stopCh <-chan struct{}, watchCh c data, meta, err := catalog.Services(options) if err != nil { log.Errorf("Failed to list services: %s", err) + errorCh <- err return } @@ -220,6 +223,7 @@ func (p *CatalogProvider) watchCatalogServices(stopCh <-chan struct{}, watchCh c nodes, _, err := catalog.Service(key, "", &api.QueryOptions{}) if err != nil { log.Errorf("Failed to get detail of service %s: %s", key, err) + errorCh <- err return } nodesID := getServiceIds(nodes) @@ -531,9 +535,10 @@ func (p *CatalogProvider) getNodes(index map[string][]string) ([]catalogUpdate, func (p *CatalogProvider) watch(configurationChan chan<- types.ConfigMessage, stop chan bool) error { stopCh := make(chan struct{}) watchCh := make(chan map[string][]string) + errorCh := make(chan error) - p.watchHealthState(stopCh, watchCh) - p.watchCatalogServices(stopCh, watchCh) + p.watchHealthState(stopCh, watchCh, errorCh) + p.watchCatalogServices(stopCh, watchCh, errorCh) defer close(stopCh) defer close(watchCh) @@ -556,6 +561,8 @@ func (p *CatalogProvider) watch(configurationChan chan<- types.ConfigMessage, st ProviderName: "consul_catalog", Configuration: configuration, } + case err := <-errorCh: + return err } } } From 08503655d9da2a2d56b0134dbd0c5952800f3929 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 16 Oct 2017 17:38:03 +0200 Subject: [PATCH 10/13] Backward compatibility for sticky --- docs/basics.md | 16 ++++---- provider/consul/consul_catalog.go | 15 ++++--- provider/consul/consul_catalog_test.go | 32 ++------------- provider/docker/docker.go | 25 ++++++------ provider/docker/docker_test.go | 40 +------------------ provider/ecs/ecs.go | 21 ++++++---- provider/kubernetes/kubernetes.go | 10 +++-- provider/kubernetes/kubernetes_test.go | 6 +-- provider/kv/kv.go | 18 +++++---- provider/kv/kv_test.go | 42 -------------------- provider/marathon/marathon.go | 21 +++++----- provider/marathon/marathon_test.go | 54 ++++++++++++++------------ provider/rancher/rancher.go | 26 +++++++------ provider/rancher/rancher_test.go | 46 +--------------------- server/server.go | 3 +- templates/consul_catalog.tmpl | 4 +- templates/docker.tmpl | 3 +- templates/ecs.tmpl | 3 +- templates/kubernetes.tmpl | 5 ++- templates/kv.tmpl | 4 +- templates/marathon.tmpl | 3 +- templates/rancher.tmpl | 3 +- 22 files changed, 140 insertions(+), 260 deletions(-) diff --git a/docs/basics.md b/docs/basics.md index 79ecf9398..f117e554c 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -354,21 +354,19 @@ The default cookie name is an abbreviation of a sha1 (ex: `_1d52e`). On subsequent requests, the client will be directed to the backend stored in the cookie if it is still healthy. If not, a new backend will be assigned. -To activate sticky session: ```toml [backends] [backends.backend1] + # Enable sticky session [backends.backend1.loadbalancer.stickiness] -``` -To customize the cookie name: - -```toml -[backends] - [backends.backend1] - [backends.backend1.loadbalancer.stickiness] - cookieName = "my_cookie" + # Customize the cookie name + # + # Optional + # Default: a sha1 (6 chars) + # + # cookieName = "my_cookie" ``` The deprecated way: diff --git a/provider/consul/consul_catalog.go b/provider/consul/consul_catalog.go index 868bcd6b4..db9c7bbc2 100644 --- a/provider/consul/consul_catalog.go +++ b/provider/consul/consul_catalog.go @@ -397,17 +397,19 @@ func (p *CatalogProvider) getBasicAuth(tags []string) []string { return []string{} } -func (p *CatalogProvider) hasStickinessLabel(tags []string) bool { - stickinessTag := p.getTag(types.LabelBackendLoadbalancerStickiness, tags, "") - +func (p *CatalogProvider) getSticky(tags []string) string { stickyTag := p.getTag(types.LabelBackendLoadbalancerSticky, tags, "") if len(stickyTag) > 0 { log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) + } else { + stickyTag = "false" } + return stickyTag +} - stickiness := len(stickinessTag) > 0 && strings.EqualFold(strings.TrimSpace(stickinessTag), "true") - sticky := len(stickyTag) > 0 && strings.EqualFold(strings.TrimSpace(stickyTag), "true") - return stickiness || sticky +func (p *CatalogProvider) hasStickinessLabel(tags []string) bool { + stickinessTag := p.getTag(types.LabelBackendLoadbalancerStickiness, tags, "") + return len(stickinessTag) > 0 && strings.EqualFold(strings.TrimSpace(stickinessTag), "true") } func (p *CatalogProvider) getStickinessCookieName(tags []string) string { @@ -465,6 +467,7 @@ func (p *CatalogProvider) buildConfig(catalog []catalogUpdate) *types.Configurat "getBackendName": p.getBackendName, "getBackendAddress": p.getBackendAddress, "getBasicAuth": p.getBasicAuth, + "getSticky": p.getSticky, "hasStickinessLabel": p.hasStickinessLabel, "getStickinessCookieName": p.getStickinessCookieName, "getAttribute": p.getAttribute, diff --git a/provider/consul/consul_catalog_test.go b/provider/consul/consul_catalog_test.go index 7b8ce4a7b..8ffdd3e0d 100644 --- a/provider/consul/consul_catalog_test.go +++ b/provider/consul/consul_catalog_test.go @@ -904,43 +904,19 @@ func TestConsulCatalogHasStickinessLabel(t *testing.T) { tags: []string{}, expected: false, }, - { - desc: "sticky=true", - tags: []string{ - "traefik.backend.loadbalancer.sticky=true", - }, - expected: true, - }, { desc: "stickiness=true", tags: []string{ - "traefik.backend.loadbalancer.stickiness=true", + types.LabelBackendLoadbalancerStickiness + "=true", }, expected: true, }, { - desc: "sticky=true and stickiness=true", + desc: "stickiness=false", tags: []string{ - "traefik.backend.loadbalancer.sticky=true", - "traefik.backend.loadbalancer.stickiness=true", + types.LabelBackendLoadbalancerStickiness + "=false", }, - expected: true, - }, - { - desc: "sticky=false and stickiness=true", - tags: []string{ - "traefik.backend.loadbalancer.sticky=true", - "traefik.backend.loadbalancer.stickiness=false", - }, - expected: true, - }, - { - desc: "sticky=true and stickiness=false", - tags: []string{ - "traefik.backend.loadbalancer.sticky=true", - "traefik.backend.loadbalancer.stickiness=false", - }, - expected: true, + expected: false, }, } diff --git a/provider/docker/docker.go b/provider/docker/docker.go index b1570e81c..2b7f80454 100644 --- a/provider/docker/docker.go +++ b/provider/docker/docker.go @@ -275,6 +275,7 @@ func (p *Provider) loadDockerConfig(containersInspected []dockerData) *types.Con "hasMaxConnLabels": p.hasMaxConnLabels, "getMaxConnAmount": p.getMaxConnAmount, "getMaxConnExtractorFunc": p.getMaxConnExtractorFunc, + "getSticky": p.getSticky, "getStickinessCookieName": p.getStickinessCookieName, "hasStickinessLabel": p.hasStickinessLabel, "getIsBackendLBSwarm": p.getIsBackendLBSwarm, @@ -465,10 +466,10 @@ func (p *Provider) getServiceProtocol(container dockerData, serviceName string) func (p *Provider) hasLoadBalancerLabel(container dockerData) bool { _, errMethod := getLabel(container, types.LabelBackendLoadbalancerMethod) _, errSticky := getLabel(container, types.LabelBackendLoadbalancerSticky) - if errMethod != nil && errSticky != nil { - return false - } - return true + _, errStickiness := getLabel(container, types.LabelBackendLoadbalancerStickiness) + _, errCookieName := getLabel(container, types.LabelBackendLoadbalancerStickinessCookieName) + + return errMethod == nil || errSticky == nil || errStickiness == nil || errCookieName == nil } func (p *Provider) hasMaxConnLabels(container dockerData) bool { @@ -646,15 +647,17 @@ func (p *Provider) getWeight(container dockerData) string { func (p *Provider) hasStickinessLabel(container dockerData) bool { labelStickiness, errStickiness := getLabel(container, types.LabelBackendLoadbalancerStickiness) + return errStickiness == nil && len(labelStickiness) > 0 && strings.EqualFold(strings.TrimSpace(labelStickiness), "true") +} - labelSticky, errSticky := getLabel(container, types.LabelBackendLoadbalancerSticky) - if len(labelSticky) > 0 { - log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) +func (p *Provider) getSticky(container dockerData) string { + if label, err := getLabel(container, types.LabelBackendLoadbalancerSticky); err == nil { + if len(label) > 0 { + log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) + } + return label } - - stickiness := errStickiness == nil && len(labelStickiness) > 0 && strings.EqualFold(strings.TrimSpace(labelStickiness), "true") - sticky := errSticky == nil && len(labelSticky) > 0 && strings.EqualFold(strings.TrimSpace(labelSticky), "true") - return stickiness || sticky + return "false" } func (p *Provider) getStickinessCookieName(container dockerData) string { diff --git a/provider/docker/docker_test.go b/provider/docker/docker_test.go index 0701dc2e2..718890e3e 100644 --- a/provider/docker/docker_test.go +++ b/provider/docker/docker_test.go @@ -1060,24 +1060,10 @@ func TestDockerHasStickinessLabel(t *testing.T) { expected bool }{ { - desc: "no sticky/stickiness-label", + desc: "no stickiness-label", container: containerJSON(), expected: false, }, - { - desc: "sticky true", - container: containerJSON(labels(map[string]string{ - types.LabelBackendLoadbalancerSticky: "true", - })), - expected: true, - }, - { - desc: "sticky false", - container: containerJSON(labels(map[string]string{ - types.LabelBackendLoadbalancerSticky: "false", - })), - expected: false, - }, { desc: "stickiness true", container: containerJSON(labels(map[string]string{ @@ -1092,30 +1078,6 @@ func TestDockerHasStickinessLabel(t *testing.T) { })), expected: false, }, - { - desc: "sticky true + stickiness false", - container: containerJSON(labels(map[string]string{ - types.LabelBackendLoadbalancerSticky: "true", - types.LabelBackendLoadbalancerStickiness: "false", - })), - expected: true, - }, - { - desc: "sticky false + stickiness true", - container: containerJSON(labels(map[string]string{ - types.LabelBackendLoadbalancerSticky: "false", - types.LabelBackendLoadbalancerStickiness: "true", - })), - expected: true, - }, - { - desc: "sticky false + stickiness false", - container: containerJSON(labels(map[string]string{ - types.LabelBackendLoadbalancerSticky: "false", - types.LabelBackendLoadbalancerStickiness: "false", - })), - expected: false, - }, } for _, test := range testCases { diff --git a/provider/ecs/ecs.go b/provider/ecs/ecs.go index d76e3f939..9b5eab4b7 100644 --- a/provider/ecs/ecs.go +++ b/provider/ecs/ecs.go @@ -184,6 +184,7 @@ func (p *Provider) loadECSConfig(ctx context.Context, client *awsClient) (*types "getFrontendRule": p.getFrontendRule, "getBasicAuth": p.getBasicAuth, "getLoadBalancerMethod": p.getLoadBalancerMethod, + "getLoadBalancerSticky": p.getLoadBalancerSticky, "hasStickinessLabel": p.hasStickinessLabel, "getStickinessCookieName": p.getStickinessCookieName, } @@ -485,16 +486,20 @@ func getFirstInstanceLabel(instances []ecsInstance, labelName string) string { return "" } +func (p *Provider) getLoadBalancerSticky(instances []ecsInstance) string { + if len(instances) > 0 { + label := getFirstInstanceLabel(instances, types.LabelBackendLoadbalancerSticky) + if label != "" { + log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) + return label + } + } + return "false" +} + func (p *Provider) hasStickinessLabel(instances []ecsInstance) bool { stickinessLabel := getFirstInstanceLabel(instances, types.LabelBackendLoadbalancerStickiness) - - stickyLabel := getFirstInstanceLabel(instances, types.LabelBackendLoadbalancerSticky) - if len(stickyLabel) > 0 { - log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) - } - stickiness := len(stickinessLabel) > 0 && strings.EqualFold(strings.TrimSpace(stickinessLabel), "true") - sticky := len(stickyLabel) > 0 && strings.EqualFold(strings.TrimSpace(stickyLabel), "true") - return stickiness || sticky + return len(stickinessLabel) > 0 && strings.EqualFold(strings.TrimSpace(stickinessLabel), "true") } func (p *Provider) getStickinessCookieName(instances []ecsInstance) string { diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index 5d38b5af7..05b9382c1 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -248,13 +248,15 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) templateObjects.Backends[r.Host+pa.Path].LoadBalancer.Method = "drr" } - if len(service.Annotations[types.LabelBackendLoadbalancerSticky]) > 0 { + if sticky := service.Annotations[types.LabelBackendLoadbalancerSticky]; len(sticky) > 0 { log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) + templateObjects.Backends[r.Host+pa.Path].LoadBalancer.Sticky = strings.EqualFold(strings.TrimSpace(sticky), "true") } - if service.Annotations[types.LabelBackendLoadbalancerSticky] == "true" || service.Annotations[types.LabelBackendLoadbalancerStickiness] == "true" { - templateObjects.Backends[r.Host+pa.Path].LoadBalancer.Stickiness = &types.Stickiness{ - CookieName: r.Host + pa.Path, + if service.Annotations[types.LabelBackendLoadbalancerStickiness] == "true" { + templateObjects.Backends[r.Host+pa.Path].LoadBalancer.Stickiness = &types.Stickiness{} + if cookieName := service.Annotations[types.LabelBackendLoadbalancerStickinessCookieName]; len(cookieName) > 0 { + templateObjects.Backends[r.Host+pa.Path].LoadBalancer.Stickiness.CookieName = cookieName } } diff --git a/provider/kubernetes/kubernetes_test.go b/provider/kubernetes/kubernetes_test.go index 5e0a66bf9..cdd5fe9bc 100644 --- a/provider/kubernetes/kubernetes_test.go +++ b/provider/kubernetes/kubernetes_test.go @@ -1325,9 +1325,7 @@ func TestServiceAnnotations(t *testing.T) { CircuitBreaker: nil, LoadBalancer: &types.LoadBalancer{ Method: "wrr", - Stickiness: &types.Stickiness{ - CookieName: "bar", - }, + Sticky: true, }, }, }, @@ -1356,7 +1354,7 @@ func TestServiceAnnotations(t *testing.T) { }, } - assert.Equal(t, expected, actual) + assert.EqualValues(t, expected, actual) } func TestIngressAnnotations(t *testing.T) { diff --git a/provider/kv/kv.go b/provider/kv/kv.go index c75d2f231..1ebb4298f 100644 --- a/provider/kv/kv.go +++ b/provider/kv/kv.go @@ -144,6 +144,7 @@ func (p *Provider) loadConfig() *types.Configuration { "Get": p.get, "SplitGet": p.splitGet, "Last": p.last, + "getSticky": p.getSticky, "hasStickinessLabel": p.hasStickinessLabel, "getStickinessCookieName": p.getStickinessCookieName, } @@ -242,18 +243,19 @@ func (p *Provider) checkConstraints(keys ...string) bool { return true } -func (p *Provider) hasStickinessLabel(rootPath string) bool { - stickyValue := p.get("false", rootPath, "/loadbalancer", "/sticky") - - sticky := len(stickyValue) != 0 && strings.EqualFold(strings.TrimSpace(stickyValue), "true") - if sticky { +func (p *Provider) getSticky(rootPath string) string { + stickyValue := p.get("", rootPath, "/loadbalancer", "/sticky") + if len(stickyValue) > 0 { log.Warnf("Deprecated configuration found: %s. Please use %s.", "loadbalancer/sticky", "loadbalancer/stickiness") + } else { + stickyValue = "false" } + return stickyValue +} +func (p *Provider) hasStickinessLabel(rootPath string) bool { stickinessValue := p.get("false", rootPath, "/loadbalancer", "/stickiness") - stickiness := len(stickinessValue) > 0 && strings.EqualFold(strings.TrimSpace(stickinessValue), "true") - - return stickiness || sticky + return len(stickinessValue) > 0 && strings.EqualFold(strings.TrimSpace(stickinessValue), "true") } func (p *Provider) getStickinessCookieName(rootPath string) string { diff --git a/provider/kv/kv_test.go b/provider/kv/kv_test.go index 91aa64de8..a78c0ac90 100644 --- a/provider/kv/kv_test.go +++ b/provider/kv/kv_test.go @@ -399,48 +399,6 @@ func TestKVHasStickinessLabel(t *testing.T) { }, expected: true, }, - { - desc: "stickiness=true and sticky=true", - KVPairs: []*store.KVPair{ - { - Key: "loadbalancer/stickiness", - Value: []byte("true"), - }, - { - Key: "loadbalancer/sticky", - Value: []byte("true"), - }, - }, - expected: true, - }, - { - desc: "stickiness=false and sticky=true", - KVPairs: []*store.KVPair{ - { - Key: "loadbalancer/stickiness", - Value: []byte("false"), - }, - { - Key: "loadbalancer/sticky", - Value: []byte("true"), - }, - }, - expected: true, - }, - { - desc: "stickiness=true and sticky=false", - KVPairs: []*store.KVPair{ - { - Key: "loadbalancer/stickiness", - Value: []byte("true"), - }, - { - Key: "loadbalancer/sticky", - Value: []byte("false"), - }, - }, - expected: true, - }, } for _, test := range testCases { diff --git a/provider/marathon/marathon.go b/provider/marathon/marathon.go index 982306bf7..41e61a703 100644 --- a/provider/marathon/marathon.go +++ b/provider/marathon/marathon.go @@ -188,8 +188,9 @@ func (p *Provider) loadMarathonConfig() *types.Configuration { "getMaxConnAmount": p.getMaxConnAmount, "getLoadBalancerMethod": p.getLoadBalancerMethod, "getCircuitBreakerExpression": p.getCircuitBreakerExpression, - "getStickinessCookieName": p.getStickinessCookieName, + "getSticky": p.getSticky, "hasStickinessLabel": p.hasStickinessLabel, + "getStickinessCookieName": p.getStickinessCookieName, "hasHealthCheckLabels": p.hasHealthCheckLabels, "getHealthCheckPath": p.getHealthCheckPath, "getHealthCheckInterval": p.getHealthCheckInterval, @@ -429,17 +430,17 @@ func (p *Provider) getProtocol(application marathon.Application, serviceName str return "http" } +func (p *Provider) getSticky(application marathon.Application) string { + if sticky, ok := p.getAppLabel(application, types.LabelBackendLoadbalancerSticky); ok { + log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) + return sticky + } + return "false" +} + func (p *Provider) hasStickinessLabel(application marathon.Application) bool { labelStickiness, okStickiness := p.getAppLabel(application, types.LabelBackendLoadbalancerStickiness) - - labelSticky, okSticky := p.getAppLabel(application, types.LabelBackendLoadbalancerSticky) - if len(labelSticky) > 0 { - log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) - } - - stickiness := okStickiness && len(labelStickiness) > 0 && strings.EqualFold(strings.TrimSpace(labelStickiness), "true") - sticky := okSticky && len(labelSticky) > 0 && strings.EqualFold(strings.TrimSpace(labelSticky), "true") - return stickiness || sticky + return okStickiness && len(labelStickiness) > 0 && strings.EqualFold(strings.TrimSpace(labelStickiness), "true") } func (p *Provider) getStickinessCookieName(application marathon.Application) string { diff --git a/provider/marathon/marathon_test.go b/provider/marathon/marathon_test.go index 145cefe5e..4c58ab62e 100644 --- a/provider/marathon/marathon_test.go +++ b/provider/marathon/marathon_test.go @@ -854,6 +854,36 @@ func TestMarathonGetProtocol(t *testing.T) { }) } } +func TestMarathonGetSticky(t *testing.T) { + testCases := []struct { + desc string + application marathon.Application + expected string + }{ + { + desc: "label missing", + application: application(), + expected: "false", + }, + { + desc: "label existing", + application: application(label(types.LabelBackendLoadbalancerSticky, "true")), + expected: "true", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + provider := &Provider{} + actual := provider.getSticky(test.application) + if actual != test.expected { + t.Errorf("actual %q, expected %q", actual, test.expected) + } + }) + } +} func TestMarathonHasStickinessLabel(t *testing.T) { testCases := []struct { @@ -866,16 +896,6 @@ func TestMarathonHasStickinessLabel(t *testing.T) { application: application(), expected: false, }, - { - desc: "sticky=true (deprecated)", - application: application(label(types.LabelBackendLoadbalancerSticky, "true")), - expected: true, - }, - { - desc: "sticky=false (deprecated)", - application: application(label(types.LabelBackendLoadbalancerSticky, "false")), - expected: false, - }, { desc: "stickiness=true", application: application(label(types.LabelBackendLoadbalancerStickiness, "true")), @@ -886,20 +906,6 @@ func TestMarathonHasStickinessLabel(t *testing.T) { application: application(label(types.LabelBackendLoadbalancerStickiness, "true")), expected: true, }, - { - desc: "sticky=false stickiness=true ", - application: application( - label(types.LabelBackendLoadbalancerStickiness, "true"), - label(types.LabelBackendLoadbalancerSticky, "false")), - expected: true, - }, - { - desc: "sticky=true stickiness=false ", - application: application( - label(types.LabelBackendLoadbalancerStickiness, "false"), - label(types.LabelBackendLoadbalancerSticky, "true")), - expected: true, - }, } for _, test := range testCases { diff --git a/provider/rancher/rancher.go b/provider/rancher/rancher.go index 948c3c30b..341a285cc 100644 --- a/provider/rancher/rancher.go +++ b/provider/rancher/rancher.go @@ -92,10 +92,10 @@ func (p *Provider) getLoadBalancerMethod(service rancherData) string { func (p *Provider) hasLoadBalancerLabel(service rancherData) bool { _, errMethod := getServiceLabel(service, types.LabelBackendLoadbalancerMethod) _, errSticky := getServiceLabel(service, types.LabelBackendLoadbalancerSticky) - if errMethod != nil && errSticky != nil { - return false - } - return true + _, errStickiness := getServiceLabel(service, types.LabelBackendLoadbalancerStickiness) + _, errCookieName := getServiceLabel(service, types.LabelBackendLoadbalancerStickinessCookieName) + + return errMethod == nil || errSticky == nil || errStickiness == nil || errCookieName == nil } func (p *Provider) hasCircuitBreakerLabel(service rancherData) bool { @@ -112,17 +112,18 @@ func (p *Provider) getCircuitBreakerExpression(service rancherData) string { return "NetworkErrorRatio() > 1" } +func (p *Provider) getSticky(service rancherData) string { + if _, err := getServiceLabel(service, types.LabelBackendLoadbalancerSticky); err == nil { + log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) + return "true" + } + return "false" +} + func (p *Provider) hasStickinessLabel(service rancherData) bool { labelStickiness, errStickiness := getServiceLabel(service, types.LabelBackendLoadbalancerStickiness) - labelSticky, errSticky := getServiceLabel(service, types.LabelBackendLoadbalancerSticky) - if len(labelSticky) > 0 { - log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) - } - - stickiness := errStickiness == nil && len(labelStickiness) > 0 && strings.EqualFold(strings.TrimSpace(labelStickiness), "true") - sticky := errSticky == nil && len(labelSticky) > 0 && strings.EqualFold(strings.TrimSpace(labelSticky), "true") - return stickiness || sticky + return errStickiness == nil && len(labelStickiness) > 0 && strings.EqualFold(strings.TrimSpace(labelStickiness), "true") } func (p *Provider) getStickinessCookieName(service rancherData, backendName string) string { @@ -235,6 +236,7 @@ func (p *Provider) loadRancherConfig(services []rancherData) *types.Configuratio "hasMaxConnLabels": p.hasMaxConnLabels, "getMaxConnAmount": p.getMaxConnAmount, "getMaxConnExtractorFunc": p.getMaxConnExtractorFunc, + "getSticky": p.getSticky, "hasStickinessLabel": p.hasStickinessLabel, "getStickinessCookieName": p.getStickinessCookieName, } diff --git a/provider/rancher/rancher_test.go b/provider/rancher/rancher_test.go index d56b552e9..92426eef4 100644 --- a/provider/rancher/rancher_test.go +++ b/provider/rancher/rancher_test.go @@ -616,16 +616,6 @@ func TestRancherHasStickinessLabel(t *testing.T) { }, expected: false, }, - { - desc: "sticky=true", - service: rancherData{ - Name: "test-service", - Labels: map[string]string{ - types.LabelBackendLoadbalancerSticky: "true", - }, - }, - expected: true, - }, { desc: "stickiness=true", service: rancherData{ @@ -637,49 +627,15 @@ func TestRancherHasStickinessLabel(t *testing.T) { expected: true, }, { - desc: "sticky=true and stickiness=true", + desc: "stickiness=true", service: rancherData{ Name: "test-service", Labels: map[string]string{ - types.LabelBackendLoadbalancerSticky: "true", - types.LabelBackendLoadbalancerStickiness: "true", - }, - }, - expected: true, - }, - { - desc: "sticky=false and stickiness=false", - service: rancherData{ - Name: "test-service", - Labels: map[string]string{ - types.LabelBackendLoadbalancerSticky: "false", types.LabelBackendLoadbalancerStickiness: "false", }, }, expected: false, }, - { - desc: "sticky=true and stickiness=false", - service: rancherData{ - Name: "test-service", - Labels: map[string]string{ - types.LabelBackendLoadbalancerSticky: "true", - types.LabelBackendLoadbalancerStickiness: "false", - }, - }, - expected: true, - }, - { - desc: "sticky=false and stickiness=true", - service: rancherData{ - Name: "test-service", - Labels: map[string]string{ - types.LabelBackendLoadbalancerSticky: "false", - types.LabelBackendLoadbalancerStickiness: "true", - }, - }, - expected: true, - }, } for _, test := range testCases { diff --git a/server/server.go b/server/server.go index cfcd91411..7a8893d06 100644 --- a/server/server.go +++ b/server/server.go @@ -1172,7 +1172,8 @@ func (server *Server) configureFrontends(frontends map[string]*types.Frontend) { } func (*Server) configureBackends(backends map[string]*types.Backend) { - for backendName, backend := range backends { + for backendName := range backends { + backend := backends[backendName] if backend.LoadBalancer != nil && backend.LoadBalancer.Sticky { log.Warnf("Deprecated configuration found: %s. Please use %s.", "backend.LoadBalancer.Sticky", "backend.LoadBalancer.Stickiness") } diff --git a/templates/consul_catalog.tmpl b/templates/consul_catalog.tmpl index 93777c55c..1d5861735 100644 --- a/templates/consul_catalog.tmpl +++ b/templates/consul_catalog.tmpl @@ -18,9 +18,9 @@ [backends."backend-{{$service}}".loadbalancer] method = "{{getAttribute "backend.loadbalancer" .Attributes "wrr"}}" - sticky = {{getAttribute "backend.loadbalancer.sticky" .Attributes "false"}} + sticky = {{getSticky .Attributes}} {{if hasStickinessLabel .Attributes}} - [Backends."backend-{{$service}}".LoadBalancer.Stickiness] + [backends."backend-{{$service}}".loadbalancer.stickiness] cookieName = "{{getStickinessCookieName .Attributes}}" {{end}} diff --git a/templates/docker.tmpl b/templates/docker.tmpl index ef6f69526..c2a7cf54b 100644 --- a/templates/docker.tmpl +++ b/templates/docker.tmpl @@ -8,8 +8,9 @@ {{if hasLoadBalancerLabel $backend}} [backends.backend-{{$backendName}}.loadbalancer] method = "{{getLoadBalancerMethod $backend}}" + sticky = {{getSticky $backend}} {{if hasStickinessLabel $backend}} - [Backends."{{$backendName}}".LoadBalancer.Stickiness] + [backends.backend-{{$backendName}}.loadBalancer.stickiness] cookieName = "{{getStickinessCookieName $backend}}" {{end}} {{end}} diff --git a/templates/ecs.tmpl b/templates/ecs.tmpl index f42642ad4..74c96504a 100644 --- a/templates/ecs.tmpl +++ b/templates/ecs.tmpl @@ -1,8 +1,9 @@ [backends]{{range $serviceName, $instances := .Services}} [backends.backend-{{ $serviceName }}.loadbalancer] method = "{{ getLoadBalancerMethod $instances}}" + sticky = {{ getLoadBalancerSticky $instances}} {{if hasStickinessLabel $instances}} - [Backends.backend-{{ $serviceName }}.LoadBalancer.Stickiness] + [backends.backend-{{ $serviceName }}.loadbalancer.stickiness] cookieName = "{{getStickinessCookieName $instances}}" {{end}} diff --git a/templates/kubernetes.tmpl b/templates/kubernetes.tmpl index bab94ae19..8f31c3e79 100644 --- a/templates/kubernetes.tmpl +++ b/templates/kubernetes.tmpl @@ -6,8 +6,11 @@ {{end}} [backends."{{$backendName}}".loadbalancer] method = "{{$backend.LoadBalancer.Method}}" + {{if $backend.LoadBalancer.Sticky}} + sticky = true + {{end}} {{if $backend.LoadBalancer.Stickiness}} - [Backends."{{$backendName}}".LoadBalancer.Stickiness] + [backends."{{$backendName}}".loadbalancer.stickiness] cookieName = "{{$backend.LoadBalancer.Stickiness.CookieName}}" {{end}} {{range $serverName, $server := $backend.Servers}} diff --git a/templates/kv.tmpl b/templates/kv.tmpl index ebbbb39c2..291a14d38 100644 --- a/templates/kv.tmpl +++ b/templates/kv.tmpl @@ -16,9 +16,9 @@ {{with $loadBalancer}} [backends."{{$backendName}}".loadBalancer] method = "{{$loadBalancer}}" - sticky = {{ Get "false" . "/loadbalancer/" "sticky" }} + sticky = {{ getSticky . }} {{if hasStickinessLabel $backend}} - [Backends."{{$backendName}}".LoadBalancer.Stickiness] + [backends."{{$backendName}}".loadBalancer.stickiness] cookieName = {{getStickinessCookieName $backend}} {{end}} {{end}} diff --git a/templates/marathon.tmpl b/templates/marathon.tmpl index aaa7f2e6a..e03fd601c 100644 --- a/templates/marathon.tmpl +++ b/templates/marathon.tmpl @@ -20,8 +20,9 @@ {{ if hasLoadBalancerLabels $app }} [backends."backend{{getBackend $app $serviceName }}".loadbalancer] method = "{{getLoadBalancerMethod $app }}" + sticky = {{getSticky $app}} {{if hasStickinessLabel $app}} - [Backends."backend{{getBackend $app $serviceName }}".LoadBalancer.Stickiness] + [backends."backend{{getBackend $app $serviceName }}".loadbalancer.stickiness] cookieName = "{{getStickinessCookieName $app}}" {{end}} {{end}} diff --git a/templates/rancher.tmpl b/templates/rancher.tmpl index 1f911e5b2..a5355c252 100644 --- a/templates/rancher.tmpl +++ b/templates/rancher.tmpl @@ -8,8 +8,9 @@ {{if hasLoadBalancerLabel $backend}} [backends.backend-{{$backendName}}.loadbalancer] method = "{{getLoadBalancerMethod $backend}}" + sticky = {{getSticky $backend}} {{if hasStickinessLabel $backend}} - [Backends."{{$backendName}}".LoadBalancer.Stickiness] + [backends.backend-{{$backendName}}.loadbalancer.stickiness] cookieName = "{{getStickinessCookieName $backend}}" {{end}} {{end}} From 47ff51e6405847d640e87d105801c934cfb89ed7 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Mon, 16 Oct 2017 18:06:04 +0200 Subject: [PATCH 11/13] add retry backoff to staert config loading --- cmd/traefik/traefik.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index 969a03ee9..84fd5af06 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -14,11 +14,13 @@ import ( "time" "github.com/Sirupsen/logrus" + "github.com/cenk/backoff" "github.com/containous/flaeg" "github.com/containous/staert" "github.com/containous/traefik/acme" "github.com/containous/traefik/cluster" "github.com/containous/traefik/configuration" + "github.com/containous/traefik/job" "github.com/containous/traefik/log" "github.com/containous/traefik/provider/ecs" "github.com/containous/traefik/provider/kubernetes" @@ -209,7 +211,15 @@ Complete documentation is available at https://traefik.io`, traefikConfiguration.Cluster.Store = &types.Store{Prefix: kv.Prefix, Store: kv.Store} } s.AddSource(kv) - if _, err := s.LoadConfig(); err != nil { + operation := func() error { + _, err := s.LoadConfig() + return err + } + notify := func(err error, time time.Duration) { + log.Errorf("Load config error: %+v, retrying in %s", err, time) + } + err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify) + if err != nil { fmtlog.Printf("Error loading configuration: %s\n", err) os.Exit(-1) } From ff4c7b82bc471975b5982b7aa4763df47832e240 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 16 Oct 2017 18:42:03 +0200 Subject: [PATCH 12/13] Prepare release v1.4.0 --- CHANGELOG.md | 220 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index acc1bc899..1803bb329 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,225 @@ # Change Log +## [v1.4.0](https://github.com/containous/traefik/tree/v1.4.0) (2017-05-31) +[All Commits](https://github.com/containous/traefik/compare/v1.3.0-rc1...v1.4.0) + +**Enhancements:** +- **[acme]** Display Traefik logs in integration tests ([#2114](https://github.com/containous/traefik/pull/2114) by [ldez](https://github.com/ldez)) +- **[acme]** Make the ACME developments testing easier ([#1769](https://github.com/containous/traefik/pull/1769) by [nmengin](https://github.com/nmengin)) +- **[acme]** contrib: Dump keys/certs from acme.json to files ([#1484](https://github.com/containous/traefik/pull/1484) by [brianredbeard](https://github.com/brianredbeard)) +- **[api]** Add HTTP HEAD handling to /ping endpoint ([#1768](https://github.com/containous/traefik/pull/1768) by [martinbaillie](https://github.com/martinbaillie)) +- **[authentication,consulcatalog]** Add Basic auth for consul catalog ([#2027](https://github.com/containous/traefik/pull/2027) by [mmatur](https://github.com/mmatur)) +- **[authentication,marathon]** Add marathon label to configure basic auth ([#1799](https://github.com/containous/traefik/pull/1799) by [nikore](https://github.com/nikore)) +- **[authentication,ecs]** Add basic auth for ecs ([#2026](https://github.com/containous/traefik/pull/2026) by [mmatur](https://github.com/mmatur)) +- **[authentication,middleware]** Add forward authentication option ([#1972](https://github.com/containous/traefik/pull/1972) by [drampelt](https://github.com/drampelt)) +- **[authentication]** Manage Headers for the Authentication forwarding. ([#2132](https://github.com/containous/traefik/pull/2132) by [ldez](https://github.com/ldez)) +- **[consulcatalog,sticky-session]** Enable loadbalancer.sticky for Consul Catalog ([#1917](https://github.com/containous/traefik/pull/1917) by [nbonneval](https://github.com/nbonneval)) +- **[consulcatalog]** Exposed by default feature in Consul Catalog ([#2006](https://github.com/containous/traefik/pull/2006) by [mmatur](https://github.com/mmatur)) +- **[consulcatalog]** Speeding up consul catalog health change detection ([#1694](https://github.com/containous/traefik/pull/1694) by [vholovko](https://github.com/vholovko)) +- **[consulcatalog]** Enhanced flexibility in Consul Catalog configuration ([#1565](https://github.com/containous/traefik/pull/1565) by [aantono](https://github.com/aantono)) +- **[docker,k8s]** IP Whitelists for Frontend (with Docker- & Kubernetes-Provider Support) ([#1332](https://github.com/containous/traefik/pull/1332) by [MaZderMind](https://github.com/MaZderMind)) +- **[ecs,sticky-session]** Enable loadbalancer.sticky for ECS ([#1925](https://github.com/containous/traefik/pull/1925) by [mmatur](https://github.com/mmatur)) +- **[ecs]** Add support for several ECS backends ([#1913](https://github.com/containous/traefik/pull/1913) by [mmatur](https://github.com/mmatur)) +- **[file]** Allow file provider to load service config from files in a directory. ([#1672](https://github.com/containous/traefik/pull/1672) by [rjshep](https://github.com/rjshep)) +- **[healthcheck]** Add healthcheck command ([#1982](https://github.com/containous/traefik/pull/1982) by [emilevauge](https://github.com/emilevauge)) +- **[healthcheck]** Allow overriding the port used for healthchecks ([#1567](https://github.com/containous/traefik/pull/1567) by [bakins](https://github.com/bakins)) +- **[k8s,rules]** kubernetes ingress rewrite-target implementation ([#1723](https://github.com/containous/traefik/pull/1723) by [mlaccetti](https://github.com/mlaccetti)) +- **[k8s]** Added ability to override frontend priority for k8s ingress router ([#1874](https://github.com/containous/traefik/pull/1874) by [DiverOfDark](https://github.com/DiverOfDark)) +- **[kv]** Adds definitions to backend kv template for health checking ([#1644](https://github.com/containous/traefik/pull/1644) by [zachomedia](https://github.com/zachomedia)) +- **[logs,dynamodb,ecs,marathon]** Link some providers logs to Traefik ([#1746](https://github.com/containous/traefik/pull/1746) by [ldez](https://github.com/ldez)) +- **[logs,marathon]** remove confusing go-marathon log message ([#1810](https://github.com/containous/traefik/pull/1810) by [marco-jantke](https://github.com/marco-jantke)) +- **[logs]** Send traefik logs to stdout instead stderr ([#2054](https://github.com/containous/traefik/pull/2054) by [marco-jantke](https://github.com/marco-jantke)) +- **[logs]** enable logging to stdout for access logs ([#1683](https://github.com/containous/traefik/pull/1683) by [marco-jantke](https://github.com/marco-jantke)) +- **[logs]** Logs & errors review ([#1673](https://github.com/containous/traefik/pull/1673) by [ldez](https://github.com/ldez)) +- **[logs]** Switch access logging to logrus ([#1647](https://github.com/containous/traefik/pull/1647) by [rjshep](https://github.com/rjshep)) +- **[logs]** log X-Forwarded-For as ClientHost if present ([#1946](https://github.com/containous/traefik/pull/1946) by [mildis](https://github.com/mildis)) +- **[logs]** Restore: First stage of access logging middleware. ([#1571](https://github.com/containous/traefik/pull/1571) by [ldez](https://github.com/ldez)) +- **[logs]** Add log file close and reopen on receipt of SIGUSR1 ([#1761](https://github.com/containous/traefik/pull/1761) by [rjshep](https://github.com/rjshep)) +- **[logs]** add RetryAttempts to AccessLog in JSON format ([#1793](https://github.com/containous/traefik/pull/1793) by [marco-jantke](https://github.com/marco-jantke)) +- **[logs]** Add JSON as access logging format ([#1669](https://github.com/containous/traefik/pull/1669) by [rjshep](https://github.com/rjshep)) +- **[marathon]** Support multi-port service routing for containers running on Marathon ([#1742](https://github.com/containous/traefik/pull/1742) by [aantono](https://github.com/aantono)) +- **[marathon]** Improve Marathon integration tests. ([#1406](https://github.com/containous/traefik/pull/1406) by [timoreimann](https://github.com/timoreimann)) +- **[marathon]** Exported getSubDomain function from Marathon provider ([#1693](https://github.com/containous/traefik/pull/1693) by [aantono](https://github.com/aantono)) +- **[marathon]** Use test builder. ([#1871](https://github.com/containous/traefik/pull/1871) by [timoreimann](https://github.com/timoreimann)) +- **[marathon]** Add support for readiness checks. ([#1883](https://github.com/containous/traefik/pull/1883) by [timoreimann](https://github.com/timoreimann)) +- **[marathon]** Move marathon mock ([#1732](https://github.com/containous/traefik/pull/1732) by [ldez](https://github.com/ldez)) +- **[marathon]** Use single API call to fetch Marathon resources. ([#1815](https://github.com/containous/traefik/pull/1815) by [timoreimann](https://github.com/timoreimann)) +- **[metrics]** Added RetryMetrics to DataDog and StatsD providers ([#1884](https://github.com/containous/traefik/pull/1884) by [aantono](https://github.com/aantono)) +- **[metrics]** Extract metrics to own package and refactor implementations ([#1968](https://github.com/containous/traefik/pull/1968) by [marco-jantke](https://github.com/marco-jantke)) +- **[metrics]** Add metrics for backend_retries_total ([#1504](https://github.com/containous/traefik/pull/1504) by [marco-jantke](https://github.com/marco-jantke)) +- **[metrics]** Add status code to request duration metric ([#1755](https://github.com/containous/traefik/pull/1755) by [marco-jantke](https://github.com/marco-jantke)) +- **[middleware]** Add trusted whitelist proxy protocol ([#2234](https://github.com/containous/traefik/pull/2234) by [emilevauge](https://github.com/emilevauge))) +- **[metrics]** DataDog and StatsD Metrics Support ([#1701](https://github.com/containous/traefik/pull/1701) by [aantono](https://github.com/aantono)) +- **[middleware]** Create Header Middleware ([#1236](https://github.com/containous/traefik/pull/1236) by [dtomcej](https://github.com/dtomcej)) +- **[middleware]** Add configurable timeouts and curate default timeout settings ([#1873](https://github.com/containous/traefik/pull/1873) by [marco-jantke](https://github.com/marco-jantke)) +- **[middleware]** Fix command bug content. ([#2002](https://github.com/containous/traefik/pull/2002) by [ldez](https://github.com/ldez)) +- **[middleware]** Retry only on real network errors ([#1549](https://github.com/containous/traefik/pull/1549) by [marco-jantke](https://github.com/marco-jantke)) +- **[middleware]** Return 503 on empty backend ([#1748](https://github.com/containous/traefik/pull/1748) by [marco-jantke](https://github.com/marco-jantke)) +- **[middleware]** Custom Error Pages ([#1675](https://github.com/containous/traefik/pull/1675) by [bparli](https://github.com/bparli)) +- **[oxy]** Support X-Forwarded-Port. ([#1960](https://github.com/containous/traefik/pull/1960) by [ldez](https://github.com/ldez)) +- **[provider,tls]** Added a check to ensure clientTLS configuration contains either a cert or a key ([#1932](https://github.com/containous/traefik/pull/1932) by [aantono](https://github.com/aantono)) +- **[provider]** Deflake integration tests ([#1599](https://github.com/containous/traefik/pull/1599) by [ldez](https://github.com/ldez)) +- **[provider]** Factorize labels ([#1843](https://github.com/containous/traefik/pull/1843) by [ldez](https://github.com/ldez)) +- **[provider]** Replace go routine by Safe.Go ([#1879](https://github.com/containous/traefik/pull/1879) by [ldez](https://github.com/ldez)) +- **[rancher]** Refactor into dual Rancher API/Metadata providers ([#1563](https://github.com/containous/traefik/pull/1563) by [martinbaillie](https://github.com/martinbaillie)) +- **[rules]** Add support for Query String filtering ([#1934](https://github.com/containous/traefik/pull/1934) by [driverpt](https://github.com/driverpt)) +- **[rules]** Simplify stripPrefix and stripPrefixRegex tests ([#1699](https://github.com/containous/traefik/pull/1699) by [ldez](https://github.com/ldez)) +- **[rules]** Enhance rules tests. ([#1679](https://github.com/containous/traefik/pull/1679) by [ldez](https://github.com/ldez)) +- **[sticky-session]** make the cookie name unique to the backend being served ([#1716](https://github.com/containous/traefik/pull/1716) by [richardjq](https://github.com/richardjq)) +- **[tls]** Handle RootCAs certificate ([#1789](https://github.com/containous/traefik/pull/1789) by [Juliens](https://github.com/Juliens)) +- **[tls]** enable TLS client forwarding ([#1446](https://github.com/containous/traefik/pull/1446) by [drewwells](https://github.com/drewwells)) +- **[websocket]** Add tests for urlencoded part in url ([#2199](https://github.com/containous/traefik/pull/2199) by [Juliens](https://github.com/Juliens)) +- **[websocket]** Add test for SSL TERMINATION in Websocket IT ([#2063](https://github.com/containous/traefik/pull/2063) by [Juliens](https://github.com/Juliens) +- **[webui]** Proxy in dev mode ([#1544](https://github.com/containous/traefik/pull/1544) by [maxwo](https://github.com/maxwo)) +- **[webui]** Minor Health UI fixes ([#1651](https://github.com/containous/traefik/pull/1651) by [mihaitodor](https://github.com/mihaitodor)) +- Fail fast in IT and fix some flaky tests ([#2126](https://github.com/containous/traefik/pull/2126) by [ldez](https://github.com/ldez)) +- extract lb configuration steps into method ([#1841](https://github.com/containous/traefik/pull/1841) by [marco-jantke](https://github.com/marco-jantke)) +- Add whitelist configuration option for entrypoints ([#1702](https://github.com/containous/traefik/pull/1702) by [christopherobin](https://github.com/christopherobin)) +- Enhance integration tests ([#1842](https://github.com/containous/traefik/pull/1842) by [ldez](https://github.com/ldez)) +- Add helloworld tests with gRPC ([#1845](https://github.com/containous/traefik/pull/1845) by [Juliens](https://github.com/Juliens)) +- Add the sprig functions in the template engine ([#1891](https://github.com/containous/traefik/pull/1891) by [thomasbach76](https://github.com/thomasbach76)) +- Refactor globalConfiguration / WebProvider ([#1938](https://github.com/containous/traefik/pull/1938) by [Juliens](https://github.com/Juliens)) +- Code cleaning. ([#1956](https://github.com/containous/traefik/pull/1956) by [ldez](https://github.com/ldez)) +- Add proxy protocol ([#2004](https://github.com/containous/traefik/pull/2004) by [emilevauge](https://github.com/emilevauge)) +- Bump gorilla/mux version. ([#1954](https://github.com/containous/traefik/pull/1954) by [ldez](https://github.com/ldez)) + +**Bug fixes:** +- **[cluster,kv]** Be certain to clear our marshalled representation before reloading it ([#2165](https://github.com/containous/traefik/pull/2165) by [gozer](https://github.com/gozer)) +- **[consulcatalog,docker,ecs,k8s,kv,marathon,rancher,sticky-session]** Backward compatibility for sticky ([#2266](https://github.com/containous/traefik/pull/2266) by [ldez](https://github.com/ldez)) +- **[consulcatalog,docker,ecs,k8s,marathon,rancher,sticky-session]** Stickiness cookie name ([#2232](https://github.com/containous/traefik/pull/2232) by [ldez](https://github.com/ldez)) +- **[consulcatalog,docker,ecs,k8s,marathon,rancher,sticky-session]** Stickiness cookie name. ([#2251](https://github.com/containous/traefik/pull/2251) by [ldez](https://github.com/ldez)) +- **[consulcatalog]** Fix consul catalog retry ([#2263](https://github.com/containous/traefik/pull/2263) by [mmatur](https://github.com/mmatur)) +- **[consulcatalog]** Flaky tests and refresh problem in consul catalog ([#2148](https://github.com/containous/traefik/pull/2148) by [Juliens](https://github.com/Juliens)) +- **[consulcatalog]** Consul catalog failed to remove service ([#2157](https://github.com/containous/traefik/pull/2157) by [Juliens](https://github.com/Juliens)) +- **[consulcatalog]** Fix Consul Catalog refresh ([#2089](https://github.com/containous/traefik/pull/2089) by [Juliens](https://github.com/Juliens)) +- **[docker]** Changed Docker network filter to allow any swarm network ([#2244](https://github.com/containous/traefik/pull/2244) by [pistolero](https://github.com/pistolero)) +- **[docker]** Error handling for docker swarm mode ([#1533](https://github.com/containous/traefik/pull/1533) by [tanyadegurechaff](https://github.com/tanyadegurechaff)) +- **[ecs]** Handle empty ECS Clusters properly ([#2170](https://github.com/containous/traefik/pull/2170) by [jeffreykoetsier](https://github.com/jeffreykoetsier)) +- **[healthcheck]** Fix healthcheck port ([#2131](https://github.com/containous/traefik/pull/2131) by [fredix](https://github.com/fredix)) +- **[healthcheck]** Bind healthcheck to backend by entryPointName ([#1868](https://github.com/containous/traefik/pull/1868) by [chrigl](https://github.com/chrigl)) +- **[k8s]** Continue processing on invalid auth-realm annotation. ([#2252](https://github.com/containous/traefik/pull/2252) by [timoreimann](https://github.com/timoreimann)) +- **[k8s]** Use default frontend priority of zero. ([#1906](https://github.com/containous/traefik/pull/1906) by [timoreimann](https://github.com/timoreimann)) +- **[kv]** add retry backoff to staert config loading ([#2268](https://github.com/containous/traefik/pull/2268) by [emilevauge](https://github.com/emilevauge)) +- **[logs,middleware]** Enable loss less rotation of log files ([#2062](https://github.com/containous/traefik/pull/2062) by [marco-jantke](https://github.com/marco-jantke)) +- **[logs,middleware]** Access log default values ([#2061](https://github.com/containous/traefik/pull/2061) by [ldez](https://github.com/ldez)) +- **[logs]** Fix flakiness in log rotation test ([#2213](https://github.com/containous/traefik/pull/2213) by [marco-jantke](https://github.com/marco-jantke)) +- **[marathon]** Assign filtered tasks to apps contained in slice. ([#1881](https://github.com/containous/traefik/pull/1881) by [timoreimann](https://github.com/timoreimann)) +- **[marathon]** Fix fallback to other nodes for Marathon ([#1740](https://github.com/containous/traefik/pull/1740) by [marco-jantke](https://github.com/marco-jantke)) +- **[metrics]** prometheus, HTTP method and utf8 ([#2081](https://github.com/containous/traefik/pull/2081) by [ldez](https://github.com/ldez)) +- **[middleware]** Enable prefix matching within slash boundaries ([#2214](https://github.com/containous/traefik/pull/2214) by [marco-jantke](https://github.com/marco-jantke)) +- **[middleware]** Fix SSE subscriptions when retries are enabled ([#2145](https://github.com/containous/traefik/pull/2145) by [marco-jantke](https://github.com/marco-jantke)) +- **[middleware]** compress: preserve status code ([#1948](https://github.com/containous/traefik/pull/1948) by [ldez](https://github.com/ldez)) +- **[rancher]** Add stack name to backend name generation to fix rancher metadata backend ([#2107](https://github.com/containous/traefik/pull/2107) by [SantoDE](https://github.com/SantoDE)) +- **[rancher]** Rancher host IP address ([#2101](https://github.com/containous/traefik/pull/2101) by [matq007](https://github.com/matq007)) +- **[rancher]** fix seconds to really be seconds ([#2259](https://github.com/containous/traefik/pull/2259) by [SantoDE](https://github.com/SantoDE)) +- **[rancher]** fix rancher api environment get ([#2053](https://github.com/containous/traefik/pull/2053) by [SantoDE](https://github.com/SantoDE)) +- **[sticky-session]** Sanitize cookie names. ([#2216](https://github.com/containous/traefik/pull/2216) by [timoreimann](https://github.com/timoreimann)) +- **[sticky-session]** Setting the Cookie Path explicitly to root ([#1950](https://github.com/containous/traefik/pull/1950) by [marcopaga](https://github.com/marcopaga)) +- **[websocket]** Forward upgrade error from backend ([#2187](https://github.com/containous/traefik/pull/2187) by [Juliens](https://github.com/Juliens)) +- **[websocket]** RawPath and Transfer TLSConfig in websocket ([#2088](https://github.com/containous/traefik/pull/2088) by [Juliens](https://github.com/Juliens)) +- Nil body retries ([#2258](https://github.com/containous/traefik/pull/2258) by [Juliens](https://github.com/Juliens)) +- Fix deprecated IdleTimeout config ([#2143](https://github.com/containous/traefik/pull/2143) by [marco-jantke](https://github.com/marco-jantke)) +- Fixes entry points configuration. ([#2120](https://github.com/containous/traefik/pull/2120) by [ldez](https://github.com/ldez)) +- Delay first version check ([#2215](https://github.com/containous/traefik/pull/2215) by [emilevauge](https://github.com/emilevauge)) +- Move http2 configure transport ([#2231](https://github.com/containous/traefik/pull/2231) by [Juliens](https://github.com/Juliens)) +- Fix error in prepareServer ([#2076](https://github.com/containous/traefik/pull/2076) by [emilevauge](https://github.com/emilevauge)) +- New entry point parser. ([#2248](https://github.com/containous/traefik/pull/2248) by [ldez](https://github.com/ldez)) +- Add TrustForwardHeader options. ([#2262](https://github.com/containous/traefik/pull/2262) by [ldez](https://github.com/ldez)) +- `bug` command. ([#2178](https://github.com/containous/traefik/pull/2178) by [ldez](https://github.com/ldez)) + +**Documentation:** +- **[acme,provider]** Enhance documentation readability. ([#2095](https://github.com/containous/traefik/pull/2095) by [ldez](https://github.com/ldez)) +- **[acme,provider]** Fix whitespaces ([#2075](https://github.com/containous/traefik/pull/2075) by [chulkilee](https://github.com/chulkilee)) +- **[acme,provider]** Re-organize documentation ([#2012](https://github.com/containous/traefik/pull/2012) by [jmaitrehenry](https://github.com/jmaitrehenry)) +- **[acme]** Fix grammar ([#2208](https://github.com/containous/traefik/pull/2208) by [mvasin](https://github.com/mvasin)) +- **[acme]** Add guide for Docker, Traefik & Letsencrypt ([#1923](https://github.com/containous/traefik/pull/1923) by [mvdstam](https://github.com/mvdstam)) +- **[acme]** Improve Let's Encrypt documentation ([#1885](https://github.com/containous/traefik/pull/1885) by [nmengin](https://github.com/nmengin)) +- **[acme]** Update docs for dnsimple env vars. ([#1872](https://github.com/containous/traefik/pull/1872) by [untalpierre](https://github.com/untalpierre)) +- **[api]** Add examples of proxying ping ([#2102](https://github.com/containous/traefik/pull/2102) by [deitch](https://github.com/deitch)) +- **[authentication,k8s]** traefik controller access to secrets ([#1707](https://github.com/containous/traefik/pull/1707) by [spinto](https://github.com/spinto)) +- **[consul,tls]** doc change regarding consul SSL ([#1774](https://github.com/containous/traefik/pull/1774) by [bitsofinfo](https://github.com/bitsofinfo)) +- **[consulcatalog,docker,ecs,k8s,marathon,rancher,sticky-session]** Stickiness documentation ([#2238](https://github.com/containous/traefik/pull/2238) by [ldez](https://github.com/ldez)) +- **[consul]** added consul acl token note ([#1720](https://github.com/containous/traefik/pull/1720) by [bitsofinfo](https://github.com/bitsofinfo)) +- **[docker]** Updating Docker output and curl for sticky sessions ([#2150](https://github.com/containous/traefik/pull/2150) by [jtyr](https://github.com/jtyr)) +- **[docker]** Add more visibility to docker stack deploy label issue ([#1984](https://github.com/containous/traefik/pull/1984) by [jmaitrehenry](https://github.com/jmaitrehenry)) +- **[ecs]** Fix IAM policy sid. ([#2066](https://github.com/containous/traefik/pull/2066) by [charlieoleary](https://github.com/charlieoleary)) +- **[k8s,marathon]** Mark Marathon and Kubernetes as constraint-supporting. ([#1964](https://github.com/containous/traefik/pull/1964) by [timoreimann](https://github.com/timoreimann)) +- **[k8s]** Add guide section on production advice, esp. CPU. ([#2113](https://github.com/containous/traefik/pull/2113) by [timoreimann](https://github.com/timoreimann)) +- **[k8s]** Document ways to partition Ingresses in the k8s guide. ([#2223](https://github.com/containous/traefik/pull/2223) by [timoreimann](https://github.com/timoreimann)) +- **[k8s]** Remove pod from RBAC rules. ([#2229](https://github.com/containous/traefik/pull/2229) by [timoreimann](https://github.com/timoreimann)) +- **[k8s]** Quote priority values in annotation examples. ([#2230](https://github.com/containous/traefik/pull/2230) by [timoreimann](https://github.com/timoreimann)) +- **[k8s]** Fix invalid service yaml example ([#2059](https://github.com/containous/traefik/pull/2059) by [kairen](https://github.com/kairen)) +- **[k8s]** Update usage of `.local` with `.minikube` in k8s docs ([#1551](https://github.com/containous/traefik/pull/1551) by [errm](https://github.com/errm)) +- **[k8s]** Update the documentation to use DaemonSet or Deployment ([#1735](https://github.com/containous/traefik/pull/1735) by [saschagrunert](https://github.com/saschagrunert)) +- **[k8s]** Fix docs about default namespaces. ([#1961](https://github.com/containous/traefik/pull/1961) by [timoreimann](https://github.com/timoreimann)) +- **[k8s]** Moved namespace to correct place ([#1911](https://github.com/containous/traefik/pull/1911) by [markround](https://github.com/markround)) +- **[k8s]** examples/k8s: fix ui ingress port out of sync with deployment ([#1943](https://github.com/containous/traefik/pull/1943) by [borancar](https://github.com/borancar)) +- **[k8s]** Add secrets resource to in-line RBAC spec. ([#1890](https://github.com/containous/traefik/pull/1890) by [timoreimann](https://github.com/timoreimann)) +- **[k8s]** Improve documentation. ([#1831](https://github.com/containous/traefik/pull/1831) by [timoreimann](https://github.com/timoreimann)) +- **[marathon]** Fix documentation glitches. ([#1996](https://github.com/containous/traefik/pull/1996) by [timoreimann](https://github.com/timoreimann)) +- **[metrics]** Enhance web backend documentation ([#2122](https://github.com/containous/traefik/pull/2122) by [ldez](https://github.com/ldez)) +- **[mesos]** fix: documentation Mesos. ([#2029](https://github.com/containous/traefik/pull/2029) by [ldez](https://github.com/ldez)) +- **[middleware]** Improve compression documentation ([#2184](https://github.com/containous/traefik/pull/2184) by [errm](https://github.com/errm)) +- **[provider]** Clarify that provider-enabling argument parameters set all defaults. ([#1830](https://github.com/containous/traefik/pull/1830) by [timoreimann](https://github.com/timoreimann)) +- **[rancher]** Update Rancher documentation. ([#1776](https://github.com/containous/traefik/pull/1776) by [ldez](https://github.com/ldez)) +- **[webui]** Document yarnpkg. ([#1558](https://github.com/containous/traefik/pull/1558) by [Stibbons](https://github.com/Stibbons)) +- Add forward auth documentation. ([#2110](https://github.com/containous/traefik/pull/2110) by [ldez](https://github.com/ldez)) +- User guide gRPC ([#2108](https://github.com/containous/traefik/pull/2108) by [Juliens](https://github.com/Juliens)) +- Document custom error page restrictions. ([#2104](https://github.com/containous/traefik/pull/2104) by [timoreimann](https://github.com/timoreimann)) +- Prepare release v1.4.0-rc3 ([#2135](https://github.com/containous/traefik/pull/2135) by [Juliens](https://github.com/Juliens)) +- Update gRPC example ([#2191](https://github.com/containous/traefik/pull/2191) by [jsenon](https://github.com/jsenon)) +- Prepare release v1.4.0-rc2 ([#2091](https://github.com/containous/traefik/pull/2091) by [ldez](https://github.com/ldez)) +- Fix grammar mistake in the kv-config docs ([#2197](https://github.com/containous/traefik/pull/2197) by [chr4](https://github.com/chr4)) +- Update cluster.md ([#2073](https://github.com/containous/traefik/pull/2073) by [kmbremner](https://github.com/kmbremner)) +- Prepare release v1.4.0-rc4 ([#2201](https://github.com/containous/traefik/pull/2201) by [nmengin](https://github.com/nmengin)) +- Prepare release v1.4.0-rc5 ([#2241](https://github.com/containous/traefik/pull/2241) by [ldez](https://github.com/ldez)) +- Enhance documentation. ([#2048](https://github.com/containous/traefik/pull/2048) by [ldez](https://github.com/ldez)) +- doc: add notes on server urls with path ([#2045](https://github.com/containous/traefik/pull/2045) by [chulkilee](https://github.com/chulkilee)) +- Enhance security headers doc. ([#2042](https://github.com/containous/traefik/pull/2042) by [ldez](https://github.com/ldez)) +- HTTPS for images, video and links in docs. ([#2041](https://github.com/containous/traefik/pull/2041) by [ldez](https://github.com/ldez)) +- Fix error pages configuration. ([#2038](https://github.com/containous/traefik/pull/2038) by [ldez](https://github.com/ldez)) +- Fix Proxy Protocol documentation ([#2253](https://github.com/containous/traefik/pull/2253) by [emilevauge](https://github.com/emilevauge)) +- Update GraceTimeOut documentation ([#1875](https://github.com/containous/traefik/pull/1875) by [marco-jantke](https://github.com/marco-jantke)) +- Release cycle. ([#1812](https://github.com/containous/traefik/pull/1812) by [ldez](https://github.com/ldez)) +- Update contributing guide build steps ([#1801](https://github.com/containous/traefik/pull/1801) by [jsturtevant](https://github.com/jsturtevant)) +- Add Nicolas Mengin to maintainers ([#1792](https://github.com/containous/traefik/pull/1792) by [emilevauge](https://github.com/emilevauge)) +- Add Julien Salleyron to maintainers ([#1790](https://github.com/containous/traefik/pull/1790) by [emilevauge](https://github.com/emilevauge)) +- Change to a more flexible PR review process ([#1781](https://github.com/containous/traefik/pull/1781) by [emilevauge](https://github.com/emilevauge)) +- Traefik "bug" command documentation ([#1811](https://github.com/containous/traefik/pull/1811) by [ldez](https://github.com/ldez)) +- Change Traefik intro video ([#1893](https://github.com/containous/traefik/pull/1893) by [emilevauge](https://github.com/emilevauge)) +- Prepare release v1.4.0-rc1 ([#2021](https://github.com/containous/traefik/pull/2021) by [ldez](https://github.com/ldez)) +- Add play-with-docker example ([#1726](https://github.com/containous/traefik/pull/1726) by [marcosnils](https://github.com/marcosnils)) +- Add Marco Jantke to maintainers ([#1980](https://github.com/containous/traefik/pull/1980) by [emilevauge](https://github.com/emilevauge)) +- Remove Russel from maintainers ([#1614](https://github.com/containous/traefik/pull/1614) by [emilevauge](https://github.com/emilevauge)) +- Update CONTRIBUTING.md. ([#1667](https://github.com/containous/traefik/pull/1667) by [timoreimann](https://github.com/timoreimann)) +- drop "slave" wording for "worker" ([#1645](https://github.com/containous/traefik/pull/1645) by [djalal](https://github.com/djalal)) +- Use more inclusive language in README.md {guys => folks} ([#1640](https://github.com/containous/traefik/pull/1640) by [igorwwwwwwwwwwwwwwwwwwww](https://github.com/igorwwwwwwwwwwwwwwwwwwww)) +- Remove Thomas Recloux from maintainers ([#1616](https://github.com/containous/traefik/pull/1616) by [emilevauge](https://github.com/emilevauge)) +- Update documentation for 1.4 release ([#2011](https://github.com/containous/traefik/pull/2011) by [emilevauge](https://github.com/emilevauge)) +- Small toml documentation update ([#1603](https://github.com/containous/traefik/pull/1603) by [antoine-aumjaud](https://github.com/antoine-aumjaud)) +- Add @ldez to maintainers ([#1589](https://github.com/containous/traefik/pull/1589) by [emilevauge](https://github.com/emilevauge)) +- doc: add labels documentation. ([#1582](https://github.com/containous/traefik/pull/1582) by [ldez](https://github.com/ldez)) +- Update golang version in contributing guide ([#2018](https://github.com/containous/traefik/pull/2018) by [ArikaChen](https://github.com/ArikaChen)) +- toml page - replace li by table ([#1995](https://github.com/containous/traefik/pull/1995) by [jmaitrehenry](https://github.com/jmaitrehenry)) + +**Misc:** +- Merge v1.3.7 ([#2013](https://github.com/containous/traefik/pull/2013) by [ldez](https://github.com/ldez)) +- Merge 1.3.6 ([#1992](https://github.com/containous/traefik/pull/1992) by [ldez](https://github.com/ldez)) +- Merge 1.3.5 ([#1909](https://github.com/containous/traefik/pull/1909) by [ldez](https://github.com/ldez)) +- Merge 1.3.3 ([#1836](https://github.com/containous/traefik/pull/1836) by [ldez](https://github.com/ldez)) +- Merge v1.3.2 to master ([#1809](https://github.com/containous/traefik/pull/1809) by [ldez](https://github.com/ldez)) +- Merge current v1.3 ([#1797](https://github.com/containous/traefik/pull/1797) by [ldez](https://github.com/ldez)) +- Merge current v1.3 ([#1786](https://github.com/containous/traefik/pull/1786) by [ldez](https://github.com/ldez)) +- Merge v1.3.1 to master ([#1763](https://github.com/containous/traefik/pull/1763) by [ldez](https://github.com/ldez)) +- Merge current v1.3 ([#1753](https://github.com/containous/traefik/pull/1753) by [ldez](https://github.com/ldez)) +- Merge current v1.3 ([#1705](https://github.com/containous/traefik/pull/1705) by [ldez](https://github.com/ldez)) +- Merge current v1.3 to master ([#1697](https://github.com/containous/traefik/pull/1697) by [ldez](https://github.com/ldez)) +- Merge v1 3 0 ([#1692](https://github.com/containous/traefik/pull/1692) by [ldez](https://github.com/ldez)) +- Merge current v1.3 to master (rc3) ([#1666](https://github.com/containous/traefik/pull/1666) by [ldez](https://github.com/ldez)) +- Merge current v1.3 to master ([#1643](https://github.com/containous/traefik/pull/1643) by [ldez](https://github.com/ldez)) +- Merge v1.3.0-rc2 master ([#1613](https://github.com/containous/traefik/pull/1613) by [emilevauge](https://github.com/emilevauge)) +- Merge v1.3 branch into master [2017-05-11] ([#1548](https://github.com/containous/traefik/pull/1548) by [timoreimann](https://github.com/timoreimann)) + ## [v1.4.0-rc5](https://github.com/containous/traefik/tree/v1.4.0-rc5) (2017-10-10) [All Commits](https://github.com/containous/traefik/compare/v1.4.0-rc4...v1.4.0-rc5) From 9c8df8b9cefd7972d66c33c9a8cba05e07768b27 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 16 Oct 2017 19:44:02 +0200 Subject: [PATCH 13/13] Fix 1.4.0 release date --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1803bb329..837a06fc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log -## [v1.4.0](https://github.com/containous/traefik/tree/v1.4.0) (2017-05-31) +## [v1.4.0](https://github.com/containous/traefik/tree/v1.4.0) (2017-10-16) [All Commits](https://github.com/containous/traefik/compare/v1.3.0-rc1...v1.4.0) **Enhancements:**