diff --git a/CHANGELOG.md b/CHANGELOG.md index 111236bc1..0776d25b6 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) diff --git a/docs/configuration/backends/kubernetes.md b/docs/configuration/backends/kubernetes.md index 2b95025e2..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 @@ -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/docker-and-lets-encrypt.md b/docs/user-guide/docker-and-lets-encrypt.md index 9f1dce240..24c149016 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: diff --git a/docs/user-guide/examples.md b/docs/user-guide/examples.md index 91b650747..666b7c918 100644 --- a/docs/user-guide/examples.md +++ b/docs/user-guide/examples.md @@ -140,7 +140,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 @@ -173,7 +173,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 @@ -201,7 +201,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 diff --git a/docs/user-guide/kubernetes.md b/docs/user-guide/kubernetes.md index 84fb0a78b..c5fa9b3ec 100644 --- a/docs/user-guide/kubernetes.md +++ b/docs/user-guide/kubernetes.md @@ -82,7 +82,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 @@ -330,6 +330,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. diff --git a/docs/user-guide/kv-config.md b/docs/user-guide/kv-config.md index 3874cb266..3a4c684d1 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/glide.lock b/glide.lock index 37d5b4c9d..07b356541 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 7fd36649e80749e16bbfa69777e0f90a017fbc2f67d7efd46373716a16b1a60a -updated: 2017-11-02T11:39:20.438135-04:00 +hash: 5aef5628a880e04fac9cd9db2a33f1f4716680b0c338f3aa803d9786f253405a +updated: 2017-11-15T18:39:20.364720581+02:00 imports: - name: cloud.google.com/go version: 2e6a95edb1071d750f6d7db777bf66cd2997af6c @@ -398,7 +398,9 @@ imports: repo: https://github.com/ijc25/Gotty.git vcs: git - name: github.com/NYTimes/gziphandler - version: 97ae7fbaf81620fe97840685304a78a306a39c64 + 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 403d0a1b2..66af71c30 100644 --- a/glide.yaml +++ b/glide.yaml @@ -80,6 +80,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/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/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 19689dd96..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{} @@ -137,6 +158,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/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) }) } } 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 { 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 diff --git a/vendor/github.com/NYTimes/gziphandler/gzip.go b/vendor/github.com/NYTimes/gziphandler/gzip.go index b6af9115a..901c4554a 100644 --- a/vendor/github.com/NYTimes/gziphandler/gzip.go +++ b/vendor/github.com/NYTimes/gziphandler/gzip.go @@ -150,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 @@ -190,10 +192,16 @@ 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 { fw.Flush() }