From 70812c70fcb77148709753ed4556bb774f41acf1 Mon Sep 17 00:00:00 2001 From: Bernhard Millauer Date: Fri, 3 Nov 2017 17:02:14 +0100 Subject: [PATCH 01/11] Postfix windows binaries with .exe --- script/crossbinary-default | 8 ++++++-- script/crossbinary-others | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/script/crossbinary-default b/script/crossbinary-default index 83a197438..13e3b303d 100755 --- a/script/crossbinary-default +++ b/script/crossbinary-default @@ -24,13 +24,17 @@ GIT_REPO_URL='github.com/containous/traefik/version' GO_BUILD_CMD="go build -ldflags" GO_BUILD_OPT="-s -w -X ${GIT_REPO_URL}.Version=${VERSION} -X ${GIT_REPO_URL}.Codename=${CODENAME} -X ${GIT_REPO_URL}.BuildDate=${DATE}" -# Build 386 amd64 binaries +# Build amd64 binaries OS_PLATFORM_ARG=(linux windows darwin) OS_ARCH_ARG=(amd64) for OS in ${OS_PLATFORM_ARG[@]}; do + BIN_EXT='' + if [ "$OS" == "windows" ]; then + BIN_EXT='.exe' + fi for ARCH in ${OS_ARCH_ARG[@]}; do echo "Building binary for ${OS}/${ARCH}..." - GOARCH=${ARCH} GOOS=${OS} CGO_ENABLED=0 ${GO_BUILD_CMD} "${GO_BUILD_OPT}" -o "dist/traefik_${OS}-${ARCH}" ./cmd/traefik/ + GOARCH=${ARCH} GOOS=${OS} CGO_ENABLED=0 ${GO_BUILD_CMD} "${GO_BUILD_OPT}" -o "dist/traefik_${OS}-${ARCH}${BIN_EXT}" ./cmd/traefik/ done done diff --git a/script/crossbinary-others b/script/crossbinary-others index 51d9a01b2..d0c59eb15 100755 --- a/script/crossbinary-others +++ b/script/crossbinary-others @@ -28,9 +28,13 @@ GO_BUILD_OPT="-s -w -X ${GIT_REPO_URL}.Version=${VERSION} -X ${GIT_REPO_URL}.Cod OS_PLATFORM_ARG=(linux windows darwin) OS_ARCH_ARG=(386) for OS in ${OS_PLATFORM_ARG[@]}; do + BIN_EXT='' + if [ "$OS" == "windows" ]; then + BIN_EXT='.exe' + fi for ARCH in ${OS_ARCH_ARG[@]}; do - echo "Building binary for $OS/$ARCH..." - GOARCH=${ARCH} GOOS=${OS} CGO_ENABLED=0 ${GO_BUILD_CMD} "$GO_BUILD_OPT" -o "dist/traefik_$OS-$ARCH" ./cmd/traefik/ + echo "Building binary for ${OS}/${ARCH}..." + GOARCH=${ARCH} GOOS=${OS} CGO_ENABLED=0 ${GO_BUILD_CMD} "${GO_BUILD_OPT}" -o "dist/traefik_${OS}-${ARCH}${BIN_EXT}" ./cmd/traefik/ done done From bc8d68bd318e18a39b0e1c9a975091450617edf0 Mon Sep 17 00:00:00 2001 From: Tom Saleeba Date: Tue, 7 Nov 2017 21:50:03 +1100 Subject: [PATCH 02/11] docs: fix some typos --- docs/user-guide/examples.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/user-guide/examples.md b/docs/user-guide/examples.md index 07128cdc8..7f9b19ff5 100644 --- a/docs/user-guide/examples.md +++ b/docs/user-guide/examples.md @@ -137,7 +137,7 @@ This configuration allows generating a Let's Encrypt certificate during the firs * TLS handshakes will be slow when requesting a hostname certificate for the first time, this can leads to DDoS attacks. * Let's Encrypt have rate limiting: https://letsencrypt.org/docs/rate-limits - That's why, it's better to use the `onHostRule` optin if possible. + That's why, it's better to use the `onHostRule` option if possible. ### DNS challenge @@ -170,7 +170,7 @@ entryPoint = "https" DNS challenge needs environment variables to be executed. This variables have to be set on the machine/container which host Traefik. -These variables has described [in this section](/configuration/acme/#dnsprovider). +These variables are described [in this section](/configuration/acme/#dnsprovider). ### OnHostRule option and provided certificates @@ -198,7 +198,7 @@ Traefik will only try to generate a Let's encrypt certificate if the domain cann #### Prerequisites -Before to use Let's Encrypt in a Traefik cluster, take a look to [the key-value store explanations](/user-guide/kv-config) and more precisely to [this section](/user-guide/kv-config/#store-configuration-in-key-value-store) in the way to know how to migrate from a acme local storage *(acme.json file)* to a key-value store configuration. +Before you use Let's Encrypt in a Traefik cluster, take a look to [the key-value store explanations](/user-guide/kv-config) and more precisely at [this section](/user-guide/kv-config/#store-configuration-in-key-value-store), which will describe how to migrate from a acme local storage *(acme.json file)* to a key-value store configuration. #### Configuration From 58a438167bf5e46a5f2309d8226db131e3894d24 Mon Sep 17 00:00:00 2001 From: Jan Collijs Date: Wed, 8 Nov 2017 15:12:03 +0100 Subject: [PATCH 03/11] Minor fix for docker volume vs created directory --- docs/user-guide/docker-and-lets-encrypt.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/user-guide/docker-and-lets-encrypt.md b/docs/user-guide/docker-and-lets-encrypt.md index baec79b72..d1f61e71f 100644 --- a/docs/user-guide/docker-and-lets-encrypt.md +++ b/docs/user-guide/docker-and-lets-encrypt.md @@ -59,8 +59,8 @@ services: - web volumes: - /var/run/docker.sock:/var/run/docker.sock - - /srv/traefik/traefik.toml:/traefik.toml - - /srv/traefik/acme.json:/acme.json + - /opt/traefik/traefik.toml:/traefik.toml + - /opt/traefik/acme.json:/acme.json container_name: traefik networks: From 9bd0fff31975dd856836855770862ac18823e0f2 Mon Sep 17 00:00:00 2001 From: SALLEYRON Julien Date: Thu, 9 Nov 2017 00:48:03 +0100 Subject: [PATCH 04/11] Keep status when stream mode and compress --- .../anonymize/anonymize_doOnJSON_test.go | 3 ++- glide.lock | 2 +- middlewares/compress_test.go | 25 +++++++++++++++++++ middlewares/headers.go | 3 ++- middlewares/headers_test.go | 5 ++-- vendor/github.com/NYTimes/gziphandler/gzip.go | 8 +++++- 6 files changed, 40 insertions(+), 6 deletions(-) diff --git a/cmd/traefik/anonymize/anonymize_doOnJSON_test.go b/cmd/traefik/anonymize/anonymize_doOnJSON_test.go index 87f43d125..91ebbf05f 100644 --- a/cmd/traefik/anonymize/anonymize_doOnJSON_test.go +++ b/cmd/traefik/anonymize/anonymize_doOnJSON_test.go @@ -1,8 +1,9 @@ package anonymize import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func Test_doOnJSON(t *testing.T) { diff --git a/glide.lock b/glide.lock index 806ea99f0..560b517e4 100644 --- a/glide.lock +++ b/glide.lock @@ -383,7 +383,7 @@ imports: repo: https://github.com/ijc25/Gotty.git vcs: git - name: github.com/NYTimes/gziphandler - version: 97ae7fbaf81620fe97840685304a78a306a39c64 + version: 0f67f3f25d3b17590ee0ab93fcb216363ee30967 - name: github.com/ogier/pflag version: 45c278ab3607870051a2ea9040bb85fcb8557481 - name: github.com/opencontainers/go-digest diff --git a/middlewares/compress_test.go b/middlewares/compress_test.go index 19689dd96..605349e50 100644 --- a/middlewares/compress_test.go +++ b/middlewares/compress_test.go @@ -137,6 +137,31 @@ func TestIntegrationShouldNotCompress(t *testing.T) { } } +func TestShouldWriteHeaderWhenFlush(t *testing.T) { + comp := &Compress{} + negro := negroni.New(comp) + negro.UseHandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Header().Add(contentEncodingHeader, gzipValue) + rw.Header().Add(varyHeader, acceptEncodingHeader) + rw.WriteHeader(http.StatusUnauthorized) + rw.(http.Flusher).Flush() + rw.Write([]byte("short")) + }) + ts := httptest.NewServer(negro) + defer ts.Close() + + req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil) + req.Header.Add(acceptEncodingHeader, gzipValue) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + + assert.Equal(t, gzipValue, resp.Header.Get(contentEncodingHeader)) + assert.Equal(t, acceptEncodingHeader, resp.Header.Get(varyHeader)) +} + func TestIntegrationShouldCompress(t *testing.T) { fakeBody := generateBytes(100000) diff --git a/middlewares/headers.go b/middlewares/headers.go index ee2368949..de4a251fb 100644 --- a/middlewares/headers.go +++ b/middlewares/headers.go @@ -3,8 +3,9 @@ package middlewares //Middleware based on https://github.com/unrolled/secure import ( - "github.com/containous/traefik/types" "net/http" + + "github.com/containous/traefik/types" ) // HeaderOptions is a struct for specifying configuration options for the headers middleware. diff --git a/middlewares/headers_test.go b/middlewares/headers_test.go index 54dc1e4de..a78a0c2ca 100644 --- a/middlewares/headers_test.go +++ b/middlewares/headers_test.go @@ -3,11 +3,12 @@ package middlewares //Middleware tests based on https://github.com/unrolled/secure import ( - "github.com/containous/traefik/testhelpers" - "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "testing" + + "github.com/containous/traefik/testhelpers" + "github.com/stretchr/testify/assert" ) var myHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/vendor/github.com/NYTimes/gziphandler/gzip.go b/vendor/github.com/NYTimes/gziphandler/gzip.go index b6af9115a..91130c424 100644 --- a/vendor/github.com/NYTimes/gziphandler/gzip.go +++ b/vendor/github.com/NYTimes/gziphandler/gzip.go @@ -82,6 +82,7 @@ type GzipResponseWriter struct { buf []byte // Holds the first part of the write before reaching the minSize or the end of the write. contentTypes []string // Only compress if the response is one of these content-types. All are accepted if empty. + flushed bool // Indicate if the stream was already flushed } // Write appends data to the gzip writer. @@ -167,7 +168,8 @@ func (w *GzipResponseWriter) init() { func (w *GzipResponseWriter) Close() error { if w.gw == nil { // Gzip not trigged yet, write out regular response. - if w.code != 0 { + // WriteHeader only if it wasn't already wrote by a Flush + if !w.flushed && w.code != 0 { w.ResponseWriter.WriteHeader(w.code) } if w.buf != nil { @@ -195,7 +197,11 @@ func (w *GzipResponseWriter) Flush() { } if fw, ok := w.ResponseWriter.(http.Flusher); ok { + if !w.flushed && w.code != 0 { + w.ResponseWriter.WriteHeader(w.code) + } fw.Flush() + w.flushed = true } } From 56affb90ae55805d1f8b975dd82e468512fc2c07 Mon Sep 17 00:00:00 2001 From: Levi Blaney Date: Thu, 9 Nov 2017 04:52:03 -0500 Subject: [PATCH 05/11] Add secret creation to docs for kubernetes backend --- docs/configuration/backends/kubernetes.md | 4 +- docs/user-guide/kubernetes.md | 68 ++++++++++++++++++++++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/docs/configuration/backends/kubernetes.md b/docs/configuration/backends/kubernetes.md index 2b95025e2..e37f50458 100644 --- a/docs/configuration/backends/kubernetes.md +++ b/docs/configuration/backends/kubernetes.md @@ -118,10 +118,10 @@ If one of the Net-Specifications are invalid, the whole list is invalid and allo ### Authentication Is possible to add additional authentication annotations in the Ingress rule. -The source of the authentication is a secret that contains usernames and passwords inside the the key auth. +The source of the authentication is a secret that contains usernames and passwords inside the key auth. - `ingress.kubernetes.io/auth-type`: `basic` -- `ingress.kubernetes.io/auth-secret` +- `ingress.kubernetes.io/auth-secret`: `mysecret` Contains the usernames and passwords with access to the paths defined in the Ingress Rule. The secret must be created in the same namespace as the Ingress rule. diff --git a/docs/user-guide/kubernetes.md b/docs/user-guide/kubernetes.md index 6ca3d1a72..77249babd 100644 --- a/docs/user-guide/kubernetes.md +++ b/docs/user-guide/kubernetes.md @@ -79,7 +79,7 @@ It is possible to use Træfik with a [Deployment](https://kubernetes.io/docs/con The Deployment objects looks like this: -```yml +```yaml --- apiVersion: v1 kind: ServiceAccount @@ -327,6 +327,72 @@ echo "$(minikube ip) traefik-ui.minikube" | sudo tee -a /etc/hosts We should now be able to visit [traefik-ui.minikube](http://traefik-ui.minikube) in the browser and view the Træfik Web UI. +## Basic Authentication + +It's possible to add additional authentication annotations in the Ingress rule. +The source of the authentication is a secret that contains usernames and passwords inside the key auth. +To read about basic auth limitations see the [Kubernetes Ingress](/configuration/backends/kubernetes) configuration page. + +#### Creating the Secret + +A. Use `htpasswd` to create a file containing the username and the base64-encoded password: + +```shell +htpasswd -c ./auth myusername +``` + +You will be prompted for a password which you will have to enter twice. +`htpasswd` will create a file with the following: + +```shell +cat auth +``` +``` +myusername:$apr1$78Jyn/1K$ERHKVRPPlzAX8eBtLuvRZ0 +``` + +B. Now use `kubectl` to create a secret in the monitoring namespace using the file created by `htpasswd`. + +```shell +kubectl create secret generic mysecret --from-file auth --namespace=monitoring +``` + +!!! note + Secret must be in same namespace as the ingress rule. + +C. Create the ingress using the following annotations to specify basic auth and that the username and password is stored in `mysecret`. + +- `ingress.kubernetes.io/auth-type: "basic"` +- `ingress.kubernetes.io/auth-secret: "mysecret"` + +Following is a full ingress example based on Prometheus: + +```yaml +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: prometheus-dashboard + namespace: monitoring + annotations: + kubernetes.io/ingress.class: traefik + ingress.kubernetes.io/auth-type: "basic" + ingress.kubernetes.io/auth-secret: "mysecret" +spec: + rules: + - host: dashboard.prometheus.example.com + http: + paths: + - backend: + serviceName: prometheus + servicePort: 9090 +``` + +You can apply the example ingress as following: + +```shell +kubectl create -f prometheus-ingress.yaml -n monitoring +``` + ## Name based routing In this example we are going to setup websites for 3 of the United Kingdoms best loved cheeses, Cheddar, Stilton and Wensleydale. From 2f62ec3632a61dd6f23b89d061616250252ba6b8 Mon Sep 17 00:00:00 2001 From: Ivan Rogov Date: Thu, 9 Nov 2017 17:54:04 +0300 Subject: [PATCH 06/11] Link corrected --- docs/configuration/backends/kubernetes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/backends/kubernetes.md b/docs/configuration/backends/kubernetes.md index e37f50458..d7369ae8f 100644 --- a/docs/configuration/backends/kubernetes.md +++ b/docs/configuration/backends/kubernetes.md @@ -2,7 +2,7 @@ Træfik can be configured to use Kubernetes Ingress as a backend configuration. -See also [Kubernetes user guide](/docs/user-guide/kubernetes). +See also [Kubernetes user guide](/user-guide/kubernetes). ## Configuration From 5c119fe2d66d14d64e974b3b8c00757f4c876364 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Fri, 10 Nov 2017 14:12:02 +0100 Subject: [PATCH 07/11] Exclude GRPC from compress --- middlewares/compress.go | 8 +++++++- middlewares/compress_test.go | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/middlewares/compress.go b/middlewares/compress.go index 810f58904..4f1fb751d 100644 --- a/middlewares/compress.go +++ b/middlewares/compress.go @@ -3,6 +3,7 @@ package middlewares import ( "compress/gzip" "net/http" + "strings" "github.com/NYTimes/gziphandler" "github.com/containous/traefik/log" @@ -13,7 +14,12 @@ type Compress struct{} // ServerHTTP is a function used by Negroni func (c *Compress) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - gzipHandler(next).ServeHTTP(rw, r) + contentType := r.Header.Get("Content-Type") + if strings.HasPrefix(contentType, "application/grpc") { + next.ServeHTTP(rw, r) + } else { + gzipHandler(next).ServeHTTP(rw, r) + } } func gzipHandler(h http.Handler) http.Handler { diff --git a/middlewares/compress_test.go b/middlewares/compress_test.go index 605349e50..743a02dba 100644 --- a/middlewares/compress_test.go +++ b/middlewares/compress_test.go @@ -16,6 +16,7 @@ import ( const ( acceptEncodingHeader = "Accept-Encoding" contentEncodingHeader = "Content-Encoding" + contentTypeHeader = "Content-Type" varyHeader = "Vary" gzipValue = "gzip" ) @@ -81,6 +82,26 @@ func TestShouldNotCompressWhenNoAcceptEncodingHeader(t *testing.T) { assert.EqualValues(t, rw.Body.Bytes(), fakeBody) } +func TestShouldNotCompressWhenGRPC(t *testing.T) { + handler := &Compress{} + + req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil) + req.Header.Add(acceptEncodingHeader, gzipValue) + req.Header.Add(contentTypeHeader, "application/grpc") + + baseBody := generateBytes(gziphandler.DefaultMinSize) + next := func(rw http.ResponseWriter, r *http.Request) { + rw.Write(baseBody) + } + + rw := httptest.NewRecorder() + handler.ServeHTTP(rw, req, next) + + assert.Empty(t, rw.Header().Get(acceptEncodingHeader)) + assert.Empty(t, rw.Header().Get(contentEncodingHeader)) + assert.EqualValues(t, rw.Body.Bytes(), baseBody) +} + func TestIntegrationShouldNotCompress(t *testing.T) { fakeCompressedBody := generateBytes(100000) comp := &Compress{} From 5ee2cae85c4aa7bb66ce3ff0561bea4245b9719c Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 13 Nov 2017 12:14:02 +0100 Subject: [PATCH 08/11] Fix Traefik reload if Consul Catalog tags change --- integration/consul_catalog_test.go | 78 ++- .../resources/compose/consul_catalog.yml | 6 +- provider/consul/consul_catalog.go | 45 +- provider/consul/consul_catalog_test.go | 503 ++++++++++++++---- 4 files changed, 508 insertions(+), 124 deletions(-) diff --git a/integration/consul_catalog_test.go b/integration/consul_catalog_test.go index fdd132122..0241d8ab3 100644 --- a/integration/consul_catalog_test.go +++ b/integration/consul_catalog_test.go @@ -36,7 +36,7 @@ func (s *ConsulCatalogSuite) SetUpSuite(c *check.C) { } func (s *ConsulCatalogSuite) waitToElectConsulLeader() error { - return try.Do(3*time.Second, func() error { + return try.Do(15*time.Second, func() error { leader, err := s.consulClient.Status().Leader() if err != nil || len(leader) == 0 { @@ -344,8 +344,82 @@ func (s *ConsulCatalogSuite) TestBasicAuthSimpleService(c *check.C) { c.Assert(err, checker.IsNil) } -func (s *ConsulCatalogSuite) TestRetryWithConsulServer(c *check.C) { +func (s *ConsulCatalogSuite) TestRefreshConfigTagChange(c *check.C) { + cmd, display := s.traefikCmd( + withConfigFile("fixtures/consul_catalog/simple.toml"), + "--consulCatalog", + "--consulCatalog.exposedByDefault=false", + "--consulCatalog.watch=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() + nginx := s.composeProject.Container(c, "nginx1") + + err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{"name=nginx1", "traefik.enable=false", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"}) + c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) + defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) + + err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 5*time.Second, try.BodyContains("nginx1")) + c.Assert(err, checker.NotNil) + + err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{"name=nginx1", "traefik.enable=true", "traefik.backend.circuitbreaker=ResponseCodeRatio(500, 600, 0, 600) > 0.5"}) + c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) + + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) + req.Host = "test.consul.localhost" + + err = try.Request(req, 20*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) + c.Assert(err, checker.IsNil) + + err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, try.BodyContains("nginx1")) + c.Assert(err, checker.IsNil) +} + +func (s *ConsulCatalogSuite) TestCircuitBreaker(c *check.C) { + cmd, display := s.traefikCmd( + withConfigFile("fixtures/consul_catalog/simple.toml"), + "--retry", + "--retry.attempts=1", + "--forwardingTimeouts.dialTimeout=5s", + "--forwardingTimeouts.responseHeaderTimeout=10s", + "--consulCatalog", + "--consulCatalog.exposedByDefault=false", + "--consulCatalog.watch=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() + + nginx := s.composeProject.Container(c, "nginx1") + nginx2 := s.composeProject.Container(c, "nginx2") + nginx3 := s.composeProject.Container(c, "nginx3") + + err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{"name=nginx1", "traefik.enable=true", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"}) + c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) + defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) + err = s.registerService("test", nginx2.NetworkSettings.IPAddress, 42, []string{"name=nginx2", "traefik.enable=true", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"}) + c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) + defer s.deregisterService("test", nginx2.NetworkSettings.IPAddress) + err = s.registerService("test", nginx3.NetworkSettings.IPAddress, 42, []string{"name=nginx3", "traefik.enable=true", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"}) + c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) + defer s.deregisterService("test", nginx3.NetworkSettings.IPAddress) + + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) + req.Host = "test.consul.localhost" + + err = try.Request(req, 20*time.Second, try.StatusCodeIs(http.StatusServiceUnavailable), 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) diff --git a/integration/resources/compose/consul_catalog.yml b/integration/resources/compose/consul_catalog.yml index b755d2ad5..9569a32c4 100644 --- a/integration/resources/compose/consul_catalog.yml +++ b/integration/resources/compose/consul_catalog.yml @@ -1,6 +1,6 @@ consul: - image: progrium/consul - command: -server -bootstrap -log-level debug -ui-dir /ui + image: consul + command: agent -server -bootstrap-expect 1 -client 0.0.0.0 -log-level debug -ui ports: - "8400:8400" - "8500:8500" @@ -15,3 +15,5 @@ nginx1: image: nginx:alpine nginx2: image: nginx:alpine +nginx3: + image: nginx:alpine diff --git a/provider/consul/consul_catalog.go b/provider/consul/consul_catalog.go index db9c7bbc2..8387e72fa 100644 --- a/provider/consul/consul_catalog.go +++ b/provider/consul/consul_catalog.go @@ -77,9 +77,14 @@ func (a nodeSorter) Less(i int, j int) bool { return lentr.Service.Port < rentr.Service.Port } -func getChangedServiceKeys(currState map[string]Service, prevState map[string]Service) ([]string, []string) { - currKeySet := fun.Set(fun.Keys(currState).([]string)).(map[string]bool) - prevKeySet := fun.Set(fun.Keys(prevState).([]string)).(map[string]bool) +func hasChanged(current map[string]Service, previous map[string]Service) bool { + addedServiceKeys, removedServiceKeys := getChangedServiceKeys(current, previous) + return len(removedServiceKeys) > 0 || len(addedServiceKeys) > 0 || hasNodeOrTagsChanged(current, previous) +} + +func getChangedServiceKeys(current map[string]Service, previous map[string]Service) ([]string, []string) { + currKeySet := fun.Set(fun.Keys(current).([]string)).(map[string]bool) + prevKeySet := fun.Set(fun.Keys(previous).([]string)).(map[string]bool) addedKeys := fun.Difference(currKeySet, prevKeySet).(map[string]bool) removedKeys := fun.Difference(prevKeySet, currKeySet).(map[string]bool) @@ -87,20 +92,23 @@ func getChangedServiceKeys(currState map[string]Service, prevState map[string]Se return fun.Keys(addedKeys).([]string), fun.Keys(removedKeys).([]string) } -func getChangedServiceNodeKeys(currState map[string]Service, prevState map[string]Service) ([]string, []string) { - var addedNodeKeys []string - var removedNodeKeys []string - for key, value := range currState { - if prevValue, ok := prevState[key]; ok { - addedKeys, removedKeys := getChangedHealthyKeys(value.Nodes, prevValue.Nodes) - addedNodeKeys = append(addedKeys) - removedNodeKeys = append(removedKeys) +func hasNodeOrTagsChanged(current map[string]Service, previous map[string]Service) bool { + var added []string + var removed []string + for key, value := range current { + if prevValue, ok := previous[key]; ok { + addedNodesKeys, removedNodesKeys := getChangedStringKeys(value.Nodes, prevValue.Nodes) + added = append(added, addedNodesKeys...) + removed = append(removed, removedNodesKeys...) + addedTagsKeys, removedTagsKeys := getChangedStringKeys(value.Tags, prevValue.Tags) + added = append(added, addedTagsKeys...) + removed = append(removed, removedTagsKeys...) } } - return addedNodeKeys, removedNodeKeys + return len(added) > 0 || len(removed) > 0 } -func getChangedHealthyKeys(currState []string, prevState []string) ([]string, []string) { +func getChangedStringKeys(currState []string, prevState []string) ([]string, []string) { currKeySet := fun.Set(currState).(map[string]bool) prevKeySet := fun.Set(prevState).(map[string]bool) @@ -163,7 +171,7 @@ func (p *CatalogProvider) watchHealthState(stopCh <-chan struct{}, watchCh chan< // A critical note is that the return of a blocking request is no guarantee of a change. // It is possible that there was an idempotent write that does not affect the result of the query. // Thus it is required to do extra check for changes... - addedKeys, removedKeys := getChangedHealthyKeys(current, flashback) + addedKeys, removedKeys := getChangedStringKeys(current, flashback) if len(addedKeys) > 0 { log.WithField("DiscoveredServices", addedKeys).Debug("Health State change detected.") @@ -242,12 +250,7 @@ func (p *CatalogProvider) watchCatalogServices(stopCh <-chan struct{}, watchCh c // A critical note is that the return of a blocking request is no guarantee of a change. // It is possible that there was an idempotent write that does not affect the result of the query. // Thus it is required to do extra check for changes... - addedServiceKeys, removedServiceKeys := getChangedServiceKeys(current, flashback) - - addedServiceNodeKeys, removedServiceNodeKeys := getChangedServiceNodeKeys(current, flashback) - - if len(removedServiceKeys) > 0 || len(removedServiceNodeKeys) > 0 || len(addedServiceKeys) > 0 || len(addedServiceNodeKeys) > 0 { - log.WithField("MissingServices", removedServiceKeys).WithField("DiscoveredServices", addedServiceKeys).Debug("Catalog Services change detected.") + if hasChanged(current, flashback) { watchCh <- data flashback = current } @@ -255,6 +258,7 @@ func (p *CatalogProvider) watchCatalogServices(stopCh <-chan struct{}, watchCh c } }) } + func getServiceIds(services []*api.CatalogService) []string { var serviceIds []string for _, service := range services { @@ -271,7 +275,6 @@ func (p *CatalogProvider) healthyNodes(service string) (catalogUpdate, error) { log.WithError(err).Errorf("Failed to fetch details of %s", service) return catalogUpdate{}, err } - nodes := fun.Filter(func(node *api.ServiceEntry) bool { return p.nodeFilter(service, node) }, data).([]*api.ServiceEntry) diff --git a/provider/consul/consul_catalog_test.go b/provider/consul/consul_catalog_test.go index 8ffdd3e0d..3e6899f80 100644 --- a/provider/consul/consul_catalog_test.go +++ b/provider/consul/consul_catalog_test.go @@ -1,7 +1,6 @@ package consul import ( - "reflect" "sort" "testing" "text/template" @@ -21,11 +20,13 @@ func TestConsulCatalogGetFrontendRule(t *testing.T) { } provider.setupFrontEndTemplate() - services := []struct { + testCases := []struct { + desc string service serviceUpdate expected string }{ { + desc: "Should return default host foo.localhost", service: serviceUpdate{ ServiceName: "foo", Attributes: []string{}, @@ -33,6 +34,7 @@ func TestConsulCatalogGetFrontendRule(t *testing.T) { expected: "Host:foo.localhost", }, { + desc: "Should return host *.example.com", service: serviceUpdate{ ServiceName: "foo", Attributes: []string{ @@ -42,6 +44,7 @@ func TestConsulCatalogGetFrontendRule(t *testing.T) { expected: "Host:*.example.com", }, { + desc: "Should return host foo.example.com", service: serviceUpdate{ ServiceName: "foo", Attributes: []string{ @@ -51,6 +54,7 @@ func TestConsulCatalogGetFrontendRule(t *testing.T) { expected: "Host:foo.example.com", }, { + desc: "Should return path prefix /bar", service: serviceUpdate{ ServiceName: "foo", Attributes: []string{ @@ -62,11 +66,14 @@ func TestConsulCatalogGetFrontendRule(t *testing.T) { }, } - for _, e := range services { - actual := provider.getFrontendRule(e.service) - if actual != e.expected { - t.Fatalf("expected %s, got %s", e.expected, actual) - } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := provider.getFrontendRule(test.service) + assert.Equal(t, test.expected, actual) + }) } } @@ -76,13 +83,15 @@ func TestConsulCatalogGetTag(t *testing.T) { Prefix: "traefik", } - services := []struct { + testCases := []struct { + desc string tags []string key string defaultValue string expected string }{ { + desc: "Should return value of foo.bar key", tags: []string{ "foo.bar=random", "traefik.backend.weight=42", @@ -94,21 +103,17 @@ func TestConsulCatalogGetTag(t *testing.T) { }, } - actual := provider.hasTag("management", []string{"management"}) - if !actual { - t.Fatalf("expected %v, got %v", true, actual) - } + assert.Equal(t, true, provider.hasTag("management", []string{"management"})) + assert.Equal(t, true, provider.hasTag("management", []string{"management=yes"})) - actual = provider.hasTag("management", []string{"management=yes"}) - if !actual { - t.Fatalf("expected %v, got %v", true, actual) - } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() - for _, e := range services { - actual := provider.getTag(e.key, e.tags, e.defaultValue) - if actual != e.expected { - t.Fatalf("expected %s, got %s", e.expected, actual) - } + actual := provider.getTag(test.key, test.tags, test.defaultValue) + assert.Equal(t, test.expected, actual) + }) } } @@ -118,13 +123,15 @@ func TestConsulCatalogGetAttribute(t *testing.T) { Prefix: "traefik", } - services := []struct { + testCases := []struct { + desc string tags []string key string defaultValue string expected string }{ { + desc: "Should return tag value 42", tags: []string{ "foo.bar=ramdom", "traefik.backend.weight=42", @@ -134,6 +141,7 @@ func TestConsulCatalogGetAttribute(t *testing.T) { expected: "42", }, { + desc: "Should return tag default value 0", tags: []string{ "foo.bar=ramdom", "traefik.backend.wei=42", @@ -144,17 +152,16 @@ func TestConsulCatalogGetAttribute(t *testing.T) { }, } - expected := provider.Prefix + ".foo" - actual := provider.getPrefixedName("foo") - if actual != expected { - t.Fatalf("expected %s, got %s", expected, actual) - } + assert.Equal(t, provider.Prefix+".foo", provider.getPrefixedName("foo")) - for _, e := range services { - actual := provider.getAttribute(e.key, e.tags, e.defaultValue) - if actual != e.expected { - t.Fatalf("expected %s, got %s", e.expected, actual) - } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := provider.getAttribute(test.key, test.tags, test.defaultValue) + assert.Equal(t, test.expected, actual) + }) } } @@ -164,13 +171,15 @@ func TestConsulCatalogGetAttributeWithEmptyPrefix(t *testing.T) { Prefix: "", } - services := []struct { + testCases := []struct { + desc string tags []string key string defaultValue string expected string }{ { + desc: "Should return tag value 42", tags: []string{ "foo.bar=ramdom", "backend.weight=42", @@ -180,6 +189,7 @@ func TestConsulCatalogGetAttributeWithEmptyPrefix(t *testing.T) { expected: "42", }, { + desc: "Should return default value 0", tags: []string{ "foo.bar=ramdom", "backend.wei=42", @@ -189,6 +199,7 @@ func TestConsulCatalogGetAttributeWithEmptyPrefix(t *testing.T) { expected: "0", }, { + desc: "Should return for.bar key value random", tags: []string{ "foo.bar=ramdom", "backend.wei=42", @@ -199,17 +210,16 @@ func TestConsulCatalogGetAttributeWithEmptyPrefix(t *testing.T) { }, } - expected := "foo" - actual := provider.getPrefixedName("foo") - if actual != expected { - t.Fatalf("expected %s, got %s", expected, actual) - } + assert.Equal(t, "foo", provider.getPrefixedName("foo")) - for _, e := range services { - actual := provider.getAttribute(e.key, e.tags, e.defaultValue) - if actual != e.expected { - t.Fatalf("expected %s, got %s", e.expected, actual) - } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := provider.getAttribute(test.key, test.tags, test.defaultValue) + assert.Equal(t, test.expected, actual) + }) } } @@ -219,11 +229,13 @@ func TestConsulCatalogGetBackendAddress(t *testing.T) { Prefix: "traefik", } - services := []struct { + testCases := []struct { + desc string node *api.ServiceEntry expected string }{ { + desc: "Should return the address of the service", node: &api.ServiceEntry{ Node: &api.Node{ Address: "10.1.0.1", @@ -235,6 +247,7 @@ func TestConsulCatalogGetBackendAddress(t *testing.T) { expected: "10.2.0.1", }, { + desc: "Should return the address of the node", node: &api.ServiceEntry{ Node: &api.Node{ Address: "10.1.0.1", @@ -247,11 +260,14 @@ func TestConsulCatalogGetBackendAddress(t *testing.T) { }, } - for _, e := range services { - actual := provider.getBackendAddress(e.node) - if actual != e.expected { - t.Fatalf("expected %s, got %s", e.expected, actual) - } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := provider.getBackendAddress(test.node) + assert.Equal(t, test.expected, actual) + }) } } @@ -261,11 +277,13 @@ func TestConsulCatalogGetBackendName(t *testing.T) { Prefix: "traefik", } - services := []struct { + testCases := []struct { + desc string node *api.ServiceEntry expected string }{ { + desc: "Should create backend name without tags", node: &api.ServiceEntry{ Service: &api.AgentService{ Service: "api", @@ -277,6 +295,7 @@ func TestConsulCatalogGetBackendName(t *testing.T) { expected: "api--10-0-0-1--80--0", }, { + desc: "Should create backend name with multiple tags", node: &api.ServiceEntry{ Service: &api.AgentService{ Service: "api", @@ -288,6 +307,7 @@ func TestConsulCatalogGetBackendName(t *testing.T) { expected: "api--10-0-0-1--80--traefik-weight-42--traefik-enable-true--1", }, { + desc: "Should create backend name with one tag", node: &api.ServiceEntry{ Service: &api.AgentService{ Service: "api", @@ -300,11 +320,15 @@ func TestConsulCatalogGetBackendName(t *testing.T) { }, } - for i, e := range services { - actual := provider.getBackendName(e.node, i) - if actual != e.expected { - t.Fatalf("expected %s, got %s", e.expected, actual) - } + for i, test := range testCases { + test := test + i := i + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := provider.getBackendName(test.node, i) + assert.Equal(t, test.expected, actual) + }) } } @@ -317,17 +341,20 @@ func TestConsulCatalogBuildConfig(t *testing.T) { frontEndRuleTemplate: template.New("consul catalog frontend rule"), } - cases := []struct { + testCases := []struct { + desc string nodes []catalogUpdate expectedFrontends map[string]*types.Frontend expectedBackends map[string]*types.Backend }{ { + desc: "Should build config of nothing", nodes: []catalogUpdate{}, expectedFrontends: map[string]*types.Frontend{}, expectedBackends: map[string]*types.Backend{}, }, { + desc: "Should build config with no frontend and backend", nodes: []catalogUpdate{ { Service: &serviceUpdate{ @@ -339,6 +366,7 @@ func TestConsulCatalogBuildConfig(t *testing.T) { expectedBackends: map[string]*types.Backend{}, }, { + desc: "Should build config who contains one frontend and one backend", nodes: []catalogUpdate{ { Service: &serviceUpdate{ @@ -408,28 +436,31 @@ func TestConsulCatalogBuildConfig(t *testing.T) { }, } - for _, c := range cases { - actualConfig := provider.buildConfig(c.nodes) - if !reflect.DeepEqual(actualConfig.Backends, c.expectedBackends) { - t.Fatalf("expected %#v, got %#v", c.expectedBackends, actualConfig.Backends) - } - if !reflect.DeepEqual(actualConfig.Frontends, c.expectedFrontends) { - t.Fatalf("expected %#v, got %#v", c.expectedFrontends["frontend-test"].BasicAuth, actualConfig.Frontends["frontend-test"].BasicAuth) - t.Fatalf("expected %#v, got %#v", c.expectedFrontends, actualConfig.Frontends) - } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actualConfig := provider.buildConfig(test.nodes) + assert.Equal(t, test.expectedBackends, actualConfig.Backends) + assert.Equal(t, test.expectedFrontends, actualConfig.Frontends) + }) } } func TestConsulCatalogNodeSorter(t *testing.T) { - cases := []struct { + testCases := []struct { + desc string nodes []*api.ServiceEntry expected []*api.ServiceEntry }{ { + desc: "Should sort nothing", nodes: []*api.ServiceEntry{}, expected: []*api.ServiceEntry{}, }, { + desc: "Should sort by node address", nodes: []*api.ServiceEntry{ { Service: &api.AgentService{ @@ -458,6 +489,7 @@ func TestConsulCatalogNodeSorter(t *testing.T) { }, }, { + desc: "Should sort by service name", nodes: []*api.ServiceEntry{ { Service: &api.AgentService{ @@ -552,6 +584,7 @@ func TestConsulCatalogNodeSorter(t *testing.T) { }, }, { + desc: "Should sort by node address", nodes: []*api.ServiceEntry{ { Service: &api.AgentService{ @@ -603,12 +636,15 @@ func TestConsulCatalogNodeSorter(t *testing.T) { }, } - for _, c := range cases { - sort.Sort(nodeSorter(c.nodes)) - actual := c.nodes - if !reflect.DeepEqual(actual, c.expected) { - t.Fatalf("expected %q, got %q", c.expected, actual) - } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + sort.Sort(nodeSorter(test.nodes)) + actual := test.nodes + assert.Equal(t, test.expected, actual) + }) } } @@ -623,11 +659,13 @@ func TestConsulCatalogGetChangedKeys(t *testing.T) { removedKeys []string } - cases := []struct { + testCases := []struct { + desc string input Input output Output }{ { + desc: "Should add 0 services and removed 0", input: Input{ currState: map[string]Service{ "foo-service": {Name: "v1"}, @@ -668,6 +706,7 @@ func TestConsulCatalogGetChangedKeys(t *testing.T) { }, }, { + desc: "Should add 3 services and removed 0", input: Input{ currState: map[string]Service{ "foo-service": {Name: "v1"}, @@ -705,6 +744,7 @@ func TestConsulCatalogGetChangedKeys(t *testing.T) { }, }, { + desc: "Should add 2 services and removed 2", input: Input{ currState: map[string]Service{ "foo-service": {Name: "v1"}, @@ -742,21 +782,20 @@ func TestConsulCatalogGetChangedKeys(t *testing.T) { }, } - for _, c := range cases { - addedKeys, removedKeys := getChangedServiceKeys(c.input.currState, c.input.prevState) + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() - if !reflect.DeepEqual(fun.Set(addedKeys), fun.Set(c.output.addedKeys)) { - t.Fatalf("Added keys comparison results: got %q, want %q", addedKeys, c.output.addedKeys) - } - - if !reflect.DeepEqual(fun.Set(removedKeys), fun.Set(c.output.removedKeys)) { - t.Fatalf("Removed keys comparison results: got %q, want %q", removedKeys, c.output.removedKeys) - } + addedKeys, removedKeys := getChangedServiceKeys(test.input.currState, test.input.prevState) + assert.Equal(t, fun.Set(test.output.addedKeys), fun.Set(addedKeys), "Added keys comparison results: got %q, want %q", addedKeys, test.output.addedKeys) + assert.Equal(t, fun.Set(test.output.removedKeys), fun.Set(removedKeys), "Removed keys comparison results: got %q, want %q", removedKeys, test.output.removedKeys) + }) } } func TestConsulCatalogFilterEnabled(t *testing.T) { - cases := []struct { + testCases := []struct { desc string exposedByDefault bool node *api.ServiceEntry @@ -842,24 +881,23 @@ func TestConsulCatalogFilterEnabled(t *testing.T) { }, } - 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 := &CatalogProvider{ Domain: "localhost", Prefix: "traefik", - ExposedByDefault: c.exposedByDefault, - } - if provider.nodeFilter("test", c.node) != c.expected { - t.Errorf("got unexpected filtering = %t", !c.expected) + ExposedByDefault: test.exposedByDefault, } + actual := provider.nodeFilter("test", test.node) + assert.Equal(t, test.expected, actual) }) } } func TestConsulCatalogGetBasicAuth(t *testing.T) { - cases := []struct { + testCases := []struct { desc string tags []string expected []string @@ -878,17 +916,15 @@ func TestConsulCatalogGetBasicAuth(t *testing.T) { }, } - 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 := &CatalogProvider{ Prefix: "traefik", } - actual := provider.getBasicAuth(c.tags) - if !reflect.DeepEqual(actual, c.expected) { - t.Errorf("actual %q, expected %q", actual, c.expected) - } + actual := provider.getBasicAuth(test.tags) + assert.Equal(t, test.expected, actual) }) } } @@ -930,7 +966,276 @@ func TestConsulCatalogHasStickinessLabel(t *testing.T) { t.Parallel() actual := provider.hasStickinessLabel(test.tags) - assert.Equal(t, actual, test.expected) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestConsulCatalogGetChangedStringKeys(t *testing.T) { + testCases := []struct { + desc string + current []string + previous []string + expectedAdded []string + expectedRemoved []string + }{ + { + desc: "1 element added, 0 removed", + current: []string{"chou"}, + previous: []string{}, + expectedAdded: []string{"chou"}, + expectedRemoved: []string{}, + }, { + desc: "0 element added, 0 removed", + current: []string{"chou"}, + previous: []string{"chou"}, + expectedAdded: []string{}, + expectedRemoved: []string{}, + }, + { + desc: "0 element added, 1 removed", + current: []string{}, + previous: []string{"chou"}, + expectedAdded: []string{}, + expectedRemoved: []string{"chou"}, + }, + { + desc: "1 element added, 1 removed", + current: []string{"carotte"}, + previous: []string{"chou"}, + expectedAdded: []string{"carotte"}, + expectedRemoved: []string{"chou"}, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actualAdded, actualRemoved := getChangedStringKeys(test.current, test.previous) + assert.Equal(t, test.expectedAdded, actualAdded) + assert.Equal(t, test.expectedRemoved, actualRemoved) + }) + } +} + +func TestConsulCatalogHasNodeOrTagschanged(t *testing.T) { + testCases := []struct { + desc string + current map[string]Service + previous map[string]Service + expected bool + }{ + { + desc: "Change detected due to change of nodes", + current: map[string]Service{ + "foo-service": { + Name: "foo", + Nodes: []string{"node1"}, + Tags: []string{}, + }, + }, + previous: map[string]Service{ + "foo-service": { + Name: "foo", + Nodes: []string{"node2"}, + Tags: []string{}, + }, + }, + expected: true, + }, + { + desc: "No change missing current service", + current: make(map[string]Service), + previous: map[string]Service{ + "foo-service": { + Name: "foo", + Nodes: []string{"node1"}, + Tags: []string{}, + }, + }, + expected: false, + }, + { + desc: "No change on nodes", + current: map[string]Service{ + "foo-service": { + Name: "foo", + Nodes: []string{"node1"}, + Tags: []string{}, + }, + }, + previous: map[string]Service{ + "foo-service": { + Name: "foo", + Nodes: []string{"node1"}, + Tags: []string{}, + }, + }, + expected: false, + }, + { + desc: "No change on nodes and tags", + current: map[string]Service{ + "foo-service": { + Name: "foo", + Nodes: []string{"node1"}, + Tags: []string{"foo=bar"}, + }, + }, + previous: map[string]Service{ + "foo-service": { + Name: "foo", + Nodes: []string{"node1"}, + Tags: []string{"foo=bar"}, + }, + }, + expected: false, + }, + { + desc: "Change detected con tags", + current: map[string]Service{ + "foo-service": { + Name: "foo", + Nodes: []string{"node1"}, + Tags: []string{"foo=bar"}, + }, + }, + previous: map[string]Service{ + "foo-service": { + Name: "foo", + Nodes: []string{"node1"}, + Tags: []string{"foo"}, + }, + }, + expected: true, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := hasNodeOrTagsChanged(test.current, test.previous) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestConsulCatalogHasChanged(t *testing.T) { + testCases := []struct { + desc string + current map[string]Service + previous map[string]Service + expected bool + }{ + { + desc: "Change detected due to change new service", + current: map[string]Service{ + "foo-service": { + Name: "foo", + Nodes: []string{"node1"}, + Tags: []string{}, + }, + }, + previous: make(map[string]Service), + expected: true, + }, + { + desc: "Change detected due to change service removed", + current: make(map[string]Service), + previous: map[string]Service{ + "foo-service": { + Name: "foo", + Nodes: []string{"node1"}, + Tags: []string{}, + }, + }, + expected: true, + }, + { + desc: "Change detected due to change of nodes", + current: map[string]Service{ + "foo-service": { + Name: "foo", + Nodes: []string{"node1"}, + Tags: []string{}, + }, + }, + previous: map[string]Service{ + "foo-service": { + Name: "foo", + Nodes: []string{"node2"}, + Tags: []string{}, + }, + }, + expected: true, + }, + { + desc: "No change on nodes", + current: map[string]Service{ + "foo-service": { + Name: "foo", + Nodes: []string{"node1"}, + Tags: []string{}, + }, + }, + previous: map[string]Service{ + "foo-service": { + Name: "foo", + Nodes: []string{"node1"}, + Tags: []string{}, + }, + }, + expected: false, + }, + { + desc: "No change on nodes and tags", + current: map[string]Service{ + "foo-service": { + Name: "foo", + Nodes: []string{"node1"}, + Tags: []string{"foo=bar"}, + }, + }, + previous: map[string]Service{ + "foo-service": { + Name: "foo", + Nodes: []string{"node1"}, + Tags: []string{"foo=bar"}, + }, + }, + expected: false, + }, + { + desc: "Change detected on tags", + current: map[string]Service{ + "foo-service": { + Name: "foo", + Nodes: []string{"node1"}, + Tags: []string{"foo=bar"}, + }, + }, + previous: map[string]Service{ + "foo-service": { + Name: "foo", + Nodes: []string{"node1"}, + Tags: []string{"foo"}, + }, + }, + expected: true, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := hasChanged(test.current, test.previous) + assert.Equal(t, test.expected, actual) }) } } From 1e3506848a445132d0eb9fdad3fc34cf426d2345 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 14 Nov 2017 11:16:03 +0100 Subject: [PATCH 09/11] Flush and errorcode --- glide.lock | 6 +++-- glide.yaml | 3 +++ vendor/github.com/NYTimes/gziphandler/gzip.go | 22 ++++++++++--------- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/glide.lock b/glide.lock index 560b517e4..f9ff37c3a 100644 --- a/glide.lock +++ b/glide.lock @@ -1,4 +1,4 @@ -hash: de7e6a0069090a5811c003db434da19fe31efcf0c9429d3ccb676295708f0d2b +hash: bbdbbc9d428937dbaf85e92a3747ebe547f1cc110fbb536c94b5efb3dde6e5ab updated: 2017-10-24T14:08:11.364720581+02:00 imports: - name: cloud.google.com/go @@ -383,7 +383,9 @@ imports: repo: https://github.com/ijc25/Gotty.git vcs: git - name: github.com/NYTimes/gziphandler - version: 0f67f3f25d3b17590ee0ab93fcb216363ee30967 + version: 26a3f68265200656f31940bc15b191f7d10b5bbd + repo: https://github.com/containous/gziphandler.git + vcs: git - name: github.com/ogier/pflag version: 45c278ab3607870051a2ea9040bb85fcb8557481 - name: github.com/opencontainers/go-digest diff --git a/glide.yaml b/glide.yaml index 2d458a630..574be7dac 100644 --- a/glide.yaml +++ b/glide.yaml @@ -79,6 +79,9 @@ import: vcs: git - package: github.com/abbot/go-http-auth - package: github.com/NYTimes/gziphandler + version: fork-containous + repo: https://github.com/containous/gziphandler.git + vcs: git - package: github.com/docker/leadership - package: github.com/satori/go.uuid version: ^1.1.0 diff --git a/vendor/github.com/NYTimes/gziphandler/gzip.go b/vendor/github.com/NYTimes/gziphandler/gzip.go index 91130c424..901c4554a 100644 --- a/vendor/github.com/NYTimes/gziphandler/gzip.go +++ b/vendor/github.com/NYTimes/gziphandler/gzip.go @@ -82,7 +82,6 @@ type GzipResponseWriter struct { buf []byte // Holds the first part of the write before reaching the minSize or the end of the write. contentTypes []string // Only compress if the response is one of these content-types. All are accepted if empty. - flushed bool // Indicate if the stream was already flushed } // Write appends data to the gzip writer. @@ -151,7 +150,9 @@ func (w *GzipResponseWriter) startGzip() error { // WriteHeader just saves the response code until close or GZIP effective writes. func (w *GzipResponseWriter) WriteHeader(code int) { - w.code = code + if w.code == 0 { + w.code = code + } } // init graps a new gzip writer from the gzipWriterPool and writes the correct @@ -168,8 +169,7 @@ func (w *GzipResponseWriter) init() { func (w *GzipResponseWriter) Close() error { if w.gw == nil { // Gzip not trigged yet, write out regular response. - // WriteHeader only if it wasn't already wrote by a Flush - if !w.flushed && w.code != 0 { + if w.code != 0 { w.ResponseWriter.WriteHeader(w.code) } if w.buf != nil { @@ -192,16 +192,18 @@ func (w *GzipResponseWriter) Close() error { // http.ResponseWriter if it is an http.Flusher. This makes GzipResponseWriter // an http.Flusher. func (w *GzipResponseWriter) Flush() { - if w.gw != nil { - w.gw.Flush() + if w.gw == nil { + // Only flush once startGzip has been called. + // + // Flush is thus a no-op until the written body + // exceeds minSize. + return } + w.gw.Flush() + if fw, ok := w.ResponseWriter.(http.Flusher); ok { - if !w.flushed && w.code != 0 { - w.ResponseWriter.WriteHeader(w.code) - } fw.Flush() - w.flushed = true } } From 96a7cc483f132b69f6a8f860ad25ddd0a31ae6b0 Mon Sep 17 00:00:00 2001 From: NicoMen Date: Tue, 14 Nov 2017 11:38:03 +0100 Subject: [PATCH 10/11] Add Traefik prefix to the KV key --- docs/user-guide/kv-config.md | 31 +++++++++++++++++++++++++++++++ provider/kv/kv.go | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/user-guide/kv-config.md b/docs/user-guide/kv-config.md index 6afd82d7c..fb8beae54 100644 --- a/docs/user-guide/kv-config.md +++ b/docs/user-guide/kv-config.md @@ -148,6 +148,37 @@ This variable must be initialized with the ACL token value. If Traefik is launched into a Docker container, the variable `CONSUL_HTTP_TOKEN` can be initialized with the `-e` Docker option : `-e "CONSUL_HTTP_TOKEN=[consul-acl-token-value]"` +If a Consul ACL is used to restrict Træfik read/write access, one of the following configurations is needed. + +- HCL format : + +``` + key "traefik" { + policy = "write" + }, + + session "" { + policy = "write" + } +``` + +- JSON format : + +```json +{ + "key": { + "traefik": { + "policy": "write" + } + }, + "session": { + "": { + "policy": "write" + } + } +} +``` + ### TLS support To connect to a Consul endpoint using SSL, simply specify `https://` in the `consul.endpoint` property diff --git a/provider/kv/kv.go b/provider/kv/kv.go index 1ebb4298f..d2d7fa8c0 100644 --- a/provider/kv/kv.go +++ b/provider/kv/kv.go @@ -102,7 +102,7 @@ func (p *Provider) watchKv(configurationChan chan<- types.ConfigMessage, prefix func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error { p.Constraints = append(p.Constraints, constraints...) operation := func() error { - if _, err := p.kvclient.Exists("qmslkjdfmqlskdjfmqlksjazçueznbvbwzlkajzebvkwjdcqmlsfj"); err != nil { + if _, err := p.kvclient.Exists(p.Prefix + "/qmslkjdfmqlskdjfmqlksjazçueznbvbwzlkajzebvkwjdcqmlsfj"); err != nil { return fmt.Errorf("Failed to test KV store connection: %v", err) } if p.Watch { From 77b111702be5c932be7dc13729a4bb06fc53357a Mon Sep 17 00:00:00 2001 From: NicoMen Date: Tue, 14 Nov 2017 12:06:03 +0100 Subject: [PATCH 11/11] Prepare release v1.4.3 --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e39f80c0f..de70e1906 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Change Log +## [v1.4.3](https://github.com/containous/traefik/tree/v1.4.3) (2017-11-14) +[All Commits](https://github.com/containous/traefik/compare/v1.4.2...v1.4.3) + +**Bug fixes:** +- **[consulcatalog]** Fix Traefik reload if Consul Catalog tags change ([#2389](https://github.com/containous/traefik/pull/2389) by [mmatur](https://github.com/mmatur)) +- **[kv]** Add Traefik prefix to the KV key ([#2400](https://github.com/containous/traefik/pull/2400) by [nmengin](https://github.com/nmengin)) +- **[middleware]** Flush and Status code ([#2403](https://github.com/containous/traefik/pull/2403) by [ldez](https://github.com/ldez)) +- **[middleware]** Exclude GRPC from compress ([#2391](https://github.com/containous/traefik/pull/2391) by [ldez](https://github.com/ldez)) +- **[middleware]** Keep status when stream mode and compress ([#2380](https://github.com/containous/traefik/pull/2380) by [Juliens](https://github.com/Juliens)) + +**Documentation:** +- **[acme]** Fix some typos ([#2363](https://github.com/containous/traefik/pull/2363) by [tomsaleeba](https://github.com/tomsaleeba)) +- **[docker]** Minor fix for docker volume vs created directory ([#2372](https://github.com/containous/traefik/pull/2372) by [visibilityspots](https://github.com/visibilityspots)) +- **[k8s]** Link corrected ([#2385](https://github.com/containous/traefik/pull/2385) by [xlazex](https://github.com/xlazex)) + +**Misc:** +- **[k8s]** Add secret creation to docs for kubernetes backend ([#2374](https://github.com/containous/traefik/pull/2374) by [shadycuz](https://github.com/shadycuz)) + ## [v1.4.2](https://github.com/containous/traefik/tree/v1.4.2) (2017-11-02) [All Commits](https://github.com/containous/traefik/compare/v1.4.1...v1.4.2)