From e6040e55f58055dd47e4925907023bcb4d6a0bf7 Mon Sep 17 00:00:00 2001 From: Kevin Maris Date: Fri, 3 Aug 2018 02:00:03 -0600 Subject: [PATCH 01/35] Update kubernetes.md --- 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 e8896c50d..fac33b231 100644 --- a/docs/configuration/backends/kubernetes.md +++ b/docs/configuration/backends/kubernetes.md @@ -108,7 +108,7 @@ The endpoint may be specified to override the environment variable values inside When the environment variables are not found, Traefik will try to connect to the Kubernetes API server with an external-cluster client. In this case, the endpoint is required. -Specifically, it may be set to the URL used by `kubectl proxy` to connect to a Kubernetes cluster using the granted autentication and authorization of the associated kubeconfig. +Specifically, it may be set to the URL used by `kubectl proxy` to connect to a Kubernetes cluster using the granted authentication and authorization of the associated kubeconfig. ### `labelselector` From ad6f41c77aa17715baf3e4157ceb0b03ee619348 Mon Sep 17 00:00:00 2001 From: Tom Mast Date: Fri, 3 Aug 2018 08:36:03 +0000 Subject: [PATCH 02/35] Simple documentation grammar update in tracing --- docs/configuration/tracing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/tracing.md b/docs/configuration/tracing.md index 0c2b62e99..275a39073 100644 --- a/docs/configuration/tracing.md +++ b/docs/configuration/tracing.md @@ -1,6 +1,6 @@ # Tracing -Tracing system allows developers to visualize call flows in there infrastructures. +The tracing system allows developers to visualize call flows in their infrastructure. We use [OpenTracing](http://opentracing.io). It is an open standard designed for distributed tracing. From 4db937b57110a49661fc2e4952ea1b05bc55af54 Mon Sep 17 00:00:00 2001 From: NicoMen Date: Fri, 3 Aug 2018 14:02:02 +0200 Subject: [PATCH 03/35] Avoid a panic during Prometheus registering --- metrics/prometheus.go | 57 +++++++++++++++++++++-------- metrics/prometheus_test.go | 74 ++++++++++++++++++++++++++++++++++++++ server/server.go | 7 ++-- 3 files changed, 121 insertions(+), 17 deletions(-) diff --git a/metrics/prometheus.go b/metrics/prometheus.go index 5fce28890..95a0efab2 100644 --- a/metrics/prometheus.go +++ b/metrics/prometheus.go @@ -7,6 +7,7 @@ import ( "sync" "github.com/containous/mux" + "github.com/containous/traefik/log" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" "github.com/go-kit/kit/metrics" @@ -15,25 +16,31 @@ import ( ) const ( - metricNamePrefix = "traefik_" + // MetricNamePrefix prefix of all metric names + MetricNamePrefix = "traefik_" // server meta information - configReloadsTotalName = metricNamePrefix + "config_reloads_total" - configReloadsFailuresTotalName = metricNamePrefix + "config_reloads_failure_total" - configLastReloadSuccessName = metricNamePrefix + "config_last_reload_success" - configLastReloadFailureName = metricNamePrefix + "config_last_reload_failure" + metricConfigPrefix = MetricNamePrefix + "config_" + configReloadsTotalName = metricConfigPrefix + "reloads_total" + configReloadsFailuresTotalName = metricConfigPrefix + "reloads_failure_total" + configLastReloadSuccessName = metricConfigPrefix + "last_reload_success" + configLastReloadFailureName = metricConfigPrefix + "last_reload_failure" // entrypoint - entrypointReqsTotalName = metricNamePrefix + "entrypoint_requests_total" - entrypointReqDurationName = metricNamePrefix + "entrypoint_request_duration_seconds" - entrypointOpenConnsName = metricNamePrefix + "entrypoint_open_connections" + metricEntryPointPrefix = MetricNamePrefix + "entrypoint_" + entrypointReqsTotalName = metricEntryPointPrefix + "requests_total" + entrypointReqDurationName = metricEntryPointPrefix + "request_duration_seconds" + entrypointOpenConnsName = metricEntryPointPrefix + "open_connections" - // backend level - backendReqsTotalName = metricNamePrefix + "backend_requests_total" - backendReqDurationName = metricNamePrefix + "backend_request_duration_seconds" - backendOpenConnsName = metricNamePrefix + "backend_open_connections" - backendRetriesTotalName = metricNamePrefix + "backend_retries_total" - backendServerUpName = metricNamePrefix + "backend_server_up" + // backend level. + + // MetricBackendPrefix prefix of all backend metric names + MetricBackendPrefix = MetricNamePrefix + "backend_" + backendReqsTotalName = MetricBackendPrefix + "requests_total" + backendReqDurationName = MetricBackendPrefix + "request_duration_seconds" + backendOpenConnsName = MetricBackendPrefix + "open_connections" + backendRetriesTotalName = MetricBackendPrefix + "retries_total" + backendServerUpName = MetricBackendPrefix + "server_up" ) // promState holds all metric state internally and acts as the only Collector we register for Prometheus. @@ -61,6 +68,16 @@ func (h PrometheusHandler) AddRoutes(router *mux.Router) { // RegisterPrometheus registers all Prometheus metrics. // It must be called only once and failing to register the metrics will lead to a panic. func RegisterPrometheus(config *types.Prometheus) Registry { + standardRegistry := initStandardRegistry(config) + + if !registerPromState() { + return nil + } + + return standardRegistry +} + +func initStandardRegistry(config *types.Prometheus) Registry { buckets := []float64{0.1, 0.3, 1.2, 5.0} if config.Buckets != nil { buckets = config.Buckets @@ -137,7 +154,6 @@ func RegisterPrometheus(config *types.Prometheus) Registry { backendRetries.cv.Describe, backendServerUp.gv.Describe, } - stdprometheus.MustRegister(promState) return &standardRegistry{ enabled: true, @@ -156,6 +172,17 @@ func RegisterPrometheus(config *types.Prometheus) Registry { } } +func registerPromState() bool { + if err := stdprometheus.Register(promState); err != nil { + if _, ok := err.(stdprometheus.AlreadyRegisteredError); !ok { + log.Errorf("Unable to register Traefik to Prometheus: %v", err) + return false + } + log.Debug("Prometheus collector already registered.") + } + return true +} + // OnConfigurationUpdate receives the current configuration from Traefik. // It then converts the configuration to the optimized package internal format // and sets it to the promState. diff --git a/metrics/prometheus_test.go b/metrics/prometheus_test.go index 41d7ab308..16d05914e 100644 --- a/metrics/prometheus_test.go +++ b/metrics/prometheus_test.go @@ -11,8 +11,82 @@ import ( "github.com/containous/traefik/types" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/stretchr/testify/assert" ) +func TestRegisterPromState(t *testing.T) { + // Reset state of global promState. + defer promState.reset() + + testCases := []struct { + desc string + prometheusSlice []*types.Prometheus + initPromState bool + unregisterPromState bool + expectedNbRegistries int + }{ + { + desc: "Register once", + prometheusSlice: []*types.Prometheus{{}}, + expectedNbRegistries: 1, + initPromState: true, + }, + { + desc: "Register once with no promState init", + prometheusSlice: []*types.Prometheus{{}}, + expectedNbRegistries: 0, + }, + { + desc: "Register twice", + prometheusSlice: []*types.Prometheus{{}, {}}, + expectedNbRegistries: 2, + initPromState: true, + }, + { + desc: "Register twice with no promstate init", + prometheusSlice: []*types.Prometheus{{}, {}}, + expectedNbRegistries: 0, + }, + { + desc: "Register twice with unregister", + prometheusSlice: []*types.Prometheus{{}, {}}, + unregisterPromState: true, + expectedNbRegistries: 2, + initPromState: true, + }, + { + desc: "Register twice with unregister but no promstate init", + prometheusSlice: []*types.Prometheus{{}, {}}, + unregisterPromState: true, + expectedNbRegistries: 0, + }, + } + + for _, test := range testCases { + actualNbRegistries := 0 + for _, prom := range test.prometheusSlice { + if test.initPromState { + initStandardRegistry(prom) + } + + promReg := registerPromState() + if promReg != false { + actualNbRegistries++ + } + + if test.unregisterPromState { + prometheus.Unregister(promState) + } + + promState.reset() + } + + prometheus.Unregister(promState) + + assert.Equal(t, test.expectedNbRegistries, actualNbRegistries) + } +} + func TestPrometheus(t *testing.T) { // Reset state of global promState. defer promState.reset() diff --git a/server/server.go b/server/server.go index 2739aad5a..d5a68cfba 100644 --- a/server/server.go +++ b/server/server.go @@ -652,8 +652,11 @@ func registerMetricClients(metricsConfig *types.Metrics) metrics.Registry { var registries []metrics.Registry if metricsConfig.Prometheus != nil { - registries = append(registries, metrics.RegisterPrometheus(metricsConfig.Prometheus)) - log.Debug("Configured Prometheus metrics") + prometheusRegister := metrics.RegisterPrometheus(metricsConfig.Prometheus) + if prometheusRegister != nil { + registries = append(registries, prometheusRegister) + log.Debug("Configured Prometheus metrics") + } } if metricsConfig.Datadog != nil { registries = append(registries, metrics.RegisterDatadog(metricsConfig.Datadog)) From 70712a0f62afa22cce94df235cd3d5ee9490023c Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Aug 2018 16:52:03 +0100 Subject: [PATCH 04/35] Typo in docker-and-lets-encrypt.md --- docs/user-guide/docker-and-lets-encrypt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user-guide/docker-and-lets-encrypt.md b/docs/user-guide/docker-and-lets-encrypt.md index 8fd443037..af7ebe93a 100644 --- a/docs/user-guide/docker-and-lets-encrypt.md +++ b/docs/user-guide/docker-and-lets-encrypt.md @@ -232,7 +232,7 @@ Finally but not unimportantly, we tell Træfik to route **to** port `9000`, sinc `Service labels` allow managing many routes for the same container. When both `container labels` and `service labels` are defined, `container labels` are just used as default values for missing `service labels` but no frontend/backend are going to be defined only with these labels. -Obviously, labels `traefik.frontend.rule` and `traefik.port` described above, will only be used to complete information set in `service labels` during the container frontends/bakends creation. +Obviously, labels `traefik.frontend.rule` and `traefik.port` described above, will only be used to complete information set in `service labels` during the container frontends/backends creation. In the example, two service names are defined : `basic` and `admin`. They allow creating two frontends and two backends. From 86add298389edad804d39bb7cb93fa31a4e1ae02 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 6 Aug 2018 13:50:02 +0200 Subject: [PATCH 05/35] Freeze mkdocs version. --- requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 01eb8ecaa..be343f58a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -mkdocs>=0.17.3 -pymdown-extensions>=1.4 -mkdocs-bootswatch>=0.4.0 -mkdocs-material>=2.2.6 +mkdocs==0.17.5 +pymdown-extensions==4.12 +mkdocs-bootswatch==0.5.0 +mkdocs-material==2.9.4 From bb331285522b2734fc36b5cde0c9c5170c4ffcfe Mon Sep 17 00:00:00 2001 From: nicolas Trauwaen Date: Mon, 6 Aug 2018 14:08:03 +0200 Subject: [PATCH 06/35] Change syntax in quick start guide --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 833c85f33..2afd6b199 100644 --- a/docs/index.md +++ b/docs/index.md @@ -138,7 +138,7 @@ IP: 172.27.0.3 Run more instances of your `whoami` service with the following command: ```shell -docker-compose up -d --scale whoami=2 +docker-compose scale whoami=2 ``` Go back to your browser ([http://localhost:8080](http://localhost:8080)) and see that Træfik has automatically detected the new instance of the container. From 7ff6e6b66f4b944b58c458df539eb4a0d968aa9b Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 6 Aug 2018 15:50:03 +0200 Subject: [PATCH 07/35] Freeze mkdocs version --- requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 01eb8ecaa..be343f58a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -mkdocs>=0.17.3 -pymdown-extensions>=1.4 -mkdocs-bootswatch>=0.4.0 -mkdocs-material>=2.2.6 +mkdocs==0.17.5 +pymdown-extensions==4.12 +mkdocs-bootswatch==0.5.0 +mkdocs-material==2.9.4 From 2d449f63e0a9d573f0da367a586881f1ce0c9084 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Wed, 8 Aug 2018 02:52:03 +0200 Subject: [PATCH 08/35] Typo in auth labels. --- docs/configuration/backends/docker.md | 8 ++++---- docs/configuration/backends/ecs.md | 4 ++-- docs/configuration/backends/rancher.md | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/configuration/backends/docker.md b/docs/configuration/backends/docker.md index 6b89ff9fe..d370cfa17 100644 --- a/docs/configuration/backends/docker.md +++ b/docs/configuration/backends/docker.md @@ -238,10 +238,10 @@ Labels can be used on containers to override default behavior. | `traefik.frontend.auth.basic=EXPR` | Sets the basic authentication to this frontend in CSV format: `User:Hash,User:Hash` [2] (DEPRECATED). | | `traefik.frontend.auth.basic.removeHeader=true` | If set to `true`, removes the `Authorization` header. | | `traefik.frontend.auth.basic.users=EXPR` | Sets the basic authentication to this frontend in CSV format: `User:Hash,User:Hash` [2]. | -| `traefik.frontend.auth.basic.usersfile=/path/.htpasswd` | Sets the basic authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | +| `traefik.frontend.auth.basic.usersFile=/path/.htpasswd` | Sets the basic authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | | `traefik.frontend.auth.digest.removeHeader=true` | If set to `true`, removes the `Authorization` header. | | `traefik.frontend.auth.digest.users=EXPR` | Sets the digest authentication to this frontend in CSV format: `User:Realm:Hash,User:Realm:Hash`. | -| `traefik.frontend.auth.digest.usersfile=/path/.htdigest` | Sets the digest authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | +| `traefik.frontend.auth.digest.usersFile=/path/.htdigest` | Sets the digest authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | | `traefik.frontend.auth.forward.address=https://example.com`| Sets the URL of the authentication server. | | `traefik.frontend.auth.forward.tls.ca=/path/ca.pem` | Sets the Certificate Authority (CA) for the TLS connection with the authentication server. | | `traefik.frontend.auth.forward.tls.caOptional=true` | Checks the certificates if present but do not force to be signed by a specified Certificate Authority (CA). | @@ -330,10 +330,10 @@ Segment labels override the default behavior. | `traefik..frontend.auth.basic=EXPR` | Same as `traefik.frontend.auth.basic` | | `traefik..frontend.auth.basic.removeHeader=true` | Same as `traefik.frontend.auth.basic.removeHeader` | | `traefik..frontend.auth.basic.users=EXPR` | Same as `traefik.frontend.auth.basic.users` | -| `traefik..frontend.auth.basic.usersfile=/path/.htpasswd` | Same as `traefik.frontend.auth.basic.usersfile` | +| `traefik..frontend.auth.basic.usersFile=/path/.htpasswd` | Same as `traefik.frontend.auth.basic.usersFile` | | `traefik..frontend.auth.digest.removeHeader=true` | Same as `traefik.frontend.auth.digest.removeHeader` | | `traefik..frontend.auth.digest.users=EXPR` | Same as `traefik.frontend.auth.digest.users` | -| `traefik..frontend.auth.digest.usersfile=/path/.htdigest` | Same as `traefik.frontend.auth.digest.usersfile` | +| `traefik..frontend.auth.digest.usersFile=/path/.htdigest` | Same as `traefik.frontend.auth.digest.usersFile` | | `traefik..frontend.auth.forward.address=https://example.com`| Same as `traefik.frontend.auth.forward.address` | | `traefik..frontend.auth.forward.tls.ca=/path/ca.pem` | Same as `traefik.frontend.auth.forward.tls.ca` | | `traefik..frontend.auth.forward.tls.caOptional=true` | Same as `traefik.frontend.auth.forward.tls.caOptional` | diff --git a/docs/configuration/backends/ecs.md b/docs/configuration/backends/ecs.md index 7c248409b..51e379c20 100644 --- a/docs/configuration/backends/ecs.md +++ b/docs/configuration/backends/ecs.md @@ -165,10 +165,10 @@ Labels can be used on task containers to override default behaviour: | `traefik.frontend.auth.basic=EXPR` | Sets basic authentication to this frontend in CSV format: `User:Hash,User:Hash` (DEPRECATED). | | `traefik.frontend.auth.basic.removeHeader=true` | If set to `true`, removes the `Authorization` header. | | `traefik.frontend.auth.basic.users=EXPR` | Sets basic authentication to this frontend in CSV format: `User:Hash,User:Hash`. | -| `traefik.frontend.auth.basic.usersfile=/path/.htpasswd` | Sets basic authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | +| `traefik.frontend.auth.basic.usersFile=/path/.htpasswd` | Sets basic authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | | `traefik.frontend.auth.digest.removeHeader=true` | If set to `true`, removes the `Authorization` header. | | `traefik.frontend.auth.digest.users=EXPR` | Sets digest authentication to this frontend in CSV format: `User:Realm:Hash,User:Realm:Hash`. | -| `traefik.frontend.auth.digest.usersfile=/path/.htdigest` | Sets digest authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | +| `traefik.frontend.auth.digest.usersFile=/path/.htdigest` | Sets digest authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | | `traefik.frontend.auth.forward.address=https://example.com`| Sets the URL of the authentication server. | | `traefik.frontend.auth.forward.tls.ca=/path/ca.pem` | Sets the Certificate Authority (CA) for the TLS connection with the authentication server. | | `traefik.frontend.auth.forward.tls.caOptional=true` | Checks the certificates if present but do not force to be signed by a specified Certificate Authority (CA). | diff --git a/docs/configuration/backends/rancher.md b/docs/configuration/backends/rancher.md index 23d925ea6..f34272bf4 100644 --- a/docs/configuration/backends/rancher.md +++ b/docs/configuration/backends/rancher.md @@ -167,10 +167,10 @@ Labels can be used on task containers to override default behavior: | `traefik.frontend.auth.basic=EXPR` | Sets the basic authentication to this frontend in CSV format: `User:Hash,User:Hash` (DEPRECATED). | | `traefik.frontend.auth.basic.removeHeader=true` | If set to `true`, removes the `Authorization` header. | | `traefik.frontend.auth.basic.users=EXPR` | Sets the basic authentication to this frontend in CSV format: `User:Hash,User:Hash` . | -| `traefik.frontend.auth.basic.usersfile=/path/.htpasswd` | Sets the basic authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | +| `traefik.frontend.auth.basic.usersFile=/path/.htpasswd` | Sets the basic authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | | `traefik.frontend.auth.digest.removeHeader=true` | If set to `true`, removes the `Authorization` header. | | `traefik.frontend.auth.digest.users=EXPR` | Sets the digest authentication to this frontend in CSV format: `User:Realm:Hash,User:Realm:Hash`. | -| `traefik.frontend.auth.digest.usersfile=/path/.htdigest` | Sets the digest authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | +| `traefik.frontend.auth.digest.usersFile=/path/.htdigest` | Sets the digest authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | | `traefik.frontend.auth.forward.address=https://example.com`| Sets the URL of the authentication server. | | `traefik.frontend.auth.forward.tls.ca=/path/ca.pem` | Sets the Certificate Authority (CA) for the TLS connection with the authentication server. | | `traefik.frontend.auth.forward.tls.caOptional=true` | Checks the certificates if present but do not force to be signed by a specified Certificate Authority (CA). | @@ -248,10 +248,10 @@ Segment labels override the default behavior. | `traefik..frontend.auth.basic=EXPR` | Same as `traefik.frontend.auth.basic` | | `traefik..frontend.auth.basic.removeHeader=true` | Same as `traefik.frontend.auth.basic.removeHeader` | | `traefik..frontend.auth.basic.users=EXPR` | Same as `traefik.frontend.auth.basic.users` | -| `traefik..frontend.auth.basic.usersfile=/path/.htpasswd` | Same as `traefik.frontend.auth.basic.usersfile` | +| `traefik..frontend.auth.basic.usersFile=/path/.htpasswd` | Same as `traefik.frontend.auth.basic.usersFile` | | `traefik..frontend.auth.digest.removeHeader=true` | Same as `traefik.frontend.auth.digest.removeHeader` | | `traefik..frontend.auth.digest.users=EXPR` | Same as `traefik.frontend.auth.digest.users` | -| `traefik..frontend.auth.digest.usersfile=/path/.htdigest` | Same as `traefik.frontend.auth.digest.usersfile` | +| `traefik..frontend.auth.digest.usersFile=/path/.htdigest` | Same as `traefik.frontend.auth.digest.usersFile` | | `traefik..frontend.auth.forward.address=https://example.com`| Same as `traefik.frontend.auth.forward.address` | | `traefik..frontend.auth.forward.tls.ca=/path/ca.pem` | Same as `traefik.frontend.auth.forward.tls.ca` | | `traefik..frontend.auth.forward.tls.caOptional=true` | Same as `traefik.frontend.auth.forward.tls.caOptional` | From d04b4fa2cc2d56010ac4cfe2a9ba3da472e59e6c Mon Sep 17 00:00:00 2001 From: NicoMen Date: Wed, 8 Aug 2018 07:58:03 +0200 Subject: [PATCH 09/35] Set a keyType to ACME if the account is stored with no KeyType --- acme/acme.go | 3 ++ provider/acme/provider.go | 6 +++ provider/acme/provider_test.go | 80 ++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+) diff --git a/acme/acme.go b/acme/acme.go index 492439d83..3a7317562 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -202,6 +202,9 @@ func (a *ACME) leadershipListener(elected bool) error { } needRegister = true + } else if len(account.KeyType) == 0 { + // Set the KeyType if not already defined in the account + account.KeyType = acmeprovider.GetKeyType(a.KeyType) } a.client, err = a.buildACMEClient(account) diff --git a/provider/acme/provider.go b/provider/acme/provider.go index 4a3ccdec2..95b816b24 100644 --- a/provider/acme/provider.go +++ b/provider/acme/provider.go @@ -309,6 +309,12 @@ func (p *Provider) initAccount() (*Account, error) { return nil, err } } + + // Set the KeyType if not already defined in the account + if len(p.account.KeyType) == 0 { + p.account.KeyType = GetKeyType(p.KeyType) + } + return p.account, nil } diff --git a/provider/acme/provider_test.go b/provider/acme/provider_test.go index abbe34a0f..e2c948e36 100644 --- a/provider/acme/provider_test.go +++ b/provider/acme/provider_test.go @@ -8,6 +8,7 @@ import ( traefiktls "github.com/containous/traefik/tls" "github.com/containous/traefik/types" "github.com/stretchr/testify/assert" + "github.com/xenolf/lego/acme" ) func TestGetUncheckedCertificates(t *testing.T) { @@ -562,3 +563,82 @@ func TestUseBackOffToObtainCertificate(t *testing.T) { }) } } + +func TestInitAccount(t *testing.T) { + testCases := []struct { + desc string + account *Account + email string + keyType string + expectedAccount *Account + }{ + { + desc: "Existing account with all information", + account: &Account{ + Email: "foo@foo.net", + KeyType: acme.EC256, + }, + expectedAccount: &Account{ + Email: "foo@foo.net", + KeyType: acme.EC256, + }, + }, + { + desc: "Account nil", + email: "foo@foo.net", + keyType: "EC256", + expectedAccount: &Account{ + Email: "foo@foo.net", + KeyType: acme.EC256, + }, + }, + { + desc: "Existing account with no email", + account: &Account{ + KeyType: acme.RSA4096, + }, + email: "foo@foo.net", + keyType: "EC256", + expectedAccount: &Account{ + Email: "foo@foo.net", + KeyType: acme.EC256, + }, + }, + { + desc: "Existing account with no key type", + account: &Account{ + Email: "foo@foo.net", + }, + email: "bar@foo.net", + keyType: "EC256", + expectedAccount: &Account{ + Email: "foo@foo.net", + KeyType: acme.EC256, + }, + }, + { + desc: "Existing account and provider with no key type", + account: &Account{ + Email: "foo@foo.net", + }, + email: "bar@foo.net", + expectedAccount: &Account{ + Email: "foo@foo.net", + KeyType: acme.RSA4096, + }, + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + acmeProvider := Provider{account: test.account, Configuration: &Configuration{Email: test.email, KeyType: test.keyType}} + + actualAccount, err := acmeProvider.initAccount() + assert.Nil(t, err, "Init account in error") + assert.Equal(t, test.expectedAccount.Email, actualAccount.Email, "unexpected email account") + assert.Equal(t, test.expectedAccount.KeyType, actualAccount.KeyType, "unexpected keyType account") + }) + } +} From 60b4095c75b9ef417e5ad525832f3e37dab9acc0 Mon Sep 17 00:00:00 2001 From: macros Date: Wed, 8 Aug 2018 19:12:03 +0200 Subject: [PATCH 10/35] Set keepalive on TCP socket so idleTimeout works --- server/server.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/server/server.go b/server/server.go index adc384d7c..c15fb6927 100644 --- a/server/server.go +++ b/server/server.go @@ -90,6 +90,22 @@ type serverEntryPoint struct { onDemandListener func(string) (*tls.Certificate, error) } +// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted +// connections. +type tcpKeepAliveListener struct { + *net.TCPListener +} + +func (ln tcpKeepAliveListener) Accept() (net.Conn, error) { + tc, err := ln.AcceptTCP() + if err != nil { + return nil, err + } + tc.SetKeepAlive(true) + tc.SetKeepAlivePeriod(3 * time.Minute) + return tc, nil +} + // NewServer returns an initialized Server. func NewServer(globalConfiguration configuration.GlobalConfiguration, provider provider.Provider) *Server { server := new(Server) @@ -803,6 +819,8 @@ func (s *Server) prepareServer(entryPointName string, entryPoint *configuration. return nil, nil, err } + listener = tcpKeepAliveListener{listener.(*net.TCPListener)} + if entryPoint.ProxyProtocol != nil { IPs, err := whitelist.NewIP(entryPoint.ProxyProtocol.TrustedIPs, entryPoint.ProxyProtocol.Insecure, false) if err != nil { From 202783ca7dae57a2bf53520272f2901571377cc5 Mon Sep 17 00:00:00 2001 From: Manjunath A Kumatagi Date: Fri, 10 Aug 2018 20:26:04 +0530 Subject: [PATCH 11/35] Add ppc64le platform support --- script/crossbinary-others | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/script/crossbinary-others b/script/crossbinary-others index e05efacf7..91658887d 100755 --- a/script/crossbinary-others +++ b/script/crossbinary-others @@ -62,3 +62,13 @@ for OS in ${OS_PLATFORM_ARG[@]}; do done done done + +# Build ppc64le binaries +OS_PLATFORM_ARG=(linux) +OS_ARCH_ARG=(ppc64le) +for OS in ${OS_PLATFORM_ARG[@]}; do + 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/ + done +done From bb2686a08f8bd9fcff7cd3486a430b951caf9eb5 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 13 Aug 2018 08:46:02 +0200 Subject: [PATCH 12/35] Update ACME documentation about TLS-ALPN challenge --- docs/configuration/acme.md | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/docs/configuration/acme.md b/docs/configuration/acme.md index 07c10678f..b7990a765 100644 --- a/docs/configuration/acme.md +++ b/docs/configuration/acme.md @@ -102,29 +102,23 @@ entryPoint = "https" # # KeyType = "RSA4096" -# Domains list. -# Only domains defined here can generate wildcard certificates. -# -# [[acme.domains]] -# main = "local1.com" -# sans = ["test1.local1.com", "test2.local1.com"] -# [[acme.domains]] -# main = "local2.com" -# [[acme.domains]] -# main = "*.local3.com" -# sans = ["local3.com", "test1.test1.local3.com"] - -# Use a HTTP-01 ACME challenge. +# Use a TLS-ALPN-01 ACME challenge. # # Optional (but recommended) # -[acme.httpChallenge] +[acme.tlsChallenge] + +# Use a HTTP-01 ACME challenge. +# +# Optional +# +# [acme.httpChallenge] # EntryPoint to use for the HTTP-01 challenges. # # Required # - entryPoint = "http" + # entryPoint = "http" # Use a DNS-01 ACME challenge rather than HTTP-01 challenge. # Note: mandatory for wildcard certificate generation. @@ -147,6 +141,18 @@ entryPoint = "https" # Default: 0 # # delayBeforeCheck = 0 + +# Domains list. +# Only domains defined here can generate wildcard certificates. +# +# [[acme.domains]] +# main = "local1.com" +# sans = ["test1.local1.com", "test2.local1.com"] +# [[acme.domains]] +# main = "local2.com" +# [[acme.domains]] +# main = "*.local3.com" +# sans = ["local3.com", "test1.test1.local3.com"] ``` ### `caServer` @@ -164,7 +170,7 @@ caServer = "https://acme-staging-v02.api.letsencrypt.org/directory" ### ACME Challenge -#### TLS Challenge +#### `tlsChallenge` Use the `TLS-ALPN-01` challenge to generate and renew ACME certificates by provisioning a TLS certificate. From 278b3180c39cc9ffb288a72dec1cf5ad2b3c4cba Mon Sep 17 00:00:00 2001 From: Daniel Tomcej Date: Tue, 14 Aug 2018 08:48:04 -0600 Subject: [PATCH 13/35] Prevent unparsable strings from being rendered in the Kubernetes template --- provider/kubernetes/kubernetes.go | 12 +++++++++++ provider/kubernetes/kubernetes_test.go | 28 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index d7a4a3109..d23173287 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -883,7 +883,19 @@ func getFrontendRedirect(i *extensionsv1beta1.Ingress, baseName, path string) *t } redirectRegex := getStringValue(i.Annotations, annotationKubernetesRedirectRegex, "") + _, err := strconv.Unquote(`"` + redirectRegex + `"`) + if err != nil { + log.Debugf("Skipping Redirect on Ingress %s/%s due to invalid regex: %s", i.Namespace, i.Name, redirectRegex) + return nil + } + redirectReplacement := getStringValue(i.Annotations, annotationKubernetesRedirectReplacement, "") + _, err = strconv.Unquote(`"` + redirectReplacement + `"`) + if err != nil { + log.Debugf("Skipping Redirect on Ingress %s/%s due to invalid replacement: %q", i.Namespace, i.Name, redirectRegex) + return nil + } + if len(redirectRegex) > 0 && len(redirectReplacement) > 0 { return &types.Redirect{ Regex: redirectRegex, diff --git a/provider/kubernetes/kubernetes_test.go b/provider/kubernetes/kubernetes_test.go index 52d8c0e15..ee296dc3c 100644 --- a/provider/kubernetes/kubernetes_test.go +++ b/provider/kubernetes/kubernetes_test.go @@ -741,6 +741,34 @@ func TestGetPassTLSCert(t *testing.T) { assert.Equal(t, expected, actual) } +func TestInvalidRedirectAnnotation(t *testing.T) { + ingresses := []*extensionsv1beta1.Ingress{ + buildIngress(iNamespace("awesome"), + iAnnotation(annotationKubernetesRedirectRegex, `bad\.regex`), + iAnnotation(annotationKubernetesRedirectReplacement, "test"), + iRules(iRule( + iHost("foo"), + iPaths(onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))))), + ), + ), + buildIngress(iNamespace("awesome"), + iAnnotation(annotationKubernetesRedirectRegex, `test`), + iAnnotation(annotationKubernetesRedirectReplacement, `bad\.replacement`), + iRules(iRule( + iHost("foo"), + iPaths(onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))))), + ), + ), + } + + for _, ingress := range ingresses { + actual := getFrontendRedirect(ingress, "test", "/") + var expected *types.Redirect + + assert.Equal(t, expected, actual) + } +} + func TestOnlyReferencesServicesFromOwnNamespace(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress(iNamespace("awesome"), From bd3c8c3cde29ad241fb5d6036dbdcad620d1284a Mon Sep 17 00:00:00 2001 From: Daniel Tomcej Date: Tue, 14 Aug 2018 09:40:04 -0600 Subject: [PATCH 14/35] Don't merge kubernetes ingresses when priority is set --- provider/kubernetes/kubernetes.go | 6 +++++- provider/kubernetes/kubernetes_test.go | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index d23173287..d57834cc9 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -224,7 +224,12 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) } for _, pa := range r.HTTP.Paths { + priority := getIntValue(i.Annotations, annotationKubernetesPriority, 0) baseName := r.Host + pa.Path + if priority > 0 { + baseName = strconv.Itoa(priority) + "-" + baseName + } + if _, exists := templateObjects.Backends[baseName]; !exists { templateObjects.Backends[baseName] = &types.Backend{ Servers: make(map[string]types.Server), @@ -250,7 +255,6 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) passHostHeader := getBoolValue(i.Annotations, annotationKubernetesPreserveHost, !p.DisablePassHostHeaders) passTLSCert := getBoolValue(i.Annotations, annotationKubernetesPassTLSCert, p.EnablePassTLSCert) - priority := getIntValue(i.Annotations, annotationKubernetesPriority, 0) entryPoints := getSliceStringValue(i.Annotations, annotationKubernetesFrontendEntryPoints) templateObjects.Frontends[baseName] = &types.Frontend{ diff --git a/provider/kubernetes/kubernetes_test.go b/provider/kubernetes/kubernetes_test.go index ee296dc3c..b4595eb68 100644 --- a/provider/kubernetes/kubernetes_test.go +++ b/provider/kubernetes/kubernetes_test.go @@ -1849,13 +1849,13 @@ func TestPriorityHeaderValue(t *testing.T) { expected := buildConfiguration( backends( - backend("foo/bar", + backend("1337-foo/bar", servers(server("http://example.com", weight(1))), lbMethod("wrr"), ), ), frontends( - frontend("foo/bar", + frontend("1337-foo/bar", passHostHeader(), priority(1337), routes( From 870755e90d0f358ed859dc2f0cca32d84b344e42 Mon Sep 17 00:00:00 2001 From: Daniel Tomcej Date: Tue, 14 Aug 2018 10:38:04 -0600 Subject: [PATCH 15/35] Extend https redirection tests, and fix incorrect behavior --- .../fixtures/https/https_redirect.toml | 38 ++++- integration/https_test.go | 132 +++++++----------- middlewares/redirect/redirect.go | 18 +-- middlewares/replace_path.go | 11 +- middlewares/replace_path_regex.go | 2 + middlewares/stripPrefix.go | 12 +- middlewares/stripPrefixRegex.go | 8 +- 7 files changed, 110 insertions(+), 111 deletions(-) diff --git a/integration/fixtures/https/https_redirect.toml b/integration/fixtures/https/https_redirect.toml index 498d14f89..fc0cdf67c 100644 --- a/integration/fixtures/https/https_redirect.toml +++ b/integration/fixtures/https/https_redirect.toml @@ -22,15 +22,49 @@ defaultEntryPoints = ["http", "https"] weight = 1 [frontends] + [frontends.frontend1] backend = "backend1" [frontends.frontend1.routes.test_1] rule = "Host: example.com; PathPrefixStrip: /api" - [frontends.frontend2] + [frontends.frontend2] backend = "backend1" [frontends.frontend2.routes.test_1] - rule = "Host: test.com; AddPrefix: /foo" + rule = "Host: example2.com; PathPrefixStrip: /api/" + [frontends.frontend3] backend = "backend1" [frontends.frontend3.routes.test_1] + rule = "Host: test.com; AddPrefix: /foo" + [frontends.frontend4] + backend = "backend1" + [frontends.frontend4.routes.test_1] + rule = "Host: test2.com; AddPrefix: /foo/" + + [frontends.frontend5] + backend = "backend1" + [frontends.frontend5.routes.test_1] rule = "Host: foo.com; PathPrefixStripRegex: /{id:[a-z]+}" + [frontends.frontend6] + backend = "backend1" + [frontends.frontend6.routes.test_1] + rule = "Host: foo2.com; PathPrefixStripRegex: /{id:[a-z]+}/" + + [frontends.frontend7] + backend = "backend1" + [frontends.frontend7.routes.test_1] + rule = "Host: bar.com; ReplacePathRegex: /api /" + [frontends.frontend8] + backend = "backend1" + [frontends.frontend8.routes.test_1] + rule = "Host: bar2.com; ReplacePathRegex: /api/ /" + + [frontends.frontend9] + backend = "backend1" + [frontends.frontend9.routes.test_1] + rule = "Host: pow.com; ReplacePath: /api" + [frontends.frontend10] + backend = "backend1" + [frontends.frontend10.routes.test_1] + rule = "Host: pow2.com; ReplacePath: /api/" + diff --git a/integration/https_test.go b/integration/https_test.go index 5db8a54ab..cf6dee11e 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -3,6 +3,7 @@ package integration import ( "bytes" "crypto/tls" + "fmt" "net" "net/http" "net/http/httptest" @@ -740,7 +741,7 @@ func (s *HTTPSSuite) TestEntrypointHttpsRedirectAndPathModification(c *check.C) defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers", 500*time.Millisecond, try.BodyContains("Host: example.com")) + err = try.GetRequest("http://127.0.0.1:8080/api/providers", 1000*time.Millisecond, try.BodyContains("Host: example.com")) c.Assert(err, checker.IsNil) client := &http.Client{ @@ -750,115 +751,82 @@ func (s *HTTPSSuite) TestEntrypointHttpsRedirectAndPathModification(c *check.C) } testCases := []struct { - desc string - host string - sourceURL string - expectedURL string + desc string + hosts []string + path string }{ { - desc: "Stripped URL redirect", - host: "example.com", - sourceURL: "http://127.0.0.1:8888/api", - expectedURL: "https://example.com:8443/api", + desc: "Stripped URL redirect", + hosts: []string{"example.com", "foo.com", "bar.com"}, + path: "/api", }, { - desc: "Stripped URL with trailing slash redirect", - host: "example.com", - sourceURL: "http://127.0.0.1:8888/api/", - expectedURL: "https://example.com:8443/api/", + desc: "Stripped URL with trailing slash redirect", + hosts: []string{"example.com", "example2.com", "foo.com", "foo2.com", "bar.com", "bar2.com"}, + path: "/api/", }, { - desc: "Stripped URL with double trailing slash redirect", - host: "example.com", - sourceURL: "http://127.0.0.1:8888/api//", - expectedURL: "https://example.com:8443/api//", + desc: "Stripped URL with double trailing slash redirect", + hosts: []string{"example.com", "example2.com", "foo.com", "foo2.com", "bar.com", "bar2.com"}, + path: "/api//", }, { - desc: "Stripped URL with path redirect", - host: "example.com", - sourceURL: "http://127.0.0.1:8888/api/bacon", - expectedURL: "https://example.com:8443/api/bacon", + desc: "Stripped URL with path redirect", + hosts: []string{"example.com", "example2.com", "foo.com", "foo2.com", "bar.com", "bar2.com"}, + path: "/api/bacon", }, { - desc: "Stripped URL with path and trailing slash redirect", - host: "example.com", - sourceURL: "http://127.0.0.1:8888/api/bacon/", - expectedURL: "https://example.com:8443/api/bacon/", + desc: "Stripped URL with path and trailing slash redirect", + hosts: []string{"example.com", "example2.com", "foo.com", "foo2.com", "bar.com", "bar2.com"}, + path: "/api/bacon/", }, { - desc: "Stripped URL with path and double trailing slash redirect", - host: "example.com", - sourceURL: "http://127.0.0.1:8888/api/bacon//", - expectedURL: "https://example.com:8443/api/bacon//", + desc: "Stripped URL with path and double trailing slash redirect", + hosts: []string{"example.com", "example2.com", "foo.com", "foo2.com", "bar.com", "bar2.com"}, + path: "/api/bacon//", }, { - desc: "Root Path with redirect", - host: "test.com", - sourceURL: "http://127.0.0.1:8888/", - expectedURL: "https://test.com:8443/", + desc: "Root Path with redirect", + hosts: []string{"test.com", "test2.com", "pow.com", "pow2.com"}, + path: "/", }, { - desc: "Root Path with double trailing slash redirect", - host: "test.com", - sourceURL: "http://127.0.0.1:8888//", - expectedURL: "https://test.com:8443//", + desc: "Root Path with double trailing slash redirect", + hosts: []string{"test.com", "test2.com", "pow.com", "pow2.com"}, + path: "//", }, { - desc: "AddPrefix with redirect", - host: "test.com", - sourceURL: "http://127.0.0.1:8888/wtf", - expectedURL: "https://test.com:8443/wtf", + desc: "Path modify with redirect", + hosts: []string{"test.com", "test2.com", "pow.com", "pow2.com"}, + path: "/wtf", }, { - desc: "AddPrefix with trailing slash redirect", - host: "test.com", - sourceURL: "http://127.0.0.1:8888/wtf/", - expectedURL: "https://test.com:8443/wtf/", + desc: "Path modify with trailing slash redirect", + hosts: []string{"test.com", "test2.com", "pow.com", "pow2.com"}, + path: "/wtf/", }, { - desc: "AddPrefix with matching path segment redirect", - host: "test.com", - sourceURL: "http://127.0.0.1:8888/wtf/foo", - expectedURL: "https://test.com:8443/wtf/foo", - }, - { - desc: "Stripped URL Regex redirect", - host: "foo.com", - sourceURL: "http://127.0.0.1:8888/api", - expectedURL: "https://foo.com:8443/api", - }, - { - desc: "Stripped URL Regex with trailing slash redirect", - host: "foo.com", - sourceURL: "http://127.0.0.1:8888/api/", - expectedURL: "https://foo.com:8443/api/", - }, - { - desc: "Stripped URL Regex with path redirect", - host: "foo.com", - sourceURL: "http://127.0.0.1:8888/api/bacon", - expectedURL: "https://foo.com:8443/api/bacon", - }, - { - desc: "Stripped URL Regex with path and trailing slash redirect", - host: "foo.com", - sourceURL: "http://127.0.0.1:8888/api/bacon/", - expectedURL: "https://foo.com:8443/api/bacon/", + desc: "Path modify with matching path segment redirect", + hosts: []string{"test.com", "test2.com", "pow.com", "pow2.com"}, + path: "/wtf/foo", }, } for _, test := range testCases { - test := test + sourceURL := fmt.Sprintf("http://127.0.0.1:8888%s", test.path) + for _, host := range test.hosts { + req, err := http.NewRequest("GET", sourceURL, nil) + c.Assert(err, checker.IsNil) + req.Host = host - req, err := http.NewRequest("GET", test.sourceURL, nil) - c.Assert(err, checker.IsNil) - req.Host = test.host + resp, err := client.Do(req) + c.Assert(err, checker.IsNil) + defer resp.Body.Close() - resp, err := client.Do(req) - c.Assert(err, checker.IsNil) - defer resp.Body.Close() + location := resp.Header.Get("Location") + expected := fmt.Sprintf("https://%s:8443%s", host, test.path) - location := resp.Header.Get("Location") - c.Assert(location, checker.Equals, test.expectedURL) + c.Assert(location, checker.Equals, expected) + } } } diff --git a/middlewares/redirect/redirect.go b/middlewares/redirect/redirect.go index 1b1dfe7dd..506d7db3f 100644 --- a/middlewares/redirect/redirect.go +++ b/middlewares/redirect/redirect.go @@ -88,19 +88,7 @@ func (h *handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http if stripPrefix, stripPrefixOk := req.Context().Value(middlewares.StripPrefixKey).(string); stripPrefixOk { if len(stripPrefix) > 0 { - tempPath := parsedURL.Path parsedURL.Path = stripPrefix - if len(tempPath) > 0 && tempPath != "/" { - parsedURL.Path = stripPrefix + tempPath - } - - if trailingSlash, trailingSlashOk := req.Context().Value(middlewares.StripPrefixSlashKey).(bool); trailingSlashOk { - if trailingSlash { - if !strings.HasSuffix(parsedURL.Path, "/") { - parsedURL.Path = fmt.Sprintf("%s/", parsedURL.Path) - } - } - } } } @@ -110,6 +98,12 @@ func (h *handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http } } + if replacePath, replacePathOk := req.Context().Value(middlewares.ReplacePathKey).(string); replacePathOk { + if len(replacePath) > 0 { + parsedURL.Path = replacePath + } + } + if newURL != oldURL { handler := &moveHandler{location: parsedURL, permanent: h.permanent} handler.ServeHTTP(rw, req) diff --git a/middlewares/replace_path.go b/middlewares/replace_path.go index 40211c771..d5bd03eac 100644 --- a/middlewares/replace_path.go +++ b/middlewares/replace_path.go @@ -1,11 +1,17 @@ package middlewares import ( + "context" "net/http" ) -// ReplacedPathHeader is the default header to set the old path to -const ReplacedPathHeader = "X-Replaced-Path" +const ( + // ReplacePathKey is the key within the request context used to + // store the replaced path + ReplacePathKey key = "ReplacePath" + // ReplacedPathHeader is the default header to set the old path to + ReplacedPathHeader = "X-Replaced-Path" +) // ReplacePath is a middleware used to replace the path of a URL request type ReplacePath struct { @@ -14,6 +20,7 @@ type ReplacePath struct { } func (s *ReplacePath) ServeHTTP(w http.ResponseWriter, r *http.Request) { + r = r.WithContext(context.WithValue(r.Context(), ReplacePathKey, r.URL.Path)) r.Header.Add(ReplacedPathHeader, r.URL.Path) r.URL.Path = s.Path r.RequestURI = r.URL.RequestURI() diff --git a/middlewares/replace_path_regex.go b/middlewares/replace_path_regex.go index 4d97c0de5..d753e86c0 100644 --- a/middlewares/replace_path_regex.go +++ b/middlewares/replace_path_regex.go @@ -1,6 +1,7 @@ package middlewares import ( + "context" "net/http" "regexp" "strings" @@ -30,6 +31,7 @@ func NewReplacePathRegexHandler(regex string, replacement string, handler http.H func (s *ReplacePathRegex) ServeHTTP(w http.ResponseWriter, r *http.Request) { if s.Regexp != nil && len(s.Replacement) > 0 && s.Regexp.MatchString(r.URL.Path) { + r = r.WithContext(context.WithValue(r.Context(), ReplacePathKey, r.URL.Path)) r.Header.Add(ReplacedPathHeader, r.URL.Path) r.URL.Path = s.Regexp.ReplaceAllString(r.URL.Path, s.Replacement) r.RequestURI = r.URL.RequestURI() diff --git a/middlewares/stripPrefix.go b/middlewares/stripPrefix.go index f5295d94c..222eb33cd 100644 --- a/middlewares/stripPrefix.go +++ b/middlewares/stripPrefix.go @@ -10,9 +10,6 @@ const ( // StripPrefixKey is the key within the request context used to // store the stripped prefix StripPrefixKey key = "StripPrefix" - // StripPrefixSlashKey is the key within the request context used to - // store the stripped slash - StripPrefixSlashKey key = "StripPrefixSlash" // ForwardedPrefixHeader is the default header to set prefix ForwardedPrefixHeader = "X-Forwarded-Prefix" ) @@ -26,21 +23,20 @@ type StripPrefix struct { func (s *StripPrefix) ServeHTTP(w http.ResponseWriter, r *http.Request) { for _, prefix := range s.Prefixes { if strings.HasPrefix(r.URL.Path, prefix) { - trailingSlash := r.URL.Path == prefix+"/" + rawReqPath := r.URL.Path r.URL.Path = stripPrefix(r.URL.Path, prefix) if r.URL.RawPath != "" { r.URL.RawPath = stripPrefix(r.URL.RawPath, prefix) } - s.serveRequest(w, r, strings.TrimSpace(prefix), trailingSlash) + s.serveRequest(w, r, strings.TrimSpace(prefix), rawReqPath) return } } http.NotFound(w, r) } -func (s *StripPrefix) serveRequest(w http.ResponseWriter, r *http.Request, prefix string, trailingSlash bool) { - r = r.WithContext(context.WithValue(r.Context(), StripPrefixSlashKey, trailingSlash)) - r = r.WithContext(context.WithValue(r.Context(), StripPrefixKey, prefix)) +func (s *StripPrefix) serveRequest(w http.ResponseWriter, r *http.Request, prefix string, rawReqPath string) { + r = r.WithContext(context.WithValue(r.Context(), StripPrefixKey, rawReqPath)) r.Header.Add(ForwardedPrefixHeader, prefix) r.RequestURI = r.URL.RequestURI() s.Handler.ServeHTTP(w, r) diff --git a/middlewares/stripPrefixRegex.go b/middlewares/stripPrefixRegex.go index d86733dd6..c249f0fb2 100644 --- a/middlewares/stripPrefixRegex.go +++ b/middlewares/stripPrefixRegex.go @@ -39,16 +39,14 @@ func (s *StripPrefixRegex) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Error("Error in stripPrefix middleware", err) return } - - trailingSlash := r.URL.Path == prefix.Path+"/" + rawReqPath := r.URL.Path r.URL.Path = r.URL.Path[len(prefix.Path):] if r.URL.RawPath != "" { r.URL.RawPath = r.URL.RawPath[len(prefix.Path):] } - r = r.WithContext(context.WithValue(r.Context(), StripPrefixSlashKey, trailingSlash)) - r = r.WithContext(context.WithValue(r.Context(), StripPrefixKey, prefix.Path)) + r = r.WithContext(context.WithValue(r.Context(), StripPrefixKey, rawReqPath)) r.Header.Add(ForwardedPrefixHeader, prefix.Path) - r.RequestURI = r.URL.RequestURI() + r.RequestURI = ensureLeadingSlash(r.URL.RequestURI()) s.Handler.ServeHTTP(w, r) return } From d81c4e6d1a36f8289f2acb2980f81fbb0d69d555 Mon Sep 17 00:00:00 2001 From: NicoMen Date: Mon, 20 Aug 2018 09:40:03 +0200 Subject: [PATCH 16/35] Avoid duplicated ACME resolution --- acme/acme.go | 39 ++++++++++++++++ acme/acme_test.go | 10 ++++- .../acme/manage_acme_docker_environment.sh | 2 +- provider/acme/provider.go | 37 ++++++++++++++- provider/acme/provider_test.go | 45 +++++++++++++++++-- 5 files changed, 126 insertions(+), 7 deletions(-) diff --git a/acme/acme.go b/acme/acme.go index e31bbbdc3..8a06b823d 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -11,6 +11,7 @@ import ( "net/http" "reflect" "strings" + "sync" "time" "github.com/BurntSushi/ty/fun" @@ -61,6 +62,8 @@ type ACME struct { jobs *channels.InfiniteChannel TLSConfig *tls.Config `description:"TLS config in case wildcard certs are used"` dynamicCerts *safe.Safe + resolvingDomains map[string]struct{} + resolvingDomainsMutex sync.RWMutex } func (a *ACME) init() error { @@ -81,6 +84,10 @@ func (a *ACME) init() error { a.defaultCertificate = cert a.jobs = channels.NewInfiniteChannel() + + // Init the currently resolved domain map + a.resolvingDomains = make(map[string]struct{}) + return nil } @@ -502,6 +509,10 @@ func (a *ACME) LoadCertificateForDomains(domains []string) { if len(uncheckedDomains) == 0 { return } + + a.addResolvingDomains(uncheckedDomains) + defer a.removeResolvingDomains(uncheckedDomains) + certificate, err := a.getDomainsCertificates(uncheckedDomains) if err != nil { log.Errorf("Error getting ACME certificates %+v : %v", uncheckedDomains, err) @@ -533,6 +544,24 @@ func (a *ACME) LoadCertificateForDomains(domains []string) { } } +func (a *ACME) addResolvingDomains(resolvingDomains []string) { + a.resolvingDomainsMutex.Lock() + defer a.resolvingDomainsMutex.Unlock() + + for _, domain := range resolvingDomains { + a.resolvingDomains[domain] = struct{}{} + } +} + +func (a *ACME) removeResolvingDomains(resolvingDomains []string) { + a.resolvingDomainsMutex.Lock() + defer a.resolvingDomainsMutex.Unlock() + + for _, domain := range resolvingDomains { + delete(a.resolvingDomains, domain) + } +} + // Get provided certificate which check a domains list (Main and SANs) // from static and dynamic provided certificates func (a *ACME) getProvidedCertificate(domains string) *tls.Certificate { @@ -568,6 +597,9 @@ func searchProvidedCertificateForDomains(domain string, certs map[string]*tls.Ce // Get provided certificate which check a domains list (Main and SANs) // from static and dynamic provided certificates func (a *ACME) getUncheckedDomains(domains []string, account *Account) []string { + a.resolvingDomainsMutex.RLock() + defer a.resolvingDomainsMutex.RUnlock() + log.Debugf("Looking for provided certificate to validate %s...", domains) allCerts := make(map[string]*tls.Certificate) @@ -590,6 +622,13 @@ func (a *ACME) getUncheckedDomains(domains []string, account *Account) []string } } + // Get currently resolved domains + for domain := range a.resolvingDomains { + if _, ok := allCerts[domain]; !ok { + allCerts[domain] = &tls.Certificate{} + } + } + // Get Configuration Domains for i := 0; i < len(a.Domains); i++ { allCerts[a.Domains[i].Main] = &tls.Certificate{} diff --git a/acme/acme_test.go b/acme/acme_test.go index 9e3d2ace4..aadfa17b6 100644 --- a/acme/acme_test.go +++ b/acme/acme_test.go @@ -331,9 +331,12 @@ func TestAcme_getUncheckedCertificates(t *testing.T) { mm["*.containo.us"] = &tls.Certificate{} mm["traefik.acme.io"] = &tls.Certificate{} - a := ACME{TLSConfig: &tls.Config{NameToCertificate: mm}} + dm := make(map[string]struct{}) + dm["*.traefik.wtf"] = struct{}{} - domains := []string{"traefik.containo.us", "trae.containo.us"} + a := ACME{TLSConfig: &tls.Config{NameToCertificate: mm}, resolvingDomains: dm} + + domains := []string{"traefik.containo.us", "trae.containo.us", "foo.traefik.wtf"} uncheckedDomains := a.getUncheckedDomains(domains, nil) assert.Empty(t, uncheckedDomains) domains = []string{"traefik.acme.io", "trae.acme.io"} @@ -351,6 +354,9 @@ func TestAcme_getUncheckedCertificates(t *testing.T) { account := Account{DomainsCertificate: domainsCertificates} uncheckedDomains = a.getUncheckedDomains(domains, &account) assert.Empty(t, uncheckedDomains) + domains = []string{"traefik.containo.us", "trae.containo.us", "traefik.wtf"} + uncheckedDomains = a.getUncheckedDomains(domains, nil) + assert.Len(t, uncheckedDomains, 1) } func TestAcme_getProvidedCertificate(t *testing.T) { diff --git a/examples/acme/manage_acme_docker_environment.sh b/examples/acme/manage_acme_docker_environment.sh index a95483c9e..58cf73362 100755 --- a/examples/acme/manage_acme_docker_environment.sh +++ b/examples/acme/manage_acme_docker_environment.sh @@ -50,7 +50,7 @@ start_boulder() { # Script usage show_usage() { echo - echo "USAGE : manage_acme_docker_environment.sh [--start|--stop|--restart]" + echo "USAGE : manage_acme_docker_environment.sh [--dev|--start|--stop|--restart]" echo } diff --git a/provider/acme/provider.go b/provider/acme/provider.go index 958a77707..74adf62d4 100644 --- a/provider/acme/provider.go +++ b/provider/acme/provider.go @@ -63,6 +63,8 @@ type Provider struct { clientMutex sync.Mutex configFromListenerChan chan types.Configuration pool *safe.Pool + resolvingDomains map[string]struct{} + resolvingDomainsMutex sync.RWMutex } // Certificate is a struct which contains all data needed from an ACME certificate @@ -127,6 +129,9 @@ func (p *Provider) init() error { return fmt.Errorf("unable to get ACME certificates : %v", err) } + // Init the currently resolved domain map + p.resolvingDomains = make(map[string]struct{}) + p.watchCertificate() p.watchNewDomains() @@ -226,6 +231,9 @@ func (p *Provider) resolveCertificate(domain types.Domain, domainFromConfigurati return nil, nil } + p.addResolvingDomains(uncheckedDomains) + defer p.removeResolvingDomains(uncheckedDomains) + log.Debugf("Loading ACME certificates %+v...", uncheckedDomains) client, err := p.getClient() if err != nil { @@ -254,6 +262,24 @@ func (p *Provider) resolveCertificate(domain types.Domain, domainFromConfigurati return certificate, nil } +func (p *Provider) removeResolvingDomains(resolvingDomains []string) { + p.resolvingDomainsMutex.Lock() + defer p.resolvingDomainsMutex.Unlock() + + for _, domain := range resolvingDomains { + delete(p.resolvingDomains, domain) + } +} + +func (p *Provider) addResolvingDomains(resolvingDomains []string) { + p.resolvingDomainsMutex.Lock() + defer p.resolvingDomainsMutex.Unlock() + + for _, domain := range resolvingDomains { + p.resolvingDomains[domain] = struct{}{} + } +} + func (p *Provider) getClient() (*acme.Client, error) { p.clientMutex.Lock() defer p.clientMutex.Unlock() @@ -503,6 +529,9 @@ func (p *Provider) AddRoutes(router *mux.Router) { // Get provided certificate which check a domains list (Main and SANs) // from static and dynamic provided certificates func (p *Provider) getUncheckedDomains(domainsToCheck []string, checkConfigurationDomains bool) []string { + p.resolvingDomainsMutex.RLock() + defer p.resolvingDomainsMutex.RUnlock() + log.Debugf("Looking for provided certificate(s) to validate %q...", domainsToCheck) var allCerts []string @@ -523,6 +552,11 @@ func (p *Provider) getUncheckedDomains(domainsToCheck []string, checkConfigurati allCerts = append(allCerts, strings.Join(certificate.Domain.ToStrArray(), ",")) } + // Get currently resolved domains + for domain := range p.resolvingDomains { + allCerts = append(allCerts, domain) + } + // Get Configuration Domains if checkConfigurationDomains { for i := 0; i < len(p.Domains); i++ { @@ -540,8 +574,9 @@ func searchUncheckedDomains(domainsToCheck []string, existentDomains []string) [ uncheckedDomains = append(uncheckedDomains, domainToCheck) } } + if len(uncheckedDomains) == 0 { - log.Debugf("No ACME certificate to generate for domains %q.", domainsToCheck) + log.Debugf("No ACME certificate generation required for domains %q.", domainsToCheck) } else { log.Debugf("Domains %q need ACME certificates generation for domains %q.", domainsToCheck, strings.Join(uncheckedDomains, ",")) } diff --git a/provider/acme/provider_test.go b/provider/acme/provider_test.go index 2f6a3db96..5e79d60b8 100644 --- a/provider/acme/provider_test.go +++ b/provider/acme/provider_test.go @@ -26,6 +26,7 @@ func TestGetUncheckedCertificates(t *testing.T) { desc string dynamicCerts *safe.Safe staticCerts map[string]*tls.Certificate + resolvingDomains map[string]struct{} acmeCertificates []*Certificate domains []string expectedDomains []string @@ -138,17 +139,55 @@ func TestGetUncheckedCertificates(t *testing.T) { }, expectedDomains: []string{"traefik.wtf"}, }, + { + desc: "all domains already managed by ACME", + domains: []string{"traefik.wtf", "foo.traefik.wtf"}, + resolvingDomains: map[string]struct{}{ + "traefik.wtf": {}, + "foo.traefik.wtf": {}, + }, + expectedDomains: []string{}, + }, + { + desc: "one domain already managed by ACME", + domains: []string{"traefik.wtf", "foo.traefik.wtf"}, + resolvingDomains: map[string]struct{}{ + "traefik.wtf": {}, + }, + expectedDomains: []string{"foo.traefik.wtf"}, + }, + { + desc: "wildcard domain already managed by ACME checks the domains", + domains: []string{"bar.traefik.wtf", "foo.traefik.wtf"}, + resolvingDomains: map[string]struct{}{ + "*.traefik.wtf": {}, + }, + expectedDomains: []string{}, + }, + { + desc: "wildcard domain already managed by ACME checks domains and another domain checks one other domain, one domain still unchecked", + domains: []string{"traefik.wtf", "bar.traefik.wtf", "foo.traefik.wtf", "acme.wtf"}, + resolvingDomains: map[string]struct{}{ + "*.traefik.wtf": {}, + "traefik.wtf": {}, + }, + expectedDomains: []string{"acme.wtf"}, + }, } for _, test := range testCases { test := test + if test.resolvingDomains == nil { + test.resolvingDomains = make(map[string]struct{}) + } t.Run(test.desc, func(t *testing.T) { t.Parallel() acmeProvider := Provider{ - dynamicCerts: test.dynamicCerts, - staticCerts: test.staticCerts, - certificates: test.acmeCertificates, + dynamicCerts: test.dynamicCerts, + staticCerts: test.staticCerts, + certificates: test.acmeCertificates, + resolvingDomains: test.resolvingDomains, } domains := acmeProvider.getUncheckedDomains(test.domains, false) From 07be89d6e937baf00df30a7f254d19531aa6d45e Mon Sep 17 00:00:00 2001 From: SALLEYRON Julien Date: Mon, 20 Aug 2018 10:38:03 +0200 Subject: [PATCH 17/35] Update oxy dependency --- Gopkg.lock | 2 +- server/errorhandler.go | 20 ++- server/server.go | 117 ++++++++++++++++-- .../github.com/vulcand/oxy/buffer/buffer.go | 57 +++++---- .../vulcand/oxy/buffer/threshold.go | 1 + .../vulcand/oxy/cbreaker/cbreaker.go | 40 +++--- .../github.com/vulcand/oxy/cbreaker/effect.go | 18 ++- .../vulcand/oxy/cbreaker/fallback.go | 39 ++++-- .../vulcand/oxy/cbreaker/predicates.go | 3 +- .../github.com/vulcand/oxy/cbreaker/ratio.go | 12 +- .../vulcand/oxy/connlimit/connlimit.go | 36 ++++-- vendor/github.com/vulcand/oxy/forward/fwd.go | 84 ++++++++++--- .../github.com/vulcand/oxy/forward/headers.go | 6 +- .../github.com/vulcand/oxy/forward/rewrite.go | 7 +- .../vulcand/oxy/memmetrics/anomaly.go | 6 +- .../vulcand/oxy/memmetrics/counter.go | 12 +- .../vulcand/oxy/memmetrics/histogram.go | 30 +++-- .../vulcand/oxy/memmetrics/ratio.go | 17 +++ .../vulcand/oxy/memmetrics/roundtrip.go | 32 +++-- .../vulcand/oxy/ratelimit/bucket.go | 9 +- .../vulcand/oxy/ratelimit/bucketset.go | 8 +- .../vulcand/oxy/ratelimit/tokenlimiter.go | 49 ++++++-- .../oxy/roundrobin/RequestRewriteListener.go | 1 + .../vulcand/oxy/roundrobin/rebalancer.go | 62 +++++++--- .../github.com/vulcand/oxy/roundrobin/rr.go | 41 ++++-- .../vulcand/oxy/roundrobin/stickysessions.go | 9 +- vendor/github.com/vulcand/oxy/utils/auth.go | 2 + .../github.com/vulcand/oxy/utils/dumpreq.go | 14 ++- .../github.com/vulcand/oxy/utils/handler.go | 30 ++++- .../github.com/vulcand/oxy/utils/netutils.go | 55 +++++--- vendor/github.com/vulcand/oxy/utils/source.go | 12 +- 31 files changed, 636 insertions(+), 195 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 3cfee2ff8..7371071c5 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1217,7 +1217,7 @@ "roundrobin", "utils" ] - revision = "c2414f4542f085363f490048da2fbec5e4537eb6" + revision = "885e42fe04d8e0efa6c18facad4e0fc5757cde9b" [[projects]] name = "github.com/vulcand/predicate" diff --git a/server/errorhandler.go b/server/errorhandler.go index 732b2cc0e..de58045ed 100644 --- a/server/errorhandler.go +++ b/server/errorhandler.go @@ -1,6 +1,7 @@ package server import ( + "context" "io" "net" "net/http" @@ -9,6 +10,12 @@ import ( "github.com/containous/traefik/middlewares" ) +// StatusClientClosedRequest non-standard HTTP status code for client disconnection +const StatusClientClosedRequest = 499 + +// StatusClientClosedRequestText non-standard HTTP status for client disconnection +const StatusClientClosedRequestText = "Client Closed Request" + // RecordingErrorHandler is an error handler, implementing the vulcand/oxy // error handler interface, which is recording network errors by using the netErrorRecorder. // In addition it sets a proper HTTP status code and body, depending on the type of error occurred. @@ -34,9 +41,18 @@ func (eh *RecordingErrorHandler) ServeHTTP(w http.ResponseWriter, req *http.Requ } else if err == io.EOF { eh.netErrorRecorder.Record(req.Context()) statusCode = http.StatusBadGateway + } else if err == context.Canceled { + statusCode = StatusClientClosedRequest } w.WriteHeader(statusCode) - w.Write([]byte(http.StatusText(statusCode))) - log.Debugf("'%d %s' caused by: %v", statusCode, http.StatusText(statusCode), err) + w.Write([]byte(statusText(statusCode))) + log.Debugf("'%d %s' caused by: %v", statusCode, statusText(statusCode), err) +} + +func statusText(statusCode int) string { + if statusCode == StatusClientClosedRequest { + return StatusClientClosedRequestText + } + return http.StatusText(statusCode) } diff --git a/server/server.go b/server/server.go index c15fb6927..5db244e92 100644 --- a/server/server.go +++ b/server/server.go @@ -80,14 +80,96 @@ type Server struct { bufferPool httputil.BufferPool } +func newHijackConnectionTracker() *hijackConnectionTracker { + return &hijackConnectionTracker{ + conns: make(map[net.Conn]struct{}), + } +} + +type hijackConnectionTracker struct { + conns map[net.Conn]struct{} + lock sync.RWMutex +} + +// AddHijackedConnection add a connection in the tracked connections list +func (h *hijackConnectionTracker) AddHijackedConnection(conn net.Conn) { + h.lock.Lock() + defer h.lock.Unlock() + h.conns[conn] = struct{}{} +} + +// RemoveHijackedConnection remove a connection from the tracked connections list +func (h *hijackConnectionTracker) RemoveHijackedConnection(conn net.Conn) { + h.lock.Lock() + defer h.lock.Unlock() + delete(h.conns, conn) +} + +// Shutdown wait for the connection closing +func (h *hijackConnectionTracker) Shutdown(ctx context.Context) error { + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + for { + h.lock.RLock() + if len(h.conns) == 0 { + return nil + } + h.lock.RUnlock() + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + } + } +} + +// Close close all the connections in the tracked connections list +func (h *hijackConnectionTracker) Close() { + for conn := range h.conns { + if err := conn.Close(); err != nil { + log.Errorf("Error while closing Hijacked conn: %v", err) + } + delete(h.conns, conn) + } +} + type serverEntryPoints map[string]*serverEntryPoint type serverEntryPoint struct { - httpServer *http.Server - listener net.Listener - httpRouter *middlewares.HandlerSwitcher - certs safe.Safe - onDemandListener func(string) (*tls.Certificate, error) + httpServer *http.Server + listener net.Listener + httpRouter *middlewares.HandlerSwitcher + certs safe.Safe + onDemandListener func(string) (*tls.Certificate, error) + hijackConnectionTracker *hijackConnectionTracker +} + +func (s serverEntryPoint) Shutdown(ctx context.Context) { + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + if err := s.httpServer.Shutdown(ctx); err != nil { + if ctx.Err() == context.DeadlineExceeded { + log.Debugf("Wait server shutdown is over due to: %s", err) + err = s.httpServer.Close() + if err != nil { + log.Error(err) + } + } + } + }() + wg.Add(1) + go func() { + defer wg.Done() + if err := s.hijackConnectionTracker.Shutdown(ctx); err != nil { + if ctx.Err() == context.DeadlineExceeded { + log.Debugf("Wait hijack connection is over due to: %s", err) + s.hijackConnectionTracker.Close() + } + } + }() + wg.Wait() } // tcpKeepAliveListener sets TCP keep-alive timeouts on accepted @@ -260,10 +342,7 @@ func (s *Server) Stop() { graceTimeOut := time.Duration(s.globalConfiguration.LifeCycle.GraceTimeOut) ctx, cancel := context.WithTimeout(context.Background(), graceTimeOut) log.Debugf("Waiting %s seconds before killing connections on entrypoint %s...", graceTimeOut, serverEntryPointName) - if err := serverEntryPoint.httpServer.Shutdown(ctx); err != nil { - log.Debugf("Wait is over due to: %s", err) - serverEntryPoint.httpServer.Close() - } + serverEntryPoint.Shutdown(ctx) cancel() log.Debugf("Entrypoint %s closed", serverEntryPointName) }(sepn, sep) @@ -376,9 +455,20 @@ func (s *Server) setupServerEntryPoint(newServerEntryPointName string, newServer log.Fatal("Error preparing server: ", err) } serverEntryPoint := s.serverEntryPoints[newServerEntryPointName] + serverEntryPoint.httpServer = newSrv serverEntryPoint.listener = listener + serverEntryPoint.hijackConnectionTracker = newHijackConnectionTracker() + serverEntryPoint.httpServer.ConnState = func(conn net.Conn, state http.ConnState) { + switch state { + case http.StateHijacked: + serverEntryPoint.hijackConnectionTracker.AddHijackedConnection(conn) + case http.StateClosed: + serverEntryPoint.hijackConnectionTracker.RemoveHijackedConnection(conn) + } + } + return serverEntryPoint } @@ -1025,6 +1115,15 @@ func (s *Server) loadConfig(configurations types.Configurations, globalConfigura forward.Rewriter(rewriter), forward.ResponseModifier(responseModifier), forward.BufferPool(s.bufferPool), + forward.WebsocketConnectionClosedHook(func(req *http.Request, conn net.Conn) { + server := req.Context().Value(http.ServerContextKey).(*http.Server) + if server != nil { + connState := server.ConnState + if connState != nil { + connState(conn, http.StateClosed) + } + } + }), ) if err != nil { diff --git a/vendor/github.com/vulcand/oxy/buffer/buffer.go b/vendor/github.com/vulcand/oxy/buffer/buffer.go index e3ee40b33..d2bbe40ce 100644 --- a/vendor/github.com/vulcand/oxy/buffer/buffer.go +++ b/vendor/github.com/vulcand/oxy/buffer/buffer.go @@ -36,13 +36,12 @@ Examples of a buffering middleware: package buffer import ( + "bufio" "fmt" "io" "io/ioutil" - "net/http" - - "bufio" "net" + "net/http" "reflect" "github.com/mailgun/multibuf" @@ -74,6 +73,8 @@ type Buffer struct { next http.Handler errHandler utils.ErrorHandler + + log *log.Logger } // New returns a new buffer middleware. New() function supports optional functional arguments @@ -86,6 +87,8 @@ func New(next http.Handler, setters ...optSetter) (*Buffer, error) { maxResponseBodyBytes: DefaultMaxBodyBytes, memResponseBodyBytes: DefaultMemBodyBytes, + + log: log.StandardLogger(), } for _, s := range setters { if err := s(strm); err != nil { @@ -99,6 +102,16 @@ func New(next http.Handler, setters ...optSetter) (*Buffer, error) { return strm, nil } +// Logger defines the logger the buffer will use. +// +// It defaults to logrus.StandardLogger(), the global logger used by logrus. +func Logger(l *log.Logger) optSetter { + return func(b *Buffer) error { + b.log = l + return nil + } +} + type optSetter func(b *Buffer) error // CondSetter Conditional setter. @@ -154,7 +167,7 @@ func MaxRequestBodyBytes(m int64) optSetter { } } -// MaxRequestBody bytes sets the maximum request body to be stored in memory +// MemRequestBodyBytes bytes sets the maximum request body to be stored in memory // buffer middleware will serialize the excess to disk. func MemRequestBodyBytes(m int64) optSetter { return func(b *Buffer) error { @@ -196,8 +209,8 @@ func (b *Buffer) Wrap(next http.Handler) error { } func (b *Buffer) ServeHTTP(w http.ResponseWriter, req *http.Request) { - if log.GetLevel() >= log.DebugLevel { - logEntry := log.WithField("Request", utils.DumpHttpRequest(req)) + if b.log.Level >= log.DebugLevel { + logEntry := b.log.WithField("Request", utils.DumpHttpRequest(req)) logEntry.Debug("vulcand/oxy/buffer: begin ServeHttp on request") defer logEntry.Debug("vulcand/oxy/buffer: completed ServeHttp on request") } @@ -210,11 +223,11 @@ func (b *Buffer) ServeHTTP(w http.ResponseWriter, req *http.Request) { // Read the body while keeping limits in mind. This reader controls the maximum bytes // to read into memory and disk. This reader returns an error if the total request size exceeds the - // prefefined MaxSizeBytes. This can occur if we got chunked request, in this case ContentLength would be set to -1 + // predefined MaxSizeBytes. This can occur if we got chunked request, in this case ContentLength would be set to -1 // and the reader would be unbounded bufio in the http.Server body, err := multibuf.New(req.Body, multibuf.MaxBytes(b.maxRequestBodyBytes), multibuf.MemBytes(b.memRequestBodyBytes)) if err != nil || body == nil { - log.Errorf("vulcand/oxy/buffer: error when reading request body, err: %v", err) + b.log.Errorf("vulcand/oxy/buffer: error when reading request body, err: %v", err) b.errHandler.ServeHTTP(w, req, err) return } @@ -235,7 +248,7 @@ func (b *Buffer) ServeHTTP(w http.ResponseWriter, req *http.Request) { // set without content length or using chunked TransferEncoding totalSize, err := body.Size() if err != nil { - log.Errorf("vulcand/oxy/buffer: failed to get request size, err: %v", err) + b.log.Errorf("vulcand/oxy/buffer: failed to get request size, err: %v", err) b.errHandler.ServeHTTP(w, req, err) return } @@ -251,7 +264,7 @@ func (b *Buffer) ServeHTTP(w http.ResponseWriter, req *http.Request) { // We create a special writer that will limit the response size, buffer it to disk if necessary writer, err := multibuf.NewWriterOnce(multibuf.MaxBytes(b.maxResponseBodyBytes), multibuf.MemBytes(b.memResponseBodyBytes)) if err != nil { - log.Errorf("vulcand/oxy/buffer: failed create response writer, err: %v", err) + b.log.Errorf("vulcand/oxy/buffer: failed create response writer, err: %v", err) b.errHandler.ServeHTTP(w, req, err) return } @@ -261,12 +274,13 @@ func (b *Buffer) ServeHTTP(w http.ResponseWriter, req *http.Request) { header: make(http.Header), buffer: writer, responseWriter: w, + log: b.log, } defer bw.Close() b.next.ServeHTTP(bw, outreq) if bw.hijacked { - log.Debugf("vulcand/oxy/buffer: connection was hijacked downstream. Not taking any action in buffer.") + b.log.Debugf("vulcand/oxy/buffer: connection was hijacked downstream. Not taking any action in buffer.") return } @@ -274,7 +288,7 @@ func (b *Buffer) ServeHTTP(w http.ResponseWriter, req *http.Request) { if bw.expectBody(outreq) { rdr, err := writer.Reader() if err != nil { - log.Errorf("vulcand/oxy/buffer: failed to read response, err: %v", err) + b.log.Errorf("vulcand/oxy/buffer: failed to read response, err: %v", err) b.errHandler.ServeHTTP(w, req, err) return } @@ -292,17 +306,17 @@ func (b *Buffer) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - attempt += 1 + attempt++ if body != nil { if _, err := body.Seek(0, 0); err != nil { - log.Errorf("vulcand/oxy/buffer: failed to rewind response body, err: %v", err) + b.log.Errorf("vulcand/oxy/buffer: failed to rewind response body, err: %v", err) b.errHandler.ServeHTTP(w, req, err) return } } outreq = b.copyRequest(req, body, totalSize) - log.Debugf("vulcand/oxy/buffer: retry Request(%v %v) attempt %v", req.Method, req.URL, attempt) + b.log.Debugf("vulcand/oxy/buffer: retry Request(%v %v) attempt %v", req.Method, req.URL, attempt) } } @@ -339,6 +353,7 @@ type bufferWriter struct { buffer multibuf.WriterOnce responseWriter http.ResponseWriter hijacked bool + log *log.Logger } // RFC2616 #4.4 @@ -376,16 +391,16 @@ func (b *bufferWriter) WriteHeader(code int) { b.code = code } -//CloseNotifier interface - this allows downstream connections to be terminated when the client terminates. +// CloseNotifier interface - this allows downstream connections to be terminated when the client terminates. func (b *bufferWriter) CloseNotify() <-chan bool { if cn, ok := b.responseWriter.(http.CloseNotifier); ok { return cn.CloseNotify() } - log.Warningf("Upstream ResponseWriter of type %v does not implement http.CloseNotifier. Returning dummy channel.", reflect.TypeOf(b.responseWriter)) + b.log.Warningf("Upstream ResponseWriter of type %v does not implement http.CloseNotifier. Returning dummy channel.", reflect.TypeOf(b.responseWriter)) return make(<-chan bool) } -//This allows connections to be hijacked for websockets for instance. +// Hijack This allows connections to be hijacked for websockets for instance. func (b *bufferWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { if hi, ok := b.responseWriter.(http.Hijacker); ok { conn, rw, err := hi.Hijack() @@ -394,12 +409,12 @@ func (b *bufferWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { } return conn, rw, err } - log.Warningf("Upstream ResponseWriter of type %v does not implement http.Hijacker. Returning dummy channel.", reflect.TypeOf(b.responseWriter)) + b.log.Warningf("Upstream ResponseWriter of type %v does not implement http.Hijacker. Returning dummy channel.", reflect.TypeOf(b.responseWriter)) return nil, nil, fmt.Errorf("The response writer that was wrapped in this proxy, does not implement http.Hijacker. It is of type: %v", reflect.TypeOf(b.responseWriter)) } -type SizeErrHandler struct { -} +// SizeErrHandler Size error handler +type SizeErrHandler struct{} func (e *SizeErrHandler) ServeHTTP(w http.ResponseWriter, req *http.Request, err error) { if _, ok := err.(*multibuf.MaxSizeReachedError); ok { diff --git a/vendor/github.com/vulcand/oxy/buffer/threshold.go b/vendor/github.com/vulcand/oxy/buffer/threshold.go index 1294bcd60..0fdde7da4 100644 --- a/vendor/github.com/vulcand/oxy/buffer/threshold.go +++ b/vendor/github.com/vulcand/oxy/buffer/threshold.go @@ -7,6 +7,7 @@ import ( "github.com/vulcand/predicate" ) +// IsValidExpression check if it's a valid expression func IsValidExpression(expr string) bool { _, err := parseExpression(expr) return err == nil diff --git a/vendor/github.com/vulcand/oxy/cbreaker/cbreaker.go b/vendor/github.com/vulcand/oxy/cbreaker/cbreaker.go index 5991a8474..4c35f1365 100644 --- a/vendor/github.com/vulcand/oxy/cbreaker/cbreaker.go +++ b/vendor/github.com/vulcand/oxy/cbreaker/cbreaker.go @@ -3,7 +3,7 @@ // Vulcan circuit breaker watches the error condtion to match // after which it activates the fallback scenario, e.g. returns the response code // or redirects the request to another location - +// // Circuit breakers start in the Standby state first, observing responses and watching location metrics. // // Once the Circuit breaker condition is met, it enters the "Tripped" state, where it activates fallback scenario @@ -31,9 +31,8 @@ import ( "sync" "time" - log "github.com/sirupsen/logrus" - "github.com/mailgun/timetools" + log "github.com/sirupsen/logrus" "github.com/vulcand/oxy/memmetrics" "github.com/vulcand/oxy/utils" ) @@ -63,6 +62,8 @@ type CircuitBreaker struct { next http.Handler clock timetools.TimeProvider + + log *log.Logger } // New creates a new CircuitBreaker middleware @@ -76,6 +77,7 @@ func New(next http.Handler, expression string, options ...CircuitBreakerOption) fallbackDuration: defaultFallbackDuration, recoveryDuration: defaultRecoveryDuration, fallback: defaultFallback, + log: log.StandardLogger(), } for _, s := range options { @@ -99,9 +101,19 @@ func New(next http.Handler, expression string, options ...CircuitBreakerOption) return cb, nil } +// Logger defines the logger the circuit breaker will use. +// +// It defaults to logrus.StandardLogger(), the global logger used by logrus. +func Logger(l *log.Logger) CircuitBreakerOption { + return func(c *CircuitBreaker) error { + c.log = l + return nil + } +} + func (c *CircuitBreaker) ServeHTTP(w http.ResponseWriter, req *http.Request) { - if log.GetLevel() >= log.DebugLevel { - logEntry := log.WithField("Request", utils.DumpHttpRequest(req)) + if c.log.Level >= log.DebugLevel { + logEntry := c.log.WithField("Request", utils.DumpHttpRequest(req)) logEntry.Debug("vulcand/oxy/circuitbreaker: begin ServeHttp on request") defer logEntry.Debug("vulcand/oxy/circuitbreaker: completed ServeHttp on request") } @@ -112,6 +124,7 @@ func (c *CircuitBreaker) ServeHTTP(w http.ResponseWriter, req *http.Request) { c.serve(w, req) } +// Wrap sets the next handler to be called by circuit breaker handler. func (c *CircuitBreaker) Wrap(next http.Handler) { c.next = next } @@ -126,7 +139,7 @@ func (c *CircuitBreaker) activateFallback(w http.ResponseWriter, req *http.Reque c.m.Lock() defer c.m.Unlock() - log.Warnf("%v is in error state", c) + c.log.Warnf("%v is in error state", c) switch c.state { case stateStandby: @@ -156,7 +169,7 @@ func (c *CircuitBreaker) activateFallback(w http.ResponseWriter, req *http.Reque func (c *CircuitBreaker) serve(w http.ResponseWriter, req *http.Request) { start := c.clock.UtcNow() - p := utils.NewProxyWriter(w) + p := utils.NewProxyWriterWithLogger(w, c.log) c.next.ServeHTTP(p, req) @@ -191,13 +204,13 @@ func (c *CircuitBreaker) exec(s SideEffect) { } go func() { if err := s.Exec(); err != nil { - log.Errorf("%v side effect failure: %v", c, err) + c.log.Errorf("%v side effect failure: %v", c, err) } }() } func (c *CircuitBreaker) setState(new cbState, until time.Time) { - log.Debugf("%v setting state to %v, until %v", c, new, until) + c.log.Debugf("%v setting state to %v, until %v", c, new, until) c.state = new c.until = until switch new { @@ -230,7 +243,7 @@ func (c *CircuitBreaker) checkAndSet() { c.lastCheck = c.clock.UtcNow().Add(c.checkPeriod) if c.state == stateTripped { - log.Debugf("%v skip set tripped", c) + c.log.Debugf("%v skip set tripped", c) return } @@ -244,7 +257,7 @@ func (c *CircuitBreaker) checkAndSet() { func (c *CircuitBreaker) setRecovering() { c.setState(stateRecovering, c.clock.UtcNow().Add(c.recoveryDuration)) - c.rc = newRatioController(c.clock, c.recoveryDuration) + c.rc = newRatioController(c.clock, c.recoveryDuration, c.log) } // CircuitBreakerOption represents an option you can pass to New. @@ -296,7 +309,7 @@ func OnTripped(s SideEffect) CircuitBreakerOption { } } -// OnTripped sets a SideEffect to run when entering the Standby state. +// OnStandby sets a SideEffect to run when entering the Standby state. // Only one SideEffect can be set for this hook. func OnStandby(s SideEffect) CircuitBreakerOption { return func(c *CircuitBreaker) error { @@ -346,8 +359,7 @@ const ( var defaultFallback = &fallback{} -type fallback struct { -} +type fallback struct{} func (f *fallback) ServeHTTP(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusServiceUnavailable) diff --git a/vendor/github.com/vulcand/oxy/cbreaker/effect.go b/vendor/github.com/vulcand/oxy/cbreaker/effect.go index 821115491..88aae1426 100644 --- a/vendor/github.com/vulcand/oxy/cbreaker/effect.go +++ b/vendor/github.com/vulcand/oxy/cbreaker/effect.go @@ -13,10 +13,12 @@ import ( "github.com/vulcand/oxy/utils" ) +// SideEffect a side effect type SideEffect interface { Exec() error } +// Webhook Web hook type Webhook struct { URL string Method string @@ -25,11 +27,15 @@ type Webhook struct { Body []byte } +// WebhookSideEffect a web hook side effect type WebhookSideEffect struct { w Webhook + + log *log.Logger } -func NewWebhookSideEffect(w Webhook) (*WebhookSideEffect, error) { +// NewWebhookSideEffectsWithLogger creates a new WebhookSideEffect +func NewWebhookSideEffectsWithLogger(w Webhook, l *log.Logger) (*WebhookSideEffect, error) { if w.Method == "" { return nil, fmt.Errorf("Supply method") } @@ -38,7 +44,12 @@ func NewWebhookSideEffect(w Webhook) (*WebhookSideEffect, error) { return nil, err } - return &WebhookSideEffect{w: w}, nil + return &WebhookSideEffect{w: w, log: l}, nil +} + +// NewWebhookSideEffect creates a new WebhookSideEffect +func NewWebhookSideEffect(w Webhook) (*WebhookSideEffect, error) { + return NewWebhookSideEffectsWithLogger(w, log.StandardLogger()) } func (w *WebhookSideEffect) getBody() io.Reader { @@ -51,6 +62,7 @@ func (w *WebhookSideEffect) getBody() io.Reader { return nil } +// Exec execute the side effect func (w *WebhookSideEffect) Exec() error { r, err := http.NewRequest(w.w.Method, w.w.URL, w.getBody()) if err != nil { @@ -73,6 +85,6 @@ func (w *WebhookSideEffect) Exec() error { if err != nil { return err } - log.Debugf("%v got response: (%s): %s", w, re.Status, string(body)) + w.log.Debugf("%v got response: (%s): %s", w, re.Status, string(body)) return nil } diff --git a/vendor/github.com/vulcand/oxy/cbreaker/fallback.go b/vendor/github.com/vulcand/oxy/cbreaker/fallback.go index a4fed70af..ea0655311 100644 --- a/vendor/github.com/vulcand/oxy/cbreaker/fallback.go +++ b/vendor/github.com/vulcand/oxy/cbreaker/fallback.go @@ -10,26 +10,36 @@ import ( "github.com/vulcand/oxy/utils" ) +// Response response model type Response struct { StatusCode int ContentType string Body []byte } +// ResponseFallback fallback response handler type ResponseFallback struct { r Response + + log *log.Logger } -func NewResponseFallback(r Response) (*ResponseFallback, error) { +// NewResponseFallbackWithLogger creates a new ResponseFallback +func NewResponseFallbackWithLogger(r Response, l *log.Logger) (*ResponseFallback, error) { if r.StatusCode == 0 { return nil, fmt.Errorf("response code should not be 0") } - return &ResponseFallback{r: r}, nil + return &ResponseFallback{r: r, log: l}, nil +} + +// NewResponseFallback creates a new ResponseFallback +func NewResponseFallback(r Response) (*ResponseFallback, error) { + return NewResponseFallbackWithLogger(r, log.StandardLogger()) } func (f *ResponseFallback) ServeHTTP(w http.ResponseWriter, req *http.Request) { - if log.GetLevel() >= log.DebugLevel { - logEntry := log.WithField("Request", utils.DumpHttpRequest(req)) + if f.log.Level >= log.DebugLevel { + logEntry := f.log.WithField("Request", utils.DumpHttpRequest(req)) logEntry.Debug("vulcand/oxy/fallback/response: begin ServeHttp on request") defer logEntry.Debug("vulcand/oxy/fallback/response: completed ServeHttp on request") } @@ -45,27 +55,38 @@ func (f *ResponseFallback) ServeHTTP(w http.ResponseWriter, req *http.Request) { } } +// Redirect redirect model type Redirect struct { URL string PreservePath bool } +// RedirectFallback fallback redirect handler type RedirectFallback struct { - u *url.URL r Redirect + + u *url.URL + + log *log.Logger } -func NewRedirectFallback(r Redirect) (*RedirectFallback, error) { +// NewRedirectFallbackWithLogger creates a new RedirectFallback +func NewRedirectFallbackWithLogger(r Redirect, l *log.Logger) (*RedirectFallback, error) { u, err := url.ParseRequestURI(r.URL) if err != nil { return nil, err } - return &RedirectFallback{u: u, r: r}, nil + return &RedirectFallback{r: r, u: u, log: l}, nil +} + +// NewRedirectFallback creates a new RedirectFallback +func NewRedirectFallback(r Redirect) (*RedirectFallback, error) { + return NewRedirectFallbackWithLogger(r, log.StandardLogger()) } func (f *RedirectFallback) ServeHTTP(w http.ResponseWriter, req *http.Request) { - if log.GetLevel() >= log.DebugLevel { - logEntry := log.WithField("Request", utils.DumpHttpRequest(req)) + if f.log.Level >= log.DebugLevel { + logEntry := f.log.WithField("Request", utils.DumpHttpRequest(req)) logEntry.Debug("vulcand/oxy/fallback/redirect: begin ServeHttp on request") defer logEntry.Debug("vulcand/oxy/fallback/redirect: completed ServeHttp on request") } diff --git a/vendor/github.com/vulcand/oxy/cbreaker/predicates.go b/vendor/github.com/vulcand/oxy/cbreaker/predicates.go index e703156d3..a858daf8c 100644 --- a/vendor/github.com/vulcand/oxy/cbreaker/predicates.go +++ b/vendor/github.com/vulcand/oxy/cbreaker/predicates.go @@ -4,7 +4,6 @@ import ( "fmt" "time" - log "github.com/sirupsen/logrus" "github.com/vulcand/predicate" ) @@ -50,7 +49,7 @@ func latencyAtQuantile(quantile float64) toInt { return func(c *CircuitBreaker) int { h, err := c.metrics.LatencyHistogram() if err != nil { - log.Errorf("Failed to get latency histogram, for %v error: %v", c, err) + c.log.Errorf("Failed to get latency histogram, for %v error: %v", c, err) return 0 } return int(h.LatencyAtQuantile(quantile) / time.Millisecond) diff --git a/vendor/github.com/vulcand/oxy/cbreaker/ratio.go b/vendor/github.com/vulcand/oxy/cbreaker/ratio.go index 4918ab8bf..96f9eeb7b 100644 --- a/vendor/github.com/vulcand/oxy/cbreaker/ratio.go +++ b/vendor/github.com/vulcand/oxy/cbreaker/ratio.go @@ -19,13 +19,17 @@ type ratioController struct { tm timetools.TimeProvider allowed int denied int + + log *log.Logger } -func newRatioController(tm timetools.TimeProvider, rampUp time.Duration) *ratioController { +func newRatioController(tm timetools.TimeProvider, rampUp time.Duration, log *log.Logger) *ratioController { return &ratioController{ duration: rampUp, tm: tm, start: tm.UtcNow(), + + log: log, } } @@ -34,17 +38,17 @@ func (r *ratioController) String() string { } func (r *ratioController) allowRequest() bool { - log.Debugf("%v", r) + r.log.Debugf("%v", r) t := r.targetRatio() // This condition answers the question - would we satisfy the target ratio if we allow this request? e := r.computeRatio(r.allowed+1, r.denied) if e < t { r.allowed++ - log.Debugf("%v allowed", r) + r.log.Debugf("%v allowed", r) return true } r.denied++ - log.Debugf("%v denied", r) + r.log.Debugf("%v denied", r) return false } diff --git a/vendor/github.com/vulcand/oxy/connlimit/connlimit.go b/vendor/github.com/vulcand/oxy/connlimit/connlimit.go index c7b392758..5d2d71468 100644 --- a/vendor/github.com/vulcand/oxy/connlimit/connlimit.go +++ b/vendor/github.com/vulcand/oxy/connlimit/connlimit.go @@ -1,4 +1,4 @@ -// package connlimit provides control over simultaneous connections coming from the same source +// Package connlimit provides control over simultaneous connections coming from the same source package connlimit import ( @@ -10,7 +10,7 @@ import ( "github.com/vulcand/oxy/utils" ) -// Limiter tracks concurrent connection per token +// ConnLimiter tracks concurrent connection per token // and is capable of rejecting connections if they are failed type ConnLimiter struct { mutex *sync.Mutex @@ -21,8 +21,10 @@ type ConnLimiter struct { next http.Handler errHandler utils.ErrorHandler + log *log.Logger } +// New creates a new ConnLimiter func New(next http.Handler, extract utils.SourceExtractor, maxConnections int64, options ...ConnLimitOption) (*ConnLimiter, error) { if extract == nil { return nil, fmt.Errorf("Extract function can not be nil") @@ -33,6 +35,7 @@ func New(next http.Handler, extract utils.SourceExtractor, maxConnections int64, maxConnections: maxConnections, connections: make(map[string]int64), next: next, + log: log.StandardLogger(), } for _, o := range options { @@ -41,11 +44,24 @@ func New(next http.Handler, extract utils.SourceExtractor, maxConnections int64, } } if cl.errHandler == nil { - cl.errHandler = defaultErrHandler + cl.errHandler = &ConnErrHandler{ + log: cl.log, + } } return cl, nil } +// Logger defines the logger the connection limiter will use. +// +// It defaults to logrus.StandardLogger(), the global logger used by logrus. +func Logger(l *log.Logger) ConnLimitOption { + return func(cl *ConnLimiter) error { + cl.log = l + return nil + } +} + +// Wrap sets the next handler to be called by connexion limiter handler. func (cl *ConnLimiter) Wrap(h http.Handler) { cl.next = h } @@ -53,12 +69,12 @@ func (cl *ConnLimiter) Wrap(h http.Handler) { func (cl *ConnLimiter) ServeHTTP(w http.ResponseWriter, r *http.Request) { token, amount, err := cl.extract.Extract(r) if err != nil { - log.Errorf("failed to extract source of the connection: %v", err) + cl.log.Errorf("failed to extract source of the connection: %v", err) cl.errHandler.ServeHTTP(w, r, err) return } if err := cl.acquire(token, amount); err != nil { - log.Debugf("limiting request source %s: %v", token, err) + cl.log.Debugf("limiting request source %s: %v", token, err) cl.errHandler.ServeHTTP(w, r, err) return } @@ -95,6 +111,7 @@ func (cl *ConnLimiter) release(token string, amount int64) { } } +// MaxConnError maximum connections reached error type MaxConnError struct { max int64 } @@ -103,12 +120,14 @@ func (m *MaxConnError) Error() string { return fmt.Sprintf("max connections reached: %d", m.max) } +// ConnErrHandler connection limiter error handler type ConnErrHandler struct { + log *log.Logger } func (e *ConnErrHandler) ServeHTTP(w http.ResponseWriter, req *http.Request, err error) { - if log.GetLevel() >= log.DebugLevel { - logEntry := log.WithField("Request", utils.DumpHttpRequest(req)) + if e.log.Level >= log.DebugLevel { + logEntry := e.log.WithField("Request", utils.DumpHttpRequest(req)) logEntry.Debug("vulcand/oxy/connlimit: begin ServeHttp on request") defer logEntry.Debug("vulcand/oxy/connlimit: completed ServeHttp on request") } @@ -121,6 +140,7 @@ func (e *ConnErrHandler) ServeHTTP(w http.ResponseWriter, req *http.Request, err utils.DefaultHandler.ServeHTTP(w, req, err) } +// ConnLimitOption connection limit option type type ConnLimitOption func(l *ConnLimiter) error // ErrorHandler sets error handler of the server @@ -130,5 +150,3 @@ func ErrorHandler(h utils.ErrorHandler) ConnLimitOption { return nil } } - -var defaultErrHandler = &ConnErrHandler{} diff --git a/vendor/github.com/vulcand/oxy/forward/fwd.go b/vendor/github.com/vulcand/oxy/forward/fwd.go index abeb3c08e..3a715e479 100644 --- a/vendor/github.com/vulcand/oxy/forward/fwd.go +++ b/vendor/github.com/vulcand/oxy/forward/fwd.go @@ -1,12 +1,15 @@ -// package forwarder implements http handler that forwards requests to remote server +// Package forward implements http handler that forwards requests to remote server // and serves back the response // websocket proxying support based on https://github.com/yhat/wsutil package forward import ( + "bytes" "crypto/tls" "errors" "fmt" + "io" + "net" "net/http" "net/http/httptest" "net/http/httputil" @@ -21,7 +24,7 @@ import ( "github.com/vulcand/oxy/utils" ) -// Oxy Logger interface of the internal +// OxyLogger interface of the internal type OxyLogger interface { log.FieldLogger GetLevel() log.Level @@ -42,8 +45,7 @@ type ReqRewriter interface { type optSetter func(f *Forwarder) error -// PassHostHeader specifies if a client's Host header field should -// be delegated +// PassHostHeader specifies if a client's Host header field should be delegated func PassHostHeader(b bool) optSetter { return func(f *Forwarder) error { f.httpForwarder.passHost = b @@ -68,8 +70,7 @@ func Rewriter(r ReqRewriter) optSetter { } } -// PassHostHeader specifies if a client's Host header field should -// be delegated +// WebsocketTLSClientConfig define the websocker client TLS configuration func WebsocketTLSClientConfig(tcc *tls.Config) optSetter { return func(f *Forwarder) error { f.httpForwarder.tlsClientConfig = tcc @@ -120,6 +121,7 @@ func Logger(l log.FieldLogger) optSetter { } } +// StateListener defines a state listener for the HTTP forwarder func StateListener(stateListener UrlForwardingStateListener) optSetter { return func(f *Forwarder) error { f.stateListener = stateListener @@ -127,6 +129,15 @@ func StateListener(stateListener UrlForwardingStateListener) optSetter { } } +// WebsocketConnectionClosedHook defines a hook called when websocket connection is closed +func WebsocketConnectionClosedHook(hook func(req *http.Request, conn net.Conn)) optSetter { + return func(f *Forwarder) error { + f.httpForwarder.websocketConnectionClosedHook = hook + return nil + } +} + +// ResponseModifier defines a response modifier for the HTTP forwarder func ResponseModifier(responseModifier func(*http.Response) error) optSetter { return func(f *Forwarder) error { f.httpForwarder.modifyResponse = responseModifier @@ -134,6 +145,7 @@ func ResponseModifier(responseModifier func(*http.Response) error) optSetter { } } +// StreamingFlushInterval defines a streaming flush interval for the HTTP forwarder func StreamingFlushInterval(flushInterval time.Duration) optSetter { return func(f *Forwarder) error { f.httpForwarder.flushInterval = flushInterval @@ -141,11 +153,13 @@ func StreamingFlushInterval(flushInterval time.Duration) optSetter { } } +// ErrorHandlingRoundTripper a error handling round tripper type ErrorHandlingRoundTripper struct { http.RoundTripper errorHandler utils.ErrorHandler } +// RoundTrip executes the round trip func (rt ErrorHandlingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { res, err := rt.RoundTripper.RoundTrip(req) if err != nil { @@ -185,15 +199,19 @@ type httpForwarder struct { log OxyLogger - bufferPool httputil.BufferPool + bufferPool httputil.BufferPool + websocketConnectionClosedHook func(req *http.Request, conn net.Conn) } +const defaultFlushInterval = time.Duration(100) * time.Millisecond + +// Connection states const ( - defaultFlushInterval = time.Duration(100) * time.Millisecond - StateConnected = iota + StateConnected = iota StateDisconnected ) +// UrlForwardingStateListener URL forwarding state listener type UrlForwardingStateListener func(*url.URL, int) // New creates an instance of Forwarder based on the provided list of configuration options @@ -293,11 +311,6 @@ func (f *httpForwarder) modifyRequest(outReq *http.Request, target *url.URL) { outReq.URL.RawQuery = u.RawQuery outReq.RequestURI = "" // Outgoing request should not have RequestURI - // Do not pass client Host header unless optsetter PassHostHeader is set. - if !f.passHost { - outReq.Host = target.Host - } - outReq.Proto = "HTTP/1.1" outReq.ProtoMajor = 1 outReq.ProtoMinor = 1 @@ -305,6 +318,11 @@ func (f *httpForwarder) modifyRequest(outReq *http.Request, target *url.URL) { if f.rewriter != nil { f.rewriter.Rewrite(outReq) } + + // Do not pass client Host header unless optsetter PassHostHeader is set. + if !f.passHost { + outReq.Host = target.Host + } } // serveHTTP forwards websocket traffic @@ -368,14 +386,40 @@ func (f *httpForwarder) serveWebSocket(w http.ResponseWriter, req *http.Request, log.Errorf("vulcand/oxy/forward/websocket: Error while upgrading connection : %v", err) return } - defer underlyingConn.Close() - defer targetConn.Close() + defer func() { + underlyingConn.Close() + targetConn.Close() + if f.websocketConnectionClosedHook != nil { + f.websocketConnectionClosedHook(req, underlyingConn.UnderlyingConn()) + } + }() errClient := make(chan error, 1) errBackend := make(chan error, 1) replicateWebsocketConn := func(dst, src *websocket.Conn, errc chan error) { + + forward := func(messageType int, reader io.Reader) error { + writer, err := dst.NextWriter(messageType) + if err != nil { + return err + } + _, err = io.Copy(writer, reader) + if err != nil { + return err + } + return writer.Close() + } + + src.SetPingHandler(func(data string) error { + return forward(websocket.PingMessage, bytes.NewReader([]byte(data))) + }) + + src.SetPongHandler(func(data string) error { + return forward(websocket.PongMessage, bytes.NewReader([]byte(data))) + }) + for { - msgType, msg, err := src.ReadMessage() + msgType, reader, err := src.NextReader() if err != nil { m := websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%v", err)) @@ -393,11 +437,11 @@ func (f *httpForwarder) serveWebSocket(w http.ResponseWriter, req *http.Request, } errc <- err if m != nil { - dst.WriteMessage(websocket.CloseMessage, m) + forward(websocket.CloseMessage, bytes.NewReader([]byte(m))) } break } - err = dst.WriteMessage(msgType, msg) + err = forward(msgType, reader) if err != nil { errc <- err break @@ -501,7 +545,7 @@ func (f *httpForwarder) serveHTTP(w http.ResponseWriter, inReq *http.Request, ct } } -// isWebsocketRequest determines if the specified HTTP request is a +// IsWebsocketRequest determines if the specified HTTP request is a // websocket handshake request func IsWebsocketRequest(req *http.Request) bool { containsHeader := func(name, value string) bool { diff --git a/vendor/github.com/vulcand/oxy/forward/headers.go b/vendor/github.com/vulcand/oxy/forward/headers.go index f884a5ee0..512e28435 100644 --- a/vendor/github.com/vulcand/oxy/forward/headers.go +++ b/vendor/github.com/vulcand/oxy/forward/headers.go @@ -1,5 +1,6 @@ package forward +// Headers const ( XForwardedProto = "X-Forwarded-Proto" XForwardedFor = "X-Forwarded-For" @@ -22,7 +23,7 @@ const ( SecWebsocketAccept = "Sec-Websocket-Accept" ) -// Hop-by-hop headers. These are removed when sent to the backend. +// HopHeaders Hop-by-hop headers. These are removed when sent to the backend. // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html // Copied from reverseproxy.go, too bad var HopHeaders = []string{ @@ -36,6 +37,7 @@ var HopHeaders = []string{ Upgrade, } +// WebsocketDialHeaders Websocket dial headers var WebsocketDialHeaders = []string{ Upgrade, Connection, @@ -45,6 +47,7 @@ var WebsocketDialHeaders = []string{ SecWebsocketAccept, } +// WebsocketUpgradeHeaders Websocket upgrade headers var WebsocketUpgradeHeaders = []string{ Upgrade, Connection, @@ -52,6 +55,7 @@ var WebsocketUpgradeHeaders = []string{ SecWebsocketExtensions, } +// XHeaders X-* headers var XHeaders = []string{ XForwardedProto, XForwardedFor, diff --git a/vendor/github.com/vulcand/oxy/forward/rewrite.go b/vendor/github.com/vulcand/oxy/forward/rewrite.go index 38a7f7fc4..60c1a1947 100644 --- a/vendor/github.com/vulcand/oxy/forward/rewrite.go +++ b/vendor/github.com/vulcand/oxy/forward/rewrite.go @@ -8,7 +8,7 @@ import ( "github.com/vulcand/oxy/utils" ) -// Rewriter is responsible for removing hop-by-hop headers and setting forwarding headers +// HeaderRewriter is responsible for removing hop-by-hop headers and setting forwarding headers type HeaderRewriter struct { TrustForwardHeader bool Hostname string @@ -19,6 +19,7 @@ func ipv6fix(clientIP string) string { return strings.Split(clientIP, "%")[0] } +// Rewrite rewrite request headers func (rw *HeaderRewriter) Rewrite(req *http.Request) { if !rw.TrustForwardHeader { utils.RemoveHeaders(req.Header, XHeaders...) @@ -85,6 +86,10 @@ func forwardedPort(req *http.Request) string { return port } + if req.Header.Get(XForwardedProto) == "https" || req.Header.Get(XForwardedProto) == "wss" { + return "443" + } + if req.TLS != nil { return "443" } diff --git a/vendor/github.com/vulcand/oxy/memmetrics/anomaly.go b/vendor/github.com/vulcand/oxy/memmetrics/anomaly.go index 5aeb13ae3..1f8dfe95d 100644 --- a/vendor/github.com/vulcand/oxy/memmetrics/anomaly.go +++ b/vendor/github.com/vulcand/oxy/memmetrics/anomaly.go @@ -6,7 +6,7 @@ import ( "time" ) -// SplitRatios provides simple anomaly detection for requests latencies. +// SplitLatencies provides simple anomaly detection for requests latencies. // it splits values into good or bad category based on the threshold and the median value. // If all values are not far from the median, it will return all values in 'good' set. // Precision is the smallest value to consider, e.g. if set to millisecond, microseconds will be ignored. @@ -23,10 +23,10 @@ func SplitLatencies(values []time.Duration, precision time.Duration) (good map[t good, bad = make(map[time.Duration]bool), make(map[time.Duration]bool) // Note that multiplier makes this function way less sensitive than ratios detector, this is to avoid noise. vgood, vbad := SplitFloat64(2, 0, ratios) - for r, _ := range vgood { + for r := range vgood { good[v2r[r]] = true } - for r, _ := range vbad { + for r := range vbad { bad[v2r[r]] = true } return good, bad diff --git a/vendor/github.com/vulcand/oxy/memmetrics/counter.go b/vendor/github.com/vulcand/oxy/memmetrics/counter.go index 361d8a878..4faf905dd 100644 --- a/vendor/github.com/vulcand/oxy/memmetrics/counter.go +++ b/vendor/github.com/vulcand/oxy/memmetrics/counter.go @@ -9,6 +9,7 @@ import ( type rcOptSetter func(*RollingCounter) error +// CounterClock defines a counter clock func CounterClock(c timetools.TimeProvider) rcOptSetter { return func(r *RollingCounter) error { r.clock = c @@ -16,7 +17,7 @@ func CounterClock(c timetools.TimeProvider) rcOptSetter { } } -// Calculates in memory failure rate of an endpoint using rolling window of a predefined size +// RollingCounter Calculates in memory failure rate of an endpoint using rolling window of a predefined size type RollingCounter struct { clock timetools.TimeProvider resolution time.Duration @@ -57,11 +58,13 @@ func NewCounter(buckets int, resolution time.Duration, options ...rcOptSetter) ( return rc, nil } +// Append append a counter func (c *RollingCounter) Append(o *RollingCounter) error { c.Inc(int(o.Count())) return nil } +// Clone clone a counter func (c *RollingCounter) Clone() *RollingCounter { c.cleanup() other := &RollingCounter{ @@ -75,6 +78,7 @@ func (c *RollingCounter) Clone() *RollingCounter { return other } +// Reset reset a counter func (c *RollingCounter) Reset() { c.lastBucket = -1 c.countedBuckets = 0 @@ -84,27 +88,33 @@ func (c *RollingCounter) Reset() { } } +// CountedBuckets gets counted buckets func (c *RollingCounter) CountedBuckets() int { return c.countedBuckets } +// Count counts func (c *RollingCounter) Count() int64 { c.cleanup() return c.sum() } +// Resolution gets resolution func (c *RollingCounter) Resolution() time.Duration { return c.resolution } +// Buckets gets buckets func (c *RollingCounter) Buckets() int { return len(c.values) } +// WindowSize gets windows size func (c *RollingCounter) WindowSize() time.Duration { return time.Duration(len(c.values)) * c.resolution } +// Inc increment counter func (c *RollingCounter) Inc(v int) { c.cleanup() c.incBucketValue(v) diff --git a/vendor/github.com/vulcand/oxy/memmetrics/histogram.go b/vendor/github.com/vulcand/oxy/memmetrics/histogram.go index 02c1d561e..2c3aa76af 100644 --- a/vendor/github.com/vulcand/oxy/memmetrics/histogram.go +++ b/vendor/github.com/vulcand/oxy/memmetrics/histogram.go @@ -20,6 +20,7 @@ type HDRHistogram struct { h *hdrhistogram.Histogram } +// NewHDRHistogram creates a new HDRHistogram func NewHDRHistogram(low, high int64, sigfigs int) (h *HDRHistogram, err error) { defer func() { if msg := recover(); msg != nil { @@ -34,37 +35,42 @@ func NewHDRHistogram(low, high int64, sigfigs int) (h *HDRHistogram, err error) }, nil } -func (r *HDRHistogram) Export() *HDRHistogram { - var hist *hdrhistogram.Histogram = nil - if r.h != nil { - snapshot := r.h.Export() +// Export export a HDRHistogram +func (h *HDRHistogram) Export() *HDRHistogram { + var hist *hdrhistogram.Histogram + if h.h != nil { + snapshot := h.h.Export() hist = hdrhistogram.Import(snapshot) } - return &HDRHistogram{low: r.low, high: r.high, sigfigs: r.sigfigs, h: hist} + return &HDRHistogram{low: h.low, high: h.high, sigfigs: h.sigfigs, h: hist} } -// Returns latency at quantile with microsecond precision +// LatencyAtQuantile sets latency at quantile with microsecond precision func (h *HDRHistogram) LatencyAtQuantile(q float64) time.Duration { return time.Duration(h.ValueAtQuantile(q)) * time.Microsecond } -// Records latencies with microsecond precision +// RecordLatencies Records latencies with microsecond precision func (h *HDRHistogram) RecordLatencies(d time.Duration, n int64) error { return h.RecordValues(int64(d/time.Microsecond), n) } +// Reset reset a HDRHistogram func (h *HDRHistogram) Reset() { h.h.Reset() } +// ValueAtQuantile sets value at quantile func (h *HDRHistogram) ValueAtQuantile(q float64) int64 { return h.h.ValueAtQuantile(q) } +// RecordValues sets record values func (h *HDRHistogram) RecordValues(v, n int64) error { return h.h.RecordValues(v, n) } +// Merge merge a HDRHistogram func (h *HDRHistogram) Merge(other *HDRHistogram) error { if other == nil { return fmt.Errorf("other is nil") @@ -75,6 +81,7 @@ func (h *HDRHistogram) Merge(other *HDRHistogram) error { type rhOptSetter func(r *RollingHDRHistogram) error +// RollingClock sets a clock func RollingClock(clock timetools.TimeProvider) rhOptSetter { return func(r *RollingHDRHistogram) error { r.clock = clock @@ -82,7 +89,7 @@ func RollingClock(clock timetools.TimeProvider) rhOptSetter { } } -// RollingHistogram holds multiple histograms and rotates every period. +// RollingHDRHistogram holds multiple histograms and rotates every period. // It provides resulting histogram as a result of a call of 'Merged' function. type RollingHDRHistogram struct { idx int @@ -96,6 +103,7 @@ type RollingHDRHistogram struct { clock timetools.TimeProvider } +// NewRollingHDRHistogram created a new RollingHDRHistogram func NewRollingHDRHistogram(low, high int64, sigfigs int, period time.Duration, bucketCount int, options ...rhOptSetter) (*RollingHDRHistogram, error) { rh := &RollingHDRHistogram{ bucketCount: bucketCount, @@ -127,6 +135,7 @@ func NewRollingHDRHistogram(low, high int64, sigfigs int, period time.Duration, return rh, nil } +// Export export a RollingHDRHistogram func (r *RollingHDRHistogram) Export() *RollingHDRHistogram { export := &RollingHDRHistogram{} export.idx = r.idx @@ -147,6 +156,7 @@ func (r *RollingHDRHistogram) Export() *RollingHDRHistogram { return export } +// Append append a RollingHDRHistogram func (r *RollingHDRHistogram) Append(o *RollingHDRHistogram) error { if r.bucketCount != o.bucketCount || r.period != o.period || r.low != o.low || r.high != o.high || r.sigfigs != o.sigfigs { return fmt.Errorf("can't merge") @@ -160,6 +170,7 @@ func (r *RollingHDRHistogram) Append(o *RollingHDRHistogram) error { return nil } +// Reset reset a RollingHDRHistogram func (r *RollingHDRHistogram) Reset() { r.idx = 0 r.lastRoll = r.clock.UtcNow() @@ -173,6 +184,7 @@ func (r *RollingHDRHistogram) rotate() { r.buckets[r.idx].Reset() } +// Merged gets merged histogram func (r *RollingHDRHistogram) Merged() (*HDRHistogram, error) { m, err := NewHDRHistogram(r.low, r.high, r.sigfigs) if err != nil { @@ -194,10 +206,12 @@ func (r *RollingHDRHistogram) getHist() *HDRHistogram { return r.buckets[r.idx] } +// RecordLatencies sets records latencies func (r *RollingHDRHistogram) RecordLatencies(v time.Duration, n int64) error { return r.getHist().RecordLatencies(v, n) } +// RecordValues set record values func (r *RollingHDRHistogram) RecordValues(v, n int64) error { return r.getHist().RecordValues(v, n) } diff --git a/vendor/github.com/vulcand/oxy/memmetrics/ratio.go b/vendor/github.com/vulcand/oxy/memmetrics/ratio.go index f21f375ea..ecfd50371 100644 --- a/vendor/github.com/vulcand/oxy/memmetrics/ratio.go +++ b/vendor/github.com/vulcand/oxy/memmetrics/ratio.go @@ -8,6 +8,7 @@ import ( type ratioOptSetter func(r *RatioCounter) error +// RatioClock sets a clock func RatioClock(clock timetools.TimeProvider) ratioOptSetter { return func(r *RatioCounter) error { r.clock = clock @@ -22,6 +23,7 @@ type RatioCounter struct { b *RollingCounter } +// NewRatioCounter creates a new RatioCounter func NewRatioCounter(buckets int, resolution time.Duration, options ...ratioOptSetter) (*RatioCounter, error) { rc := &RatioCounter{} @@ -50,39 +52,48 @@ func NewRatioCounter(buckets int, resolution time.Duration, options ...ratioOptS return rc, nil } +// Reset reset the counter func (r *RatioCounter) Reset() { r.a.Reset() r.b.Reset() } +// IsReady returns true if the counter is ready func (r *RatioCounter) IsReady() bool { return r.a.countedBuckets+r.b.countedBuckets >= len(r.a.values) } +// CountA gets count A func (r *RatioCounter) CountA() int64 { return r.a.Count() } +// CountB gets count B func (r *RatioCounter) CountB() int64 { return r.b.Count() } +// Resolution gets resolution func (r *RatioCounter) Resolution() time.Duration { return r.a.Resolution() } +// Buckets gets buckets func (r *RatioCounter) Buckets() int { return r.a.Buckets() } +// WindowSize gets windows size func (r *RatioCounter) WindowSize() time.Duration { return r.a.WindowSize() } +// ProcessedCount gets processed count func (r *RatioCounter) ProcessedCount() int64 { return r.CountA() + r.CountB() } +// Ratio gets ratio func (r *RatioCounter) Ratio() float64 { a := r.a.Count() b := r.b.Count() @@ -93,28 +104,34 @@ func (r *RatioCounter) Ratio() float64 { return float64(a) / float64(a+b) } +// IncA increment counter A func (r *RatioCounter) IncA(v int) { r.a.Inc(v) } +// IncB increment counter B func (r *RatioCounter) IncB(v int) { r.b.Inc(v) } +// TestMeter a test meter type TestMeter struct { Rate float64 NotReady bool WindowSize time.Duration } +// GetWindowSize gets windows size func (tm *TestMeter) GetWindowSize() time.Duration { return tm.WindowSize } +// IsReady returns true if the meter is ready func (tm *TestMeter) IsReady() bool { return !tm.NotReady } +// GetRate gets rate func (tm *TestMeter) GetRate() float64 { return tm.Rate } diff --git a/vendor/github.com/vulcand/oxy/memmetrics/roundtrip.go b/vendor/github.com/vulcand/oxy/memmetrics/roundtrip.go index 4bdb4bba2..34b396915 100644 --- a/vendor/github.com/vulcand/oxy/memmetrics/roundtrip.go +++ b/vendor/github.com/vulcand/oxy/memmetrics/roundtrip.go @@ -29,10 +29,16 @@ type RTMetrics struct { type rrOptSetter func(r *RTMetrics) error +// NewRTMetricsFn builder function type type NewRTMetricsFn func() (*RTMetrics, error) + +// NewCounterFn builder function type type NewCounterFn func() (*RollingCounter, error) + +// NewRollingHistogramFn builder function type type NewRollingHistogramFn func() (*RollingHDRHistogram, error) +// RTCounter set a builder function for Counter func RTCounter(new NewCounterFn) rrOptSetter { return func(r *RTMetrics) error { r.newCounter = new @@ -40,13 +46,15 @@ func RTCounter(new NewCounterFn) rrOptSetter { } } -func RTHistogram(new NewRollingHistogramFn) rrOptSetter { +// RTHistogram set a builder function for RollingHistogram +func RTHistogram(fn NewRollingHistogramFn) rrOptSetter { return func(r *RTMetrics) error { - r.newHist = new + r.newHist = fn return nil } } +// RTClock sets a clock func RTClock(clock timetools.TimeProvider) rrOptSetter { return func(r *RTMetrics) error { r.clock = clock @@ -103,7 +111,7 @@ func NewRTMetrics(settings ...rrOptSetter) (*RTMetrics, error) { return m, nil } -// Returns a new RTMetrics which is a copy of the current one +// Export Returns a new RTMetrics which is a copy of the current one func (m *RTMetrics) Export() *RTMetrics { m.statusCodesLock.RLock() defer m.statusCodesLock.RUnlock() @@ -130,11 +138,12 @@ func (m *RTMetrics) Export() *RTMetrics { return export } +// CounterWindowSize gets total windows size func (m *RTMetrics) CounterWindowSize() time.Duration { return m.total.WindowSize() } -// GetNetworkErrorRatio calculates the amont of network errors such as time outs and dropped connection +// NetworkErrorRatio calculates the amont of network errors such as time outs and dropped connection // that occurred in the given time window compared to the total requests count. func (m *RTMetrics) NetworkErrorRatio() float64 { if m.total.Count() == 0 { @@ -143,7 +152,7 @@ func (m *RTMetrics) NetworkErrorRatio() float64 { return float64(m.netErrors.Count()) / float64(m.total.Count()) } -// GetResponseCodeRatio calculates ratio of count(startA to endA) / count(startB to endB) +// ResponseCodeRatio calculates ratio of count(startA to endA) / count(startB to endB) func (m *RTMetrics) ResponseCodeRatio(startA, endA, startB, endB int) float64 { a := int64(0) b := int64(0) @@ -163,6 +172,7 @@ func (m *RTMetrics) ResponseCodeRatio(startA, endA, startB, endB int) float64 { return 0 } +// Append append a metric func (m *RTMetrics) Append(other *RTMetrics) error { if m == other { return errors.New("RTMetrics cannot append to self") @@ -196,6 +206,7 @@ func (m *RTMetrics) Append(other *RTMetrics) error { return m.histogram.Append(copied.histogram) } +// Record records a metric func (m *RTMetrics) Record(code int, duration time.Duration) { m.total.Inc(1) if code == http.StatusGatewayTimeout || code == http.StatusBadGateway { @@ -205,17 +216,17 @@ func (m *RTMetrics) Record(code int, duration time.Duration) { m.recordLatency(duration) } -// GetTotalCount returns total count of processed requests collected. +// TotalCount returns total count of processed requests collected. func (m *RTMetrics) TotalCount() int64 { return m.total.Count() } -// GetNetworkErrorCount returns total count of processed requests observed +// NetworkErrorCount returns total count of processed requests observed func (m *RTMetrics) NetworkErrorCount() int64 { return m.netErrors.Count() } -// GetStatusCodesCounts returns map with counts of the response codes +// StatusCodesCounts returns map with counts of the response codes func (m *RTMetrics) StatusCodesCounts() map[int]int64 { sc := make(map[int]int64) m.statusCodesLock.RLock() @@ -228,13 +239,14 @@ func (m *RTMetrics) StatusCodesCounts() map[int]int64 { return sc } -// GetLatencyHistogram computes and returns resulting histogram with latencies observed. +// LatencyHistogram computes and returns resulting histogram with latencies observed. func (m *RTMetrics) LatencyHistogram() (*HDRHistogram, error) { m.histogramLock.Lock() defer m.histogramLock.Unlock() return m.histogram.Merged() } +// Reset reset metrics func (m *RTMetrics) Reset() { m.statusCodesLock.Lock() defer m.statusCodesLock.Unlock() @@ -284,7 +296,7 @@ const ( counterResolution = time.Second histMin = 1 histMax = 3600000000 // 1 hour in microseconds - histSignificantFigures = 2 // signigicant figures (1% precision) + histSignificantFigures = 2 // significant figures (1% precision) histBuckets = 6 // number of sub-histograms in a rolling histogram histPeriod = 10 * time.Second // roll time ) diff --git a/vendor/github.com/vulcand/oxy/ratelimit/bucket.go b/vendor/github.com/vulcand/oxy/ratelimit/bucket.go index 78507faf9..9134d1828 100644 --- a/vendor/github.com/vulcand/oxy/ratelimit/bucket.go +++ b/vendor/github.com/vulcand/oxy/ratelimit/bucket.go @@ -7,6 +7,7 @@ import ( "github.com/mailgun/timetools" ) +// UndefinedDelay default delay const UndefinedDelay = -1 // rate defines token bucket parameters. @@ -20,7 +21,7 @@ func (r *rate) String() string { return fmt.Sprintf("rate(%v/%v, burst=%v)", r.average, r.period, r.burst) } -// Implements token bucket algorithm (http://en.wikipedia.org/wiki/Token_bucket) +// tokenBucket Implements token bucket algorithm (http://en.wikipedia.org/wiki/Token_bucket) type tokenBucket struct { // The time period controlled by the bucket in nanoseconds. period time.Duration @@ -63,7 +64,7 @@ func (tb *tokenBucket) consume(tokens int64) (time.Duration, error) { tb.updateAvailableTokens() tb.lastConsumed = 0 if tokens > tb.burst { - return UndefinedDelay, fmt.Errorf("Requested tokens larger than max tokens") + return UndefinedDelay, fmt.Errorf("requested tokens larger than max tokens") } if tb.availableTokens < tokens { return tb.timeTillAvailable(tokens), nil @@ -83,11 +84,11 @@ func (tb *tokenBucket) rollback() { tb.lastConsumed = 0 } -// Update modifies `average` and `burst` fields of the token bucket according +// update modifies `average` and `burst` fields of the token bucket according // to the provided `Rate` func (tb *tokenBucket) update(rate *rate) error { if rate.period != tb.period { - return fmt.Errorf("Period mismatch: %v != %v", tb.period, rate.period) + return fmt.Errorf("period mismatch: %v != %v", tb.period, rate.period) } tb.timePerToken = time.Duration(int64(tb.period) / rate.average) tb.burst = rate.burst diff --git a/vendor/github.com/vulcand/oxy/ratelimit/bucketset.go b/vendor/github.com/vulcand/oxy/ratelimit/bucketset.go index f4a246568..af2c8bb1c 100644 --- a/vendor/github.com/vulcand/oxy/ratelimit/bucketset.go +++ b/vendor/github.com/vulcand/oxy/ratelimit/bucketset.go @@ -2,11 +2,11 @@ package ratelimit import ( "fmt" + "sort" "strings" "time" "github.com/mailgun/timetools" - "sort" ) // TokenBucketSet represents a set of TokenBucket covering different time periods. @@ -16,7 +16,7 @@ type TokenBucketSet struct { clock timetools.TimeProvider } -// newTokenBucketSet creates a `TokenBucketSet` from the specified `rates`. +// NewTokenBucketSet creates a `TokenBucketSet` from the specified `rates`. func NewTokenBucketSet(rates *RateSet, clock timetools.TimeProvider) *TokenBucketSet { tbs := new(TokenBucketSet) tbs.clock = clock @@ -54,9 +54,10 @@ func (tbs *TokenBucketSet) Update(rates *RateSet) { } } +// Consume consume tokens func (tbs *TokenBucketSet) Consume(tokens int64) (time.Duration, error) { var maxDelay time.Duration = UndefinedDelay - var firstErr error = nil + var firstErr error for _, tokenBucket := range tbs.buckets { // We keep calling `Consume` even after a error is returned for one of // buckets because that allows us to simplify the rollback procedure, @@ -80,6 +81,7 @@ func (tbs *TokenBucketSet) Consume(tokens int64) (time.Duration, error) { return maxDelay, firstErr } +// GetMaxPeriod returns the max period func (tbs *TokenBucketSet) GetMaxPeriod() time.Duration { return tbs.maxPeriod } diff --git a/vendor/github.com/vulcand/oxy/ratelimit/tokenlimiter.go b/vendor/github.com/vulcand/oxy/ratelimit/tokenlimiter.go index be16c782b..bfd4c3b2e 100644 --- a/vendor/github.com/vulcand/oxy/ratelimit/tokenlimiter.go +++ b/vendor/github.com/vulcand/oxy/ratelimit/tokenlimiter.go @@ -1,4 +1,4 @@ -// Tokenbucket based request rate limiter +// Package ratelimit Tokenbucket based request rate limiter package ratelimit import ( @@ -13,6 +13,7 @@ import ( "github.com/vulcand/oxy/utils" ) +// DefaultCapacity default capacity const DefaultCapacity = 65536 // RateSet maintains a set of rates. It can contain only one rate per period at a time. @@ -31,15 +32,15 @@ func NewRateSet() *RateSet { // set then the new rate overrides the old one. func (rs *RateSet) Add(period time.Duration, average int64, burst int64) error { if period <= 0 { - return fmt.Errorf("Invalid period: %v", period) + return fmt.Errorf("invalid period: %v", period) } if average <= 0 { - return fmt.Errorf("Invalid average: %v", average) + return fmt.Errorf("invalid average: %v", average) } if burst <= 0 { - return fmt.Errorf("Invalid burst: %v", burst) + return fmt.Errorf("invalid burst: %v", burst) } - rs.m[period] = &rate{period, average, burst} + rs.m[period] = &rate{period: period, average: average, burst: burst} return nil } @@ -47,12 +48,15 @@ func (rs *RateSet) String() string { return fmt.Sprint(rs.m) } +// RateExtractor rate extractor type RateExtractor interface { Extract(r *http.Request) (*RateSet, error) } +// RateExtractorFunc rate extractor function type type RateExtractorFunc func(r *http.Request) (*RateSet, error) +// Extract extract from request func (e RateExtractorFunc) Extract(r *http.Request) (*RateSet, error) { return e(r) } @@ -68,20 +72,24 @@ type TokenLimiter struct { errHandler utils.ErrorHandler capacity int next http.Handler + + log *log.Logger } // New constructs a `TokenLimiter` middleware instance. func New(next http.Handler, extract utils.SourceExtractor, defaultRates *RateSet, opts ...TokenLimiterOption) (*TokenLimiter, error) { if defaultRates == nil || len(defaultRates.m) == 0 { - return nil, fmt.Errorf("Provide default rates") + return nil, fmt.Errorf("provide default rates") } if extract == nil { - return nil, fmt.Errorf("Provide extract function") + return nil, fmt.Errorf("provide extract function") } tl := &TokenLimiter{ next: next, defaultRates: defaultRates, extract: extract, + + log: log.StandardLogger(), } for _, o := range opts { @@ -98,6 +106,17 @@ func New(next http.Handler, extract utils.SourceExtractor, defaultRates *RateSet return tl, nil } +// Logger defines the logger the token limiter will use. +// +// It defaults to logrus.StandardLogger(), the global logger used by logrus. +func Logger(l *log.Logger) TokenLimiterOption { + return func(tl *TokenLimiter) error { + tl.log = l + return nil + } +} + +// Wrap sets the next handler to be called by token limiter handler. func (tl *TokenLimiter) Wrap(next http.Handler) { tl.next = next } @@ -110,7 +129,7 @@ func (tl *TokenLimiter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } if err := tl.consumeRates(req, source, amount); err != nil { - log.Warnf("limiting request %v %v, limit: %v", req.Method, req.URL, err) + tl.log.Warnf("limiting request %v %v, limit: %v", req.Method, req.URL, err) tl.errHandler.ServeHTTP(w, req, err) return } @@ -155,7 +174,7 @@ func (tl *TokenLimiter) resolveRates(req *http.Request) *RateSet { rates, err := tl.extractRates.Extract(req) if err != nil { - log.Errorf("Failed to retrieve rates: %v", err) + tl.log.Errorf("Failed to retrieve rates: %v", err) return tl.defaultRates } @@ -167,6 +186,7 @@ func (tl *TokenLimiter) resolveRates(req *http.Request) *RateSet { return rates } +// MaxRateError max rate error type MaxRateError struct { delay time.Duration } @@ -175,19 +195,21 @@ func (m *MaxRateError) Error() string { return fmt.Sprintf("max rate reached: retry-in %v", m.delay) } -type RateErrHandler struct { -} +// RateErrHandler error handler +type RateErrHandler struct{} func (e *RateErrHandler) ServeHTTP(w http.ResponseWriter, req *http.Request, err error) { if rerr, ok := err.(*MaxRateError); ok { + w.Header().Set("Retry-After", fmt.Sprintf("%.0f", rerr.delay.Seconds())) w.Header().Set("X-Retry-In", rerr.delay.String()) - w.WriteHeader(429) + w.WriteHeader(http.StatusTooManyRequests) w.Write([]byte(err.Error())) return } utils.DefaultHandler.ServeHTTP(w, req, err) } +// TokenLimiterOption token limiter option type type TokenLimiterOption func(l *TokenLimiter) error // ErrorHandler sets error handler of the server @@ -198,6 +220,7 @@ func ErrorHandler(h utils.ErrorHandler) TokenLimiterOption { } } +// ExtractRates sets the rate extractor func ExtractRates(e RateExtractor) TokenLimiterOption { return func(cl *TokenLimiter) error { cl.extractRates = e @@ -205,6 +228,7 @@ func ExtractRates(e RateExtractor) TokenLimiterOption { } } +// Clock sets the clock func Clock(clock timetools.TimeProvider) TokenLimiterOption { return func(cl *TokenLimiter) error { cl.clock = clock @@ -212,6 +236,7 @@ func Clock(clock timetools.TimeProvider) TokenLimiterOption { } } +// Capacity sets the capacity func Capacity(cap int) TokenLimiterOption { return func(cl *TokenLimiter) error { if cap <= 0 { diff --git a/vendor/github.com/vulcand/oxy/roundrobin/RequestRewriteListener.go b/vendor/github.com/vulcand/oxy/roundrobin/RequestRewriteListener.go index 418f4988c..02ae4548e 100644 --- a/vendor/github.com/vulcand/oxy/roundrobin/RequestRewriteListener.go +++ b/vendor/github.com/vulcand/oxy/roundrobin/RequestRewriteListener.go @@ -2,4 +2,5 @@ package roundrobin import "net/http" +// RequestRewriteListener function to rewrite request type RequestRewriteListener func(oldReq *http.Request, newReq *http.Request) diff --git a/vendor/github.com/vulcand/oxy/roundrobin/rebalancer.go b/vendor/github.com/vulcand/oxy/roundrobin/rebalancer.go index fec74d26b..1d182d895 100644 --- a/vendor/github.com/vulcand/oxy/roundrobin/rebalancer.go +++ b/vendor/github.com/vulcand/oxy/roundrobin/rebalancer.go @@ -16,13 +16,14 @@ import ( // RebalancerOption - functional option setter for rebalancer type RebalancerOption func(*Rebalancer) error -// Meter measures server peformance and returns it's relative value via rating +// Meter measures server performance and returns it's relative value via rating type Meter interface { Rating() float64 Record(int, time.Duration) IsReady() bool } +// NewMeterFn type of functions to create new Meter type NewMeterFn func() (Meter, error) // Rebalancer increases weights on servers that perform better than others. It also rolls back to original weights @@ -52,8 +53,11 @@ type Rebalancer struct { stickySession *StickySession requestRewriteListener RequestRewriteListener + + log *log.Logger } +// RebalancerClock sets a clock func RebalancerClock(clock timetools.TimeProvider) RebalancerOption { return func(r *Rebalancer) error { r.clock = clock @@ -61,6 +65,7 @@ func RebalancerClock(clock timetools.TimeProvider) RebalancerOption { } } +// RebalancerBackoff sets a beck off duration func RebalancerBackoff(d time.Duration) RebalancerOption { return func(r *Rebalancer) error { r.backoffDuration = d @@ -68,6 +73,7 @@ func RebalancerBackoff(d time.Duration) RebalancerOption { } } +// RebalancerMeter sets a Meter builder function func RebalancerMeter(newMeter NewMeterFn) RebalancerOption { return func(r *Rebalancer) error { r.newMeter = newMeter @@ -83,6 +89,7 @@ func RebalancerErrorHandler(h utils.ErrorHandler) RebalancerOption { } } +// RebalancerStickySession sets a sticky session func RebalancerStickySession(stickySession *StickySession) RebalancerOption { return func(r *Rebalancer) error { r.stickySession = stickySession @@ -90,7 +97,7 @@ func RebalancerStickySession(stickySession *StickySession) RebalancerOption { } } -// RebalancerErrorHandler is a functional argument that sets error handler of the server +// RebalancerRequestRewriteListener is a functional argument that sets error handler of the server func RebalancerRequestRewriteListener(rrl RequestRewriteListener) RebalancerOption { return func(r *Rebalancer) error { r.requestRewriteListener = rrl @@ -98,11 +105,14 @@ func RebalancerRequestRewriteListener(rrl RequestRewriteListener) RebalancerOpti } } +// NewRebalancer creates a new Rebalancer func NewRebalancer(handler balancerHandler, opts ...RebalancerOption) (*Rebalancer, error) { rb := &Rebalancer{ mtx: &sync.Mutex{}, next: handler, stickySession: nil, + + log: log.StandardLogger(), } for _, o := range opts { if err := o(rb); err != nil { @@ -134,6 +144,17 @@ func NewRebalancer(handler balancerHandler, opts ...RebalancerOption) (*Rebalanc return rb, nil } +// RebalancerLogger defines the logger the rebalancer will use. +// +// It defaults to logrus.StandardLogger(), the global logger used by logrus. +func RebalancerLogger(l *log.Logger) RebalancerOption { + return func(rb *Rebalancer) error { + rb.log = l + return nil + } +} + +// Servers gets all servers func (rb *Rebalancer) Servers() []*url.URL { rb.mtx.Lock() defer rb.mtx.Unlock() @@ -142,8 +163,8 @@ func (rb *Rebalancer) Servers() []*url.URL { } func (rb *Rebalancer) ServeHTTP(w http.ResponseWriter, req *http.Request) { - if log.GetLevel() >= log.DebugLevel { - logEntry := log.WithField("Request", utils.DumpHttpRequest(req)) + if rb.log.Level >= log.DebugLevel { + logEntry := rb.log.WithField("Request", utils.DumpHttpRequest(req)) logEntry.Debug("vulcand/oxy/roundrobin/rebalancer: begin ServeHttp on request") defer logEntry.Debug("vulcand/oxy/roundrobin/rebalancer: completed ServeHttp on request") } @@ -169,25 +190,25 @@ func (rb *Rebalancer) ServeHTTP(w http.ResponseWriter, req *http.Request) { } if !stuck { - url, err := rb.next.NextServer() + fwdURL, err := rb.next.NextServer() if err != nil { rb.errHandler.ServeHTTP(w, req, err) return } if log.GetLevel() >= log.DebugLevel { - //log which backend URL we're sending this request to - log.WithFields(log.Fields{"Request": utils.DumpHttpRequest(req), "ForwardURL": url}).Debugf("vulcand/oxy/roundrobin/rebalancer: Forwarding this request to URL") + // log which backend URL we're sending this request to + log.WithFields(log.Fields{"Request": utils.DumpHttpRequest(req), "ForwardURL": fwdURL}).Debugf("vulcand/oxy/roundrobin/rebalancer: Forwarding this request to URL") } if rb.stickySession != nil { - rb.stickySession.StickBackend(url, &w) + rb.stickySession.StickBackend(fwdURL, &w) } - newReq.URL = url + newReq.URL = fwdURL } - //Emit event to a listener if one exists + // Emit event to a listener if one exists if rb.requestRewriteListener != nil { rb.requestRewriteListener(req, &newReq) } @@ -215,6 +236,7 @@ func (rb *Rebalancer) reset() { rb.ratings = make([]float64, len(rb.servers)) } +// Wrap sets the next handler to be called by rebalancer handler. func (rb *Rebalancer) Wrap(next balancerHandler) error { if rb.next != nil { return fmt.Errorf("already bound to %T", rb.next) @@ -223,6 +245,7 @@ func (rb *Rebalancer) Wrap(next balancerHandler) error { return nil } +// UpsertServer upsert a server func (rb *Rebalancer) UpsertServer(u *url.URL, options ...ServerOption) error { rb.mtx.Lock() defer rb.mtx.Unlock() @@ -239,6 +262,7 @@ func (rb *Rebalancer) UpsertServer(u *url.URL, options ...ServerOption) error { return nil } +// RemoveServer remove a server func (rb *Rebalancer) RemoveServer(u *url.URL) error { rb.mtx.Lock() defer rb.mtx.Unlock() @@ -289,7 +313,7 @@ func (rb *Rebalancer) findServer(u *url.URL) (*rbServer, int) { return nil, -1 } -// Called on every load balancer ServeHTTP call, returns the suggested weights +// adjustWeights Called on every load balancer ServeHTTP call, returns the suggested weights // on every call, can adjust weights if needed. func (rb *Rebalancer) adjustWeights() { rb.mtx.Lock() @@ -319,7 +343,7 @@ func (rb *Rebalancer) adjustWeights() { func (rb *Rebalancer) applyWeights() { for _, srv := range rb.servers { - log.Debugf("upsert server %v, weight %v", srv.url, srv.curWeight) + rb.log.Debugf("upsert server %v, weight %v", srv.url, srv.curWeight) rb.next.UpsertServer(srv.url, Weight(srv.curWeight)) } } @@ -331,7 +355,7 @@ func (rb *Rebalancer) setMarkedWeights() bool { if srv.good { weight := increase(srv.curWeight) if weight <= FSMMaxWeight { - log.Debugf("increasing weight of %v from %v to %v", srv.url, srv.curWeight, weight) + rb.log.Debugf("increasing weight of %v from %v to %v", srv.url, srv.curWeight, weight) srv.curWeight = weight changed = true } @@ -378,7 +402,7 @@ func (rb *Rebalancer) markServers() bool { } } if len(g) != 0 && len(b) != 0 { - log.Debugf("bad: %v good: %v, ratings: %v", b, g, rb.ratings) + rb.log.Debugf("bad: %v good: %v, ratings: %v", b, g, rb.ratings) } return len(g) != 0 && len(b) != 0 } @@ -433,9 +457,8 @@ func decrease(target, current int) int { adjusted := current / FSMGrowFactor if adjusted < target { return target - } else { - return adjusted } + return adjusted } // rebalancer server record that keeps track of the original weight supplied by user @@ -448,9 +471,9 @@ type rbServer struct { } const ( - // This is the maximum weight that handler will set for the server + // FSMMaxWeight is the maximum weight that handler will set for the server FSMMaxWeight = 4096 - // Multiplier for the server weight + // FSMGrowFactor Multiplier for the server weight FSMGrowFactor = 4 ) @@ -460,10 +483,12 @@ type codeMeter struct { codeE int } +// Rating gets ratio func (n *codeMeter) Rating() float64 { return n.r.Ratio() } +// Record records a meter func (n *codeMeter) Record(code int, d time.Duration) { if code >= n.codeS && code < n.codeE { n.r.IncA(1) @@ -472,6 +497,7 @@ func (n *codeMeter) Record(code int, d time.Duration) { } } +// IsReady returns true if the counter is ready func (n *codeMeter) IsReady() bool { return n.r.IsReady() } diff --git a/vendor/github.com/vulcand/oxy/roundrobin/rr.go b/vendor/github.com/vulcand/oxy/roundrobin/rr.go index 053773b7d..631a97af8 100644 --- a/vendor/github.com/vulcand/oxy/roundrobin/rr.go +++ b/vendor/github.com/vulcand/oxy/roundrobin/rr.go @@ -1,4 +1,4 @@ -// package roundrobin implements dynamic weighted round robin load balancer http handler +// Package roundrobin implements dynamic weighted round robin load balancer http handler package roundrobin import ( @@ -30,6 +30,7 @@ func ErrorHandler(h utils.ErrorHandler) LBOption { } } +// EnableStickySession enable sticky session func EnableStickySession(stickySession *StickySession) LBOption { return func(s *RoundRobin) error { s.stickySession = stickySession @@ -37,7 +38,7 @@ func EnableStickySession(stickySession *StickySession) LBOption { } } -// ErrorHandler is a functional argument that sets error handler of the server +// RoundRobinRequestRewriteListener is a functional argument that sets error handler of the server func RoundRobinRequestRewriteListener(rrl RequestRewriteListener) LBOption { return func(s *RoundRobin) error { s.requestRewriteListener = rrl @@ -45,6 +46,7 @@ func RoundRobinRequestRewriteListener(rrl RequestRewriteListener) LBOption { } } +// RoundRobin implements dynamic weighted round robin load balancer http handler type RoundRobin struct { mutex *sync.Mutex next http.Handler @@ -55,8 +57,11 @@ type RoundRobin struct { currentWeight int stickySession *StickySession requestRewriteListener RequestRewriteListener + + log *log.Logger } +// New created a new RoundRobin func New(next http.Handler, opts ...LBOption) (*RoundRobin, error) { rr := &RoundRobin{ next: next, @@ -64,6 +69,8 @@ func New(next http.Handler, opts ...LBOption) (*RoundRobin, error) { mutex: &sync.Mutex{}, servers: []*server{}, stickySession: nil, + + log: log.StandardLogger(), } for _, o := range opts { if err := o(rr); err != nil { @@ -76,13 +83,24 @@ func New(next http.Handler, opts ...LBOption) (*RoundRobin, error) { return rr, nil } +// RoundRobinLogger defines the logger the round robin load balancer will use. +// +// It defaults to logrus.StandardLogger(), the global logger used by logrus. +func RoundRobinLogger(l *log.Logger) LBOption { + return func(r *RoundRobin) error { + r.log = l + return nil + } +} + +// Next returns the next handler func (r *RoundRobin) Next() http.Handler { return r.next } func (r *RoundRobin) ServeHTTP(w http.ResponseWriter, req *http.Request) { - if log.GetLevel() >= log.DebugLevel { - logEntry := log.WithField("Request", utils.DumpHttpRequest(req)) + if r.log.Level >= log.DebugLevel { + logEntry := r.log.WithField("Request", utils.DumpHttpRequest(req)) logEntry.Debug("vulcand/oxy/roundrobin/rr: begin ServeHttp on request") defer logEntry.Debug("vulcand/oxy/roundrobin/rr: completed ServeHttp on request") } @@ -116,12 +134,12 @@ func (r *RoundRobin) ServeHTTP(w http.ResponseWriter, req *http.Request) { newReq.URL = url } - if log.GetLevel() >= log.DebugLevel { - //log which backend URL we're sending this request to - log.WithFields(log.Fields{"Request": utils.DumpHttpRequest(req), "ForwardURL": newReq.URL}).Debugf("vulcand/oxy/roundrobin/rr: Forwarding this request to URL") + if r.log.Level >= log.DebugLevel { + // log which backend URL we're sending this request to + r.log.WithFields(log.Fields{"Request": utils.DumpHttpRequest(req), "ForwardURL": newReq.URL}).Debugf("vulcand/oxy/roundrobin/rr: Forwarding this request to URL") } - //Emit event to a listener if one exists + // Emit event to a listener if one exists if r.requestRewriteListener != nil { r.requestRewriteListener(req, &newReq) } @@ -129,6 +147,7 @@ func (r *RoundRobin) ServeHTTP(w http.ResponseWriter, req *http.Request) { r.next.ServeHTTP(w, &newReq) } +// NextServer gets the next server func (r *RoundRobin) NextServer() (*url.URL, error) { srv, err := r.nextServer() if err != nil { @@ -172,6 +191,7 @@ func (r *RoundRobin) nextServer() (*server, error) { } } +// RemoveServer remove a server func (r *RoundRobin) RemoveServer(u *url.URL) error { r.mutex.Lock() defer r.mutex.Unlock() @@ -185,6 +205,7 @@ func (r *RoundRobin) RemoveServer(u *url.URL) error { return nil } +// Servers gets servers URL func (r *RoundRobin) Servers() []*url.URL { r.mutex.Lock() defer r.mutex.Unlock() @@ -196,6 +217,7 @@ func (r *RoundRobin) Servers() []*url.URL { return out } +// ServerWeight gets the server weight func (r *RoundRobin) ServerWeight(u *url.URL) (int, bool) { r.mutex.Lock() defer r.mutex.Unlock() @@ -206,7 +228,7 @@ func (r *RoundRobin) ServerWeight(u *url.URL) (int, bool) { return -1, false } -// In case if server is already present in the load balancer, returns error +// UpsertServer In case if server is already present in the load balancer, returns error func (r *RoundRobin) UpsertServer(u *url.URL, options ...ServerOption) error { r.mutex.Lock() defer r.mutex.Unlock() @@ -306,6 +328,7 @@ type server struct { var defaultWeight = 1 +// SetDefaultWeight sets the default server weight func SetDefaultWeight(weight int) error { if weight < 0 { return fmt.Errorf("default weight should be >= 0") diff --git a/vendor/github.com/vulcand/oxy/roundrobin/stickysessions.go b/vendor/github.com/vulcand/oxy/roundrobin/stickysessions.go index 3fabeb975..123fbdfad 100644 --- a/vendor/github.com/vulcand/oxy/roundrobin/stickysessions.go +++ b/vendor/github.com/vulcand/oxy/roundrobin/stickysessions.go @@ -1,4 +1,3 @@ -// package stickysession is a mixin for load balancers that implements layer 7 (http cookie) session affinity package roundrobin import ( @@ -6,12 +5,14 @@ import ( "net/url" ) +// StickySession is a mixin for load balancers that implements layer 7 (http cookie) session affinity type StickySession struct { cookieName string } +// NewStickySession creates a new StickySession func NewStickySession(cookieName string) *StickySession { - return &StickySession{cookieName} + return &StickySession{cookieName: cookieName} } // GetBackend returns the backend URL stored in the sticky cookie, iff the backend is still in the valid list of servers. @@ -32,11 +33,11 @@ func (s *StickySession) GetBackend(req *http.Request, servers []*url.URL) (*url. if s.isBackendAlive(serverURL, servers) { return serverURL, true, nil - } else { - return nil, false, nil } + return nil, false, nil } +// StickBackend creates and sets the cookie func (s *StickySession) StickBackend(backend *url.URL, w *http.ResponseWriter) { cookie := &http.Cookie{Name: s.cookieName, Value: backend.String(), Path: "/"} http.SetCookie(*w, cookie) diff --git a/vendor/github.com/vulcand/oxy/utils/auth.go b/vendor/github.com/vulcand/oxy/utils/auth.go index b80b91685..4fd819cfe 100644 --- a/vendor/github.com/vulcand/oxy/utils/auth.go +++ b/vendor/github.com/vulcand/oxy/utils/auth.go @@ -6,6 +6,7 @@ import ( "strings" ) +// BasicAuth basic auth information type BasicAuth struct { Username string Password string @@ -16,6 +17,7 @@ func (ba *BasicAuth) String() string { return fmt.Sprintf("Basic %s", encoded) } +// ParseAuthHeader creates a new BasicAuth from header values func ParseAuthHeader(header string) (*BasicAuth, error) { values := strings.Fields(header) if len(values) != 2 { diff --git a/vendor/github.com/vulcand/oxy/utils/dumpreq.go b/vendor/github.com/vulcand/oxy/utils/dumpreq.go index ef34d38f6..eecb2220c 100644 --- a/vendor/github.com/vulcand/oxy/utils/dumpreq.go +++ b/vendor/github.com/vulcand/oxy/utils/dumpreq.go @@ -9,6 +9,7 @@ import ( "net/url" ) +// SerializableHttpRequest serializable HTTP request type SerializableHttpRequest struct { Method string URL *url.URL @@ -28,6 +29,7 @@ type SerializableHttpRequest struct { TLS *tls.ConnectionState } +// Clone clone a request func Clone(r *http.Request) *SerializableHttpRequest { if r == nil { return nil @@ -47,14 +49,16 @@ func Clone(r *http.Request) *SerializableHttpRequest { return rc } +// ToJson serializes to JSON func (s *SerializableHttpRequest) ToJson() string { - if jsonVal, err := json.Marshal(s); err != nil || jsonVal == nil { - return fmt.Sprintf("Error marshalling SerializableHttpRequest to json: %s", err.Error()) - } else { - return string(jsonVal) + jsonVal, err := json.Marshal(s) + if err != nil || jsonVal == nil { + return fmt.Sprintf("Error marshalling SerializableHttpRequest to json: %s", err) } + return string(jsonVal) } +// DumpHttpRequest dump a HTTP request to JSON func DumpHttpRequest(req *http.Request) string { - return fmt.Sprintf("%v", Clone(req).ToJson()) + return Clone(req).ToJson() } diff --git a/vendor/github.com/vulcand/oxy/utils/handler.go b/vendor/github.com/vulcand/oxy/utils/handler.go index 003fc0319..24b9e3a88 100644 --- a/vendor/github.com/vulcand/oxy/utils/handler.go +++ b/vendor/github.com/vulcand/oxy/utils/handler.go @@ -1,22 +1,34 @@ package utils import ( + "context" "io" "net" "net/http" + + log "github.com/sirupsen/logrus" ) +// StatusClientClosedRequest non-standard HTTP status code for client disconnection +const StatusClientClosedRequest = 499 + +// StatusClientClosedRequestText non-standard HTTP status for client disconnection +const StatusClientClosedRequestText = "Client Closed Request" + +// ErrorHandler error handler type ErrorHandler interface { ServeHTTP(w http.ResponseWriter, req *http.Request, err error) } +// DefaultHandler default error handler var DefaultHandler ErrorHandler = &StdHandler{} -type StdHandler struct { -} +// StdHandler Standard error handler +type StdHandler struct{} func (e *StdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request, err error) { statusCode := http.StatusInternalServerError + if e, ok := err.(net.Error); ok { if e.Timeout() { statusCode = http.StatusGatewayTimeout @@ -25,11 +37,23 @@ func (e *StdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request, err err } } else if err == io.EOF { statusCode = http.StatusBadGateway + } else if err == context.Canceled { + statusCode = StatusClientClosedRequest } + w.WriteHeader(statusCode) - w.Write([]byte(http.StatusText(statusCode))) + w.Write([]byte(statusText(statusCode))) + log.Debugf("'%d %s' caused by: %v", statusCode, statusText(statusCode), err) } +func statusText(statusCode int) string { + if statusCode == StatusClientClosedRequest { + return StatusClientClosedRequestText + } + return http.StatusText(statusCode) +} + +// ErrorHandlerFunc error handler function type type ErrorHandlerFunc func(http.ResponseWriter, *http.Request, error) // ServeHTTP calls f(w, r). diff --git a/vendor/github.com/vulcand/oxy/utils/netutils.go b/vendor/github.com/vulcand/oxy/utils/netutils.go index 95c30e7e5..692d30038 100644 --- a/vendor/github.com/vulcand/oxy/utils/netutils.go +++ b/vendor/github.com/vulcand/oxy/utils/netutils.go @@ -12,18 +12,29 @@ import ( log "github.com/sirupsen/logrus" ) +// ProxyWriter calls recorder, used to debug logs type ProxyWriter struct { - W http.ResponseWriter + w http.ResponseWriter code int length int64 + + log *log.Logger } -func NewProxyWriter(writer http.ResponseWriter) *ProxyWriter { +// NewProxyWriter creates a new ProxyWriter +func NewProxyWriter(w http.ResponseWriter) *ProxyWriter { + return NewProxyWriterWithLogger(w, log.StandardLogger()) +} + +// NewProxyWriterWithLogger creates a new ProxyWriter +func NewProxyWriterWithLogger(w http.ResponseWriter, l *log.Logger) *ProxyWriter { return &ProxyWriter{ - W: writer, + w: w, + log: l, } } +// StatusCode gets status code func (p *ProxyWriter) StatusCode() int { if p.code == 0 { // per contract standard lib will set this to http.StatusOK if not set @@ -33,46 +44,54 @@ func (p *ProxyWriter) StatusCode() int { return p.code } +// GetLength gets content length func (p *ProxyWriter) GetLength() int64 { return p.length } +// Header gets response header func (p *ProxyWriter) Header() http.Header { - return p.W.Header() + return p.w.Header() } func (p *ProxyWriter) Write(buf []byte) (int, error) { p.length = p.length + int64(len(buf)) - return p.W.Write(buf) + return p.w.Write(buf) } +// WriteHeader writes status code func (p *ProxyWriter) WriteHeader(code int) { p.code = code - p.W.WriteHeader(code) + p.w.WriteHeader(code) } +// Flush flush the writer func (p *ProxyWriter) Flush() { - if f, ok := p.W.(http.Flusher); ok { + if f, ok := p.w.(http.Flusher); ok { f.Flush() } } +// CloseNotify returns a channel that receives at most a single value (true) +// when the client connection has gone away. func (p *ProxyWriter) CloseNotify() <-chan bool { - if cn, ok := p.W.(http.CloseNotifier); ok { + if cn, ok := p.w.(http.CloseNotifier); ok { return cn.CloseNotify() } - log.Debugf("Upstream ResponseWriter of type %v does not implement http.CloseNotifier. Returning dummy channel.", reflect.TypeOf(p.W)) + p.log.Debugf("Upstream ResponseWriter of type %v does not implement http.CloseNotifier. Returning dummy channel.", reflect.TypeOf(p.w)) return make(<-chan bool) } +// Hijack lets the caller take over the connection. func (p *ProxyWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { - if hi, ok := p.W.(http.Hijacker); ok { + if hi, ok := p.w.(http.Hijacker); ok { return hi.Hijack() } - log.Debugf("Upstream ResponseWriter of type %v does not implement http.Hijacker. Returning dummy channel.", reflect.TypeOf(p.W)) - return nil, nil, fmt.Errorf("the response writer that was wrapped in this proxy, does not implement http.Hijacker. It is of type: %v", reflect.TypeOf(p.W)) + p.log.Debugf("Upstream ResponseWriter of type %v does not implement http.Hijacker. Returning dummy channel.", reflect.TypeOf(p.w)) + return nil, nil, fmt.Errorf("the response writer that was wrapped in this proxy, does not implement http.Hijacker. It is of type: %v", reflect.TypeOf(p.w)) } +// NewBufferWriter creates a new BufferWriter func NewBufferWriter(w io.WriteCloser) *BufferWriter { return &BufferWriter{ W: w, @@ -80,16 +99,19 @@ func NewBufferWriter(w io.WriteCloser) *BufferWriter { } } +// BufferWriter buffer writer type BufferWriter struct { H http.Header Code int W io.WriteCloser } +// Close close the writer func (b *BufferWriter) Close() error { return b.W.Close() } +// Header gets response header func (b *BufferWriter) Header() http.Header { return b.H } @@ -98,11 +120,13 @@ func (b *BufferWriter) Write(buf []byte) (int, error) { return b.W.Write(buf) } -// WriteHeader sets rw.Code. +// WriteHeader writes status code func (b *BufferWriter) WriteHeader(code int) { b.Code = code } +// CloseNotify returns a channel that receives at most a single value (true) +// when the client connection has gone away. func (b *BufferWriter) CloseNotify() <-chan bool { if cn, ok := b.W.(http.CloseNotifier); ok { return cn.CloseNotify() @@ -111,6 +135,7 @@ func (b *BufferWriter) CloseNotify() <-chan bool { return make(<-chan bool) } +// Hijack lets the caller take over the connection. func (b *BufferWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { if hi, ok := b.W.(http.Hijacker); ok { return hi.Hijack() @@ -125,10 +150,10 @@ type nopWriteCloser struct { func (*nopWriteCloser) Close() error { return nil } -// NopCloser returns a WriteCloser with a no-op Close method wrapping +// NopWriteCloser returns a WriteCloser with a no-op Close method wrapping // the provided Writer w. func NopWriteCloser(w io.Writer) io.WriteCloser { - return &nopWriteCloser{w} + return &nopWriteCloser{Writer: w} } // CopyURL provides update safe copy by avoiding shallow copying User field diff --git a/vendor/github.com/vulcand/oxy/utils/source.go b/vendor/github.com/vulcand/oxy/utils/source.go index 4ed89e13b..5306b597c 100644 --- a/vendor/github.com/vulcand/oxy/utils/source.go +++ b/vendor/github.com/vulcand/oxy/utils/source.go @@ -6,21 +6,25 @@ import ( "strings" ) -// ExtractSource extracts the source from the request, e.g. that may be client ip, or particular header that +// SourceExtractor extracts the source from the request, e.g. that may be client ip, or particular header that // identifies the source. amount stands for amount of connections the source consumes, usually 1 for connection limiters // error should be returned when source can not be identified type SourceExtractor interface { Extract(req *http.Request) (token string, amount int64, err error) } +// ExtractorFunc extractor function type type ExtractorFunc func(req *http.Request) (token string, amount int64, err error) +// Extract extract from request func (f ExtractorFunc) Extract(req *http.Request) (string, int64, error) { return f(req) } +// ExtractSource extract source function type type ExtractSource func(req *http.Request) +// NewExtractor creates a new SourceExtractor func NewExtractor(variable string) (SourceExtractor, error) { if variable == "client.ip" { return ExtractorFunc(extractClientIP), nil @@ -31,17 +35,17 @@ func NewExtractor(variable string) (SourceExtractor, error) { if strings.HasPrefix(variable, "request.header.") { header := strings.TrimPrefix(variable, "request.header.") if len(header) == 0 { - return nil, fmt.Errorf("Wrong header: %s", header) + return nil, fmt.Errorf("wrong header: %s", header) } return makeHeaderExtractor(header), nil } - return nil, fmt.Errorf("Unsupported limiting variable: '%s'", variable) + return nil, fmt.Errorf("unsupported limiting variable: '%s'", variable) } func extractClientIP(req *http.Request) (string, int64, error) { vals := strings.SplitN(req.RemoteAddr, ":", 2) if len(vals[0]) == 0 { - return "", 0, fmt.Errorf("Failed to parse client IP: %v", req.RemoteAddr) + return "", 0, fmt.Errorf("failed to parse client IP: %v", req.RemoteAddr) } return vals[0], 1, nil } From a7bb768e98064610baaf37be95b9c873386752e1 Mon Sep 17 00:00:00 2001 From: SALLEYRON Julien Date: Mon, 20 Aug 2018 11:16:02 +0200 Subject: [PATCH 18/35] Remove TLS in API --- integration/consul_test.go | 21 +++-------- integration/etcd3_test.go | 42 +++++----------------- integration/etcd_test.go | 21 +++-------- integration/fixtures/file/dir/simple2.toml | 2 +- integration/try/condition.go | 25 +++++++++++++ integration/try/try.go | 35 +++++++++++++----- types/types.go | 2 +- 7 files changed, 69 insertions(+), 79 deletions(-) diff --git a/integration/consul_test.go b/integration/consul_test.go index 7014e5149..77860fc5a 100644 --- a/integration/consul_test.go +++ b/integration/consul_test.go @@ -585,21 +585,14 @@ func (s *ConsulSuite) TestSNIDynamicTlsConfig(c *check.C) { }) c.Assert(err, checker.IsNil) - // wait for traefik - err = try.GetRequest("http://127.0.0.1:8081/api/providers", 60*time.Second, try.BodyContains("MIIEpQIBAAKCAQEA1RducBK6EiFDv3TYB8ZcrfKWRVaSfHzWicO3J5WdST9oS7hG")) - c.Assert(err, checker.IsNil) - req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) c.Assert(err, checker.IsNil) - client := &http.Client{Transport: tr1} req.Host = tr1.TLSClientConfig.ServerName req.Header.Set("Host", tr1.TLSClientConfig.ServerName) req.Header.Set("Accept", "*/*") - var resp *http.Response - resp, err = client.Do(req) + + err = try.RequestWithTransport(req, 30*time.Second, tr1, try.HasCn("snitest.com")) c.Assert(err, checker.IsNil) - cn := resp.TLS.PeerCertificates[0].Subject.CommonName - c.Assert(cn, checker.Equals, "snitest.com") // now we configure the second keypair in consul and the request for host "snitest.org" will use the second keypair for key, value := range tlsconfigure2 { @@ -614,18 +607,12 @@ func (s *ConsulSuite) TestSNIDynamicTlsConfig(c *check.C) { }) c.Assert(err, checker.IsNil) - // waiting for traefik to pull configuration - err = try.GetRequest("http://127.0.0.1:8081/api/providers", 30*time.Second, try.BodyContains("MIIEogIBAAKCAQEAvG9kL+vF57+MICehzbqcQAUlAOSl5r")) - c.Assert(err, checker.IsNil) - req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) c.Assert(err, checker.IsNil) - client = &http.Client{Transport: tr2} req.Host = tr2.TLSClientConfig.ServerName req.Header.Set("Host", tr2.TLSClientConfig.ServerName) req.Header.Set("Accept", "*/*") - resp, err = client.Do(req) + + err = try.RequestWithTransport(req, 30*time.Second, tr2, try.HasCn("snitest.org")) c.Assert(err, checker.IsNil) - cn = resp.TLS.PeerCertificates[0].Subject.CommonName - c.Assert(cn, checker.Equals, "snitest.org") } diff --git a/integration/etcd3_test.go b/integration/etcd3_test.go index 0d01d861e..3471dd9dd 100644 --- a/integration/etcd3_test.go +++ b/integration/etcd3_test.go @@ -532,21 +532,14 @@ func (s *Etcd3Suite) TestSNIDynamicTlsConfig(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - // wait for Træfik - err = try.GetRequest("http://127.0.0.1:8081/api/providers", 60*time.Second, try.BodyContains(string("MIIEpQIBAAKCAQEA1RducBK6EiFDv3TYB8ZcrfKWRVaSfHzWicO3J5WdST9oS7h"))) - c.Assert(err, checker.IsNil) - req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) c.Assert(err, checker.IsNil) - client := &http.Client{Transport: tr1} req.Host = tr1.TLSClientConfig.ServerName req.Header.Set("Host", tr1.TLSClientConfig.ServerName) req.Header.Set("Accept", "*/*") - var resp *http.Response - resp, err = client.Do(req) + + err = try.RequestWithTransport(req, 30*time.Second, tr1, try.HasCn("snitest.com")) c.Assert(err, checker.IsNil) - cn := resp.TLS.PeerCertificates[0].Subject.CommonName - c.Assert(cn, checker.Equals, "snitest.com") // now we configure the second keypair in etcd and the request for host "snitest.org" will use the second keypair @@ -562,20 +555,14 @@ func (s *Etcd3Suite) TestSNIDynamicTlsConfig(c *check.C) { }) c.Assert(err, checker.IsNil) - // waiting for Træfik to pull configuration - err = try.GetRequest("http://127.0.0.1:8081/api/providers", 30*time.Second, try.BodyContains("MIIEogIBAAKCAQEAvG9kL+vF57+MICehzbqcQAUlAOSl5r")) - c.Assert(err, checker.IsNil) - req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) c.Assert(err, checker.IsNil) - client = &http.Client{Transport: tr2} req.Host = tr2.TLSClientConfig.ServerName req.Header.Set("Host", tr2.TLSClientConfig.ServerName) req.Header.Set("Accept", "*/*") - resp, err = client.Do(req) + + err = try.RequestWithTransport(req, 30*time.Second, tr2, try.HasCn("snitest.org")) c.Assert(err, checker.IsNil) - cn = resp.TLS.PeerCertificates[0].Subject.CommonName - c.Assert(cn, checker.Equals, "snitest.org") } func (s *Etcd3Suite) TestDeleteSNIDynamicTlsConfig(c *check.C) { @@ -646,21 +633,14 @@ func (s *Etcd3Suite) TestDeleteSNIDynamicTlsConfig(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - // wait for Træfik - err = try.GetRequest(traefikWebEtcdURL+"api/providers", 60*time.Second, try.BodyContains(string("MIIEpQIBAAKCAQEA1RducBK6EiFDv3TYB8ZcrfKWRVaSfHzWicO3J5WdST9oS7h"))) - c.Assert(err, checker.IsNil) - req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) c.Assert(err, checker.IsNil) - client := &http.Client{Transport: tr1} req.Host = tr1.TLSClientConfig.ServerName req.Header.Set("Host", tr1.TLSClientConfig.ServerName) req.Header.Set("Accept", "*/*") - var resp *http.Response - resp, err = client.Do(req) + + err = try.RequestWithTransport(req, 30*time.Second, tr1, try.HasCn("snitest.com")) c.Assert(err, checker.IsNil) - cn := resp.TLS.PeerCertificates[0].Subject.CommonName - c.Assert(cn, checker.Equals, "snitest.com") // now we delete the tls cert/key pairs,so the endpoint show use default cert/key pair for key := range tlsconfigure1 { @@ -668,18 +648,12 @@ func (s *Etcd3Suite) TestDeleteSNIDynamicTlsConfig(c *check.C) { c.Assert(err, checker.IsNil) } - // waiting for Træfik to pull configuration - err = try.GetRequest(traefikWebEtcdURL+"api/providers", 30*time.Second, try.BodyNotContains("MIIEpQIBAAKCAQEA1RducBK6EiFDv3TYB8ZcrfKWRVaSfHzWicO3J5WdST9oS7h")) - c.Assert(err, checker.IsNil) - req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) c.Assert(err, checker.IsNil) - client = &http.Client{Transport: tr1} req.Host = tr1.TLSClientConfig.ServerName req.Header.Set("Host", tr1.TLSClientConfig.ServerName) req.Header.Set("Accept", "*/*") - resp, err = client.Do(req) + + err = try.RequestWithTransport(req, 30*time.Second, tr1, try.HasCn("TRAEFIK DEFAULT CERT")) c.Assert(err, checker.IsNil) - cn = resp.TLS.PeerCertificates[0].Subject.CommonName - c.Assert(cn, checker.Equals, "TRAEFIK DEFAULT CERT") } diff --git a/integration/etcd_test.go b/integration/etcd_test.go index 6e6133f86..9ae0e303f 100644 --- a/integration/etcd_test.go +++ b/integration/etcd_test.go @@ -548,21 +548,14 @@ func (s *EtcdSuite) TestSNIDynamicTlsConfig(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - // wait for Træfik - err = try.GetRequest("http://127.0.0.1:8081/api/providers", 60*time.Second, try.BodyContains(string("MIIEpQIBAAKCAQEA1RducBK6EiFDv3TYB8ZcrfKWRVaSfHzWicO3J5WdST9oS7h"))) - c.Assert(err, checker.IsNil) - req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) c.Assert(err, checker.IsNil) - client := &http.Client{Transport: tr1} req.Host = tr1.TLSClientConfig.ServerName req.Header.Set("Host", tr1.TLSClientConfig.ServerName) req.Header.Set("Accept", "*/*") - var resp *http.Response - resp, err = client.Do(req) + + err = try.RequestWithTransport(req, 30*time.Second, tr1, try.HasCn("snitest.com")) c.Assert(err, checker.IsNil) - cn := resp.TLS.PeerCertificates[0].Subject.CommonName - c.Assert(cn, checker.Equals, "snitest.com") // now we configure the second keypair in etcd and the request for host "snitest.org" will use the second keypair @@ -578,18 +571,12 @@ func (s *EtcdSuite) TestSNIDynamicTlsConfig(c *check.C) { }) c.Assert(err, checker.IsNil) - // waiting for Træfik to pull configuration - err = try.GetRequest("http://127.0.0.1:8081/api/providers", 30*time.Second, try.BodyContains("MIIEogIBAAKCAQEAvG9kL+vF57+MICehzbqcQAUlAOSl5r")) - c.Assert(err, checker.IsNil) - req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) c.Assert(err, checker.IsNil) - client = &http.Client{Transport: tr2} req.Host = tr2.TLSClientConfig.ServerName req.Header.Set("Host", tr2.TLSClientConfig.ServerName) req.Header.Set("Accept", "*/*") - resp, err = client.Do(req) + + err = try.RequestWithTransport(req, 30*time.Second, tr2, try.HasCn("snitest.org")) c.Assert(err, checker.IsNil) - cn = resp.TLS.PeerCertificates[0].Subject.CommonName - c.Assert(cn, checker.Equals, "snitest.org") } diff --git a/integration/fixtures/file/dir/simple2.toml b/integration/fixtures/file/dir/simple2.toml index e02f63550..dcbcffc57 100644 --- a/integration/fixtures/file/dir/simple2.toml +++ b/integration/fixtures/file/dir/simple2.toml @@ -2,7 +2,7 @@ [backends] [backends.backend2] [backends.backend2.servers.server1] - url = "http://172.17.0.2:80" + url = "http://172.17.0.123:80" weight = 1 [frontends] diff --git a/integration/try/condition.go b/integration/try/condition.go index e7ee9c9d2..a3d5d7656 100644 --- a/integration/try/condition.go +++ b/integration/try/condition.go @@ -88,6 +88,31 @@ func HasBody() ResponseCondition { } } +// HasCn returns a retry condition function. +// The condition returns an error if the cn is not correct. +func HasCn(cn string) ResponseCondition { + return func(res *http.Response) error { + if res.TLS == nil { + return errors.New("response doesn't have TLS") + } + + if len(res.TLS.PeerCertificates) == 0 { + return errors.New("response TLS doesn't have peer certificates") + } + + if res.TLS.PeerCertificates[0] == nil { + return errors.New("first peer certificate is nil") + } + + commonName := res.TLS.PeerCertificates[0].Subject.CommonName + if cn != commonName { + return fmt.Errorf("common name don't match: %s != %s", cn, commonName) + } + + return nil + } +} + // StatusCodeIs returns a retry condition function. // The condition returns an error if the given response's status code is not the // given HTTP status code. diff --git a/integration/try/try.go b/integration/try/try.go index f201cd0d8..0f75cbdaf 100644 --- a/integration/try/try.go +++ b/integration/try/try.go @@ -31,7 +31,7 @@ func Sleep(d time.Duration) { // response body needs to be closed or not. Callers are expected to close on // their own if the function returns a nil error. func Response(req *http.Request, timeout time.Duration) (*http.Response, error) { - return doTryRequest(req, timeout) + return doTryRequest(req, timeout, nil) } // ResponseUntilStatusCode is like Request, but returns the response for further @@ -40,7 +40,7 @@ func Response(req *http.Request, timeout time.Duration) (*http.Response, error) // response body needs to be closed or not. Callers are expected to close on // their own if the function returns a nil error. func ResponseUntilStatusCode(req *http.Request, timeout time.Duration, statusCode int) (*http.Response, error) { - return doTryRequest(req, timeout, StatusCodeIs(statusCode)) + return doTryRequest(req, timeout, nil, StatusCodeIs(statusCode)) } // GetRequest is like Do, but runs a request against the given URL and applies @@ -48,7 +48,7 @@ func ResponseUntilStatusCode(req *http.Request, timeout time.Duration, statusCod // ResponseCondition may be nil, in which case only the request against the URL must // succeed. func GetRequest(url string, timeout time.Duration, conditions ...ResponseCondition) error { - resp, err := doTryGet(url, timeout, conditions...) + resp, err := doTryGet(url, timeout, nil, conditions...) if resp != nil && resp.Body != nil { defer resp.Body.Close() @@ -62,7 +62,21 @@ func GetRequest(url string, timeout time.Duration, conditions ...ResponseConditi // ResponseCondition may be nil, in which case only the request against the URL must // succeed. func Request(req *http.Request, timeout time.Duration, conditions ...ResponseCondition) error { - resp, err := doTryRequest(req, timeout, conditions...) + resp, err := doTryRequest(req, timeout, nil, conditions...) + + if resp != nil && resp.Body != nil { + defer resp.Body.Close() + } + + return err +} + +// RequestWithTransport is like Do, but runs a request against the given URL and applies +// the condition on the response. +// ResponseCondition may be nil, in which case only the request against the URL must +// succeed. +func RequestWithTransport(req *http.Request, timeout time.Duration, transport *http.Transport, conditions ...ResponseCondition) error { + resp, err := doTryRequest(req, timeout, transport, conditions...) if resp != nil && resp.Body != nil { defer resp.Body.Close() @@ -112,24 +126,27 @@ func Do(timeout time.Duration, operation DoCondition) error { } } -func doTryGet(url string, timeout time.Duration, conditions ...ResponseCondition) (*http.Response, error) { +func doTryGet(url string, timeout time.Duration, transport *http.Transport, conditions ...ResponseCondition) (*http.Response, error) { req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, err } - return doTryRequest(req, timeout, conditions...) + return doTryRequest(req, timeout, transport, conditions...) } -func doTryRequest(request *http.Request, timeout time.Duration, conditions ...ResponseCondition) (*http.Response, error) { - return doRequest(Do, timeout, request, conditions...) +func doTryRequest(request *http.Request, timeout time.Duration, transport *http.Transport, conditions ...ResponseCondition) (*http.Response, error) { + return doRequest(Do, timeout, request, transport, conditions...) } -func doRequest(action timedAction, timeout time.Duration, request *http.Request, conditions ...ResponseCondition) (*http.Response, error) { +func doRequest(action timedAction, timeout time.Duration, request *http.Request, transport *http.Transport, conditions ...ResponseCondition) (*http.Response, error) { var resp *http.Response return resp, action(timeout, func() error { var err error client := http.DefaultClient + if transport != nil { + client.Transport = transport + } resp, err = client.Do(request) if err != nil { diff --git a/types/types.go b/types/types.go index eed737bee..ca893b7fc 100644 --- a/types/types.go +++ b/types/types.go @@ -235,7 +235,7 @@ type Configurations map[string]*Configuration type Configuration struct { Backends map[string]*Backend `json:"backends,omitempty"` Frontends map[string]*Frontend `json:"frontends,omitempty"` - TLS []*traefiktls.Configuration `json:"tls,omitempty"` + TLS []*traefiktls.Configuration `json:"-"` } // ConfigMessage hold configuration information exchanged between parts of traefik. From f062ee80c8daa0ede62f094e9a72a1f5d38dd0d0 Mon Sep 17 00:00:00 2001 From: Damien Duportal Date: Mon, 20 Aug 2018 12:02:03 +0200 Subject: [PATCH 19/35] Docs: Adding warnings and solution about the configuration exposure --- docs/configuration/api.md | 21 ++++++++++++++++++++- docs/index.md | 4 ++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/configuration/api.md b/docs/configuration/api.md index 9da05c7e8..05aa23277 100644 --- a/docs/configuration/api.md +++ b/docs/configuration/api.md @@ -4,6 +4,9 @@ ```toml # API definition +# Warning: Enabling API will expose Træfik's configuration and secret. +# It is not recommended in production, +# unless secured by authentication and authorizations [api] # Name of the related entry point # @@ -12,7 +15,7 @@ # entryPoint = "traefik" - # Enabled Dashboard + # Enable Dashboard # # Optional # Default: true @@ -38,6 +41,22 @@ For more customization, see [entry points](/configuration/entrypoints/) document ![Web UI Health](/img/traefik-health.png) +## Security + +Enabling the API will expose all configuration elements, +including secret. + +It is not recommended in production, +unless secured by authentication and authorizations. + +A good sane default (but not exhaustive) set of recommendations +would be to apply the following protection mechanism: + +* _At application level:_ enabling HTTP [Basic Authentication](#authentication) +* _At transport level:_ NOT exposing publicly the API's port, +keeping it restricted over internal networks +(restricted networks as in https://en.wikipedia.org/wiki/Principle_of_least_privilege). + ## API | Path | Method | Description | diff --git a/docs/index.md b/docs/index.md index a0fb81280..a7b6cc339 100644 --- a/docs/index.md +++ b/docs/index.md @@ -86,6 +86,10 @@ services: - /var/run/docker.sock:/var/run/docker.sock # So that Traefik can listen to the Docker events ``` +!!! warning + Enabling the Web UI with the `--api` flag might exposes configuration elements. You can read more about this on the [API/Dashboard's Security section](/configuration/api#security). + + **That's it. Now you can launch Træfik!** Start your `reverse-proxy` with the following command: From 2beb5236d0c9fe0e8f964d69688b1a05d510db30 Mon Sep 17 00:00:00 2001 From: Damien Duportal Date: Mon, 20 Aug 2018 13:34:03 +0200 Subject: [PATCH 20/35] A tiny rewording on the documentation API's page --- docs/configuration/api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration/api.md b/docs/configuration/api.md index 05aa23277..e6e8aa932 100644 --- a/docs/configuration/api.md +++ b/docs/configuration/api.md @@ -4,7 +4,7 @@ ```toml # API definition -# Warning: Enabling API will expose Træfik's configuration and secret. +# Warning: Enabling API will expose Træfik's configuration. # It is not recommended in production, # unless secured by authentication and authorizations [api] @@ -44,7 +44,7 @@ For more customization, see [entry points](/configuration/entrypoints/) document ## Security Enabling the API will expose all configuration elements, -including secret. +including sensitive data. It is not recommended in production, unless secured by authentication and authorizations. From feeb7f81a611eb58685ffd28478add6d179b091f Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 20 Aug 2018 14:46:02 +0200 Subject: [PATCH 21/35] Prepare Release v1.6.6 --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a194c611f..d763ef91c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Change Log +## [v1.6.6](https://github.com/containous/traefik/tree/v1.6.6) (2018-08-20) +[All Commits](https://github.com/containous/traefik/compare/v1.6.5...v1.6.6) + +**Bug fixes:** +- **[acme]** Avoid duplicated ACME resolution ([#3751](https://github.com/containous/traefik/pull/3751) by [nmengin](https://github.com/nmengin)) +- **[api]** Remove TLS in API ([#3788](https://github.com/containous/traefik/pull/3788) by [Juliens](https://github.com/Juliens)) +- **[cluster]** Remove unusable `--cluster` flag ([#3616](https://github.com/containous/traefik/pull/3616) by [dtomcej](https://github.com/dtomcej)) +- **[ecs]** Fix bad condition in ECS provider ([#3609](https://github.com/containous/traefik/pull/3609) by [mmatur](https://github.com/mmatur)) +- Set keepalive on TCP socket so idleTimeout works ([#3740](https://github.com/containous/traefik/pull/3740) by [ajardan](https://github.com/ajardan)) + +**Documentation:** +- A tiny rewording on the documentation API's page ([#3794](https://github.com/containous/traefik/pull/3794) by [dduportal](https://github.com/dduportal)) +- Adding warnings and solution about the configuration exposure ([#3790](https://github.com/containous/traefik/pull/3790) by [dduportal](https://github.com/dduportal)) +- Fix path to the debug pprof API ([#3608](https://github.com/containous/traefik/pull/3608) by [multani](https://github.com/multani)) + +**Misc:** +- **[oxy,websocket]** Update oxy dependency ([#3777](https://github.com/containous/traefik/pull/3777) by [Juliens](https://github.com/Juliens)) + ## [v1.6.5](https://github.com/containous/traefik/tree/v1.6.5) (2018-07-09) [All Commits](https://github.com/containous/traefik/compare/v1.6.4...v1.6.5) From e46de743285b0675ba00034eff4d5b52ca416cce Mon Sep 17 00:00:00 2001 From: Damien Duportal Date: Mon, 20 Aug 2018 16:46:04 +0200 Subject: [PATCH 22/35] Improve the wording in the documentation for Docker and fix title for Docker User Guide --- docs/configuration/backends/docker.md | 6 +++--- docs/user-guide/docker-and-lets-encrypt.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/configuration/backends/docker.md b/docs/configuration/backends/docker.md index d370cfa17..3f356729c 100644 --- a/docs/configuration/backends/docker.md +++ b/docs/configuration/backends/docker.md @@ -19,7 +19,7 @@ Træfik can be configured to use Docker as a provider. # endpoint = "unix:///var/run/docker.sock" -# Default domain used. +# Default base domain used for the frontend rules. # Can be overridden by setting the "traefik.domain" label on a container. # # Required @@ -110,7 +110,7 @@ To enable constraints see [provider-specific constraints section](/configuration # endpoint = "tcp://127.0.0.1:2375" -# Default domain used. +# Default base domain used for the frontend rules. # Can be overridden by setting the "traefik.domain" label on a services. # # Optional @@ -210,7 +210,7 @@ Labels can be used on containers to override default behavior. | Label | Description | |------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `traefik.docker.network` | Overrides the default docker network to use for connections to the container. [1] | -| `traefik.domain` | Sets the default domain for the frontend rules. | +| `traefik.domain` | Sets the default base domain for the frontend rules. For more information, check the [Container Labels section's of the user guide "Let's Encrypt & Docker"](/user-guide/docker-and-lets-encrypt/#container-labels) | | `traefik.enable=false` | Disables this container in Træfik. | | `traefik.port=80` | Registers this port. Useful when the container exposes multiples ports. | | `traefik.protocol=https` | Overrides the default `http` protocol | diff --git a/docs/user-guide/docker-and-lets-encrypt.md b/docs/user-guide/docker-and-lets-encrypt.md index af7ebe93a..6efcec069 100644 --- a/docs/user-guide/docker-and-lets-encrypt.md +++ b/docs/user-guide/docker-and-lets-encrypt.md @@ -1,4 +1,4 @@ -# Docker & Traefik +# Let's Encrypt & Docker In this use case, we want to use Træfik as a _layer-7_ load balancer with SSL termination for a set of micro-services used to run a web application. From df41cd925e7764883c119346c95a26bfcc4e134f Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Mon, 20 Aug 2018 17:08:03 +0200 Subject: [PATCH 23/35] Add vulnerability form --- docs/index.md | 5 +++++ mkdocs.yml | 3 --- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/index.md b/docs/index.md index a7b6cc339..e5c038b59 100644 --- a/docs/index.md +++ b/docs/index.md @@ -203,3 +203,8 @@ Using the tiny Docker image: ```shell docker run -d -p 8080:8080 -p 80:80 -v $PWD/traefik.toml:/etc/traefik/traefik.toml traefik ``` + +## Security + +We want to keep Træfik safe for everyone. +If you've discovered a security vulnerability in Træfik, we appreciate your help in disclosing it to us in a responsible manner, using [this form](https://security.traefik.io). \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index cf530c9e8..673698651 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -21,9 +21,6 @@ theme: accent: 'light blue' feature: tabs: false - palette: - primary: 'cyan' - accent: 'cyan' i18n: prev: 'Previous' next: 'Next' From cf2d7497e4d199c99dc2fb0c7af87e1016ddc5be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rodr=C3=ADguez?= Date: Mon, 20 Aug 2018 12:34:05 -0300 Subject: [PATCH 24/35] Mention docker-compose as a requirement in the let's encrypt guide --- docs/user-guide/docker-and-lets-encrypt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user-guide/docker-and-lets-encrypt.md b/docs/user-guide/docker-and-lets-encrypt.md index 8fd443037..22eef0a10 100644 --- a/docs/user-guide/docker-and-lets-encrypt.md +++ b/docs/user-guide/docker-and-lets-encrypt.md @@ -8,7 +8,7 @@ In addition, we want to use Let's Encrypt to automatically generate and renew SS ## Setting Up -In order for this to work, you'll need a server with a public IP address, with Docker installed on it. +In order for this to work, you'll need a server with a public IP address, with Docker and docker-compose installed on it. In this example, we're using the fictitious domain _my-awesome-app.org_. From 27e4a8a227d32343fb40850d36c326a25c10a99c Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Mon, 20 Aug 2018 17:50:04 +0200 Subject: [PATCH 25/35] Fixes bad palette in doc --- mkdocs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 673698651..f85f2c2fa 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -17,8 +17,8 @@ theme: favicon: img/traefik.icon.png logo: img/traefik.logo.png palette: - primary: 'blue' - accent: 'light blue' + primary: 'cyan' + accent: 'cyan' feature: tabs: false i18n: From 9c8e5184230551499c9e4950b8c4cef5d496d735 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 21 Aug 2018 17:12:03 +0200 Subject: [PATCH 26/35] Auth Forward with certificates in templates. --- autogen/gentemplates/gen.go | 32 ++++++++++++++++---------------- templates/consul_catalog.tmpl | 4 ++-- templates/docker.tmpl | 4 ++-- templates/ecs.tmpl | 4 ++-- templates/kubernetes.tmpl | 4 ++-- templates/kv.tmpl | 4 ++-- templates/marathon.tmpl | 4 ++-- templates/mesos.tmpl | 4 ++-- templates/rancher.tmpl | 4 ++-- 9 files changed, 32 insertions(+), 32 deletions(-) diff --git a/autogen/gentemplates/gen.go b/autogen/gentemplates/gen.go index 5390fbe2d..d07d4440d 100644 --- a/autogen/gentemplates/gen.go +++ b/autogen/gentemplates/gen.go @@ -224,8 +224,8 @@ var _templatesConsul_catalogTmpl = []byte(`[backends] [frontends."frontend-{{ $service.ServiceName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} @@ -673,8 +673,8 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}} [frontends."frontend-{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} @@ -973,8 +973,8 @@ var _templatesEcsTmpl = []byte(`[backends] [frontends."frontend-{{ $serviceName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} @@ -1246,8 +1246,8 @@ var _templatesKubernetesTmpl = []byte(`[backends] trustForwardHeader = {{ $frontend.Auth.Forward.TrustForwardHeader }} {{if $frontend.Auth.Forward.TLS }} [frontends."{{ $frontendName }}".auth.forward.tls] - cert = "{{ $frontend.Auth.Forward.TLS.Cert }}" - key = "{{ $frontend.Auth.Forward.TLS.Key }}" + cert = """{{ $frontend.Auth.Forward.TLS.Cert }}""" + key = """{{ $frontend.Auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $frontend.Auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} @@ -1466,8 +1466,8 @@ var _templatesKvTmpl = []byte(`[backends] [frontends."{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} @@ -1808,8 +1808,8 @@ var _templatesMarathonTmpl = []byte(`{{ $apps := .Applications }} [frontends."{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} @@ -2094,8 +2094,8 @@ var _templatesMesosTmpl = []byte(`[backends] [frontends."frontend-{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} @@ -2433,8 +2433,8 @@ var _templatesRancherTmpl = []byte(`{{ $backendServers := .Backends }} [frontends."frontend-{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} diff --git a/templates/consul_catalog.tmpl b/templates/consul_catalog.tmpl index c7105d55e..a7ee338ae 100644 --- a/templates/consul_catalog.tmpl +++ b/templates/consul_catalog.tmpl @@ -89,8 +89,8 @@ [frontends."frontend-{{ $service.ServiceName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} diff --git a/templates/docker.tmpl b/templates/docker.tmpl index ae01d9efe..c200551c5 100644 --- a/templates/docker.tmpl +++ b/templates/docker.tmpl @@ -89,8 +89,8 @@ [frontends."frontend-{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} diff --git a/templates/ecs.tmpl b/templates/ecs.tmpl index f230b120e..7801a2ff1 100644 --- a/templates/ecs.tmpl +++ b/templates/ecs.tmpl @@ -88,8 +88,8 @@ [frontends."frontend-{{ $serviceName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} diff --git a/templates/kubernetes.tmpl b/templates/kubernetes.tmpl index 51c03a748..57e5e6344 100644 --- a/templates/kubernetes.tmpl +++ b/templates/kubernetes.tmpl @@ -81,8 +81,8 @@ trustForwardHeader = {{ $frontend.Auth.Forward.TrustForwardHeader }} {{if $frontend.Auth.Forward.TLS }} [frontends."{{ $frontendName }}".auth.forward.tls] - cert = "{{ $frontend.Auth.Forward.TLS.Cert }}" - key = "{{ $frontend.Auth.Forward.TLS.Key }}" + cert = """{{ $frontend.Auth.Forward.TLS.Cert }}""" + key = """{{ $frontend.Auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $frontend.Auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} diff --git a/templates/kv.tmpl b/templates/kv.tmpl index 03a5dd6e1..afeebdabc 100644 --- a/templates/kv.tmpl +++ b/templates/kv.tmpl @@ -88,8 +88,8 @@ [frontends."{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} diff --git a/templates/marathon.tmpl b/templates/marathon.tmpl index 6b42630cb..01b1b09ad 100644 --- a/templates/marathon.tmpl +++ b/templates/marathon.tmpl @@ -91,8 +91,8 @@ [frontends."{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} diff --git a/templates/mesos.tmpl b/templates/mesos.tmpl index 6337cee30..1927e694d 100644 --- a/templates/mesos.tmpl +++ b/templates/mesos.tmpl @@ -91,8 +91,8 @@ [frontends."frontend-{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} diff --git a/templates/rancher.tmpl b/templates/rancher.tmpl index 0b4d38ecb..96500c41b 100644 --- a/templates/rancher.tmpl +++ b/templates/rancher.tmpl @@ -89,8 +89,8 @@ [frontends."frontend-{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} - cert = "{{ $auth.Forward.TLS.Cert }}" - key = "{{ $auth.Forward.TLS.Key }}" + cert = """{{ $auth.Forward.TLS.Cert }}""" + key = """{{ $auth.Forward.TLS.Key }}""" insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} {{end}} {{end}} From 845f1a7377a200c7f859241558e9e7e6729b6960 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Wed, 22 Aug 2018 10:18:03 +0200 Subject: [PATCH 27/35] Add security mailinglist --- README.md | 8 +++----- docs/index.md | 11 +++++++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f48029408..178c936b6 100644 --- a/README.md +++ b/README.md @@ -164,12 +164,10 @@ Each version is supported until the next one is released (e.g. 1.1.x will be sup We use [Semantic Versioning](http://semver.org/) -## Plumbing +## Mailing lists -- [Oxy](https://github.com/vulcand/oxy): an awesome proxy library made by Mailgun folks -- [Gorilla mux](https://github.com/gorilla/mux): famous request router -- [Negroni](https://github.com/urfave/negroni): web middlewares made simple -- [Lego](https://github.com/xenolf/lego): the best [Let's Encrypt](https://letsencrypt.org) library in go +- General announcements, new releases: mail at news+subscribe@traefik.io or on [the online viewer](https://groups.google.com/a/traefik.io/forum/#!forum/news) +- Security announcements: mail at security+subscribe@traefik.io or on [the online viewer](https://groups.google.com/a/traefik.io/forum/#!forum/security). ## Credits diff --git a/docs/index.md b/docs/index.md index e5c038b59..81c87b082 100644 --- a/docs/index.md +++ b/docs/index.md @@ -206,5 +206,16 @@ docker run -d -p 8080:8080 -p 80:80 -v $PWD/traefik.toml:/etc/traefik/traefik.to ## Security +### Security Advisories + +We strongly advise you to join our mailing list to be aware of the latest announcements from our security team. You can subscribe sending a mail to security+subscribe@traefik.io or on [the online viewer](https://groups.google.com/a/traefik.io/forum/#!forum/security). + +### CVE + +Reported vulnerabilities can be found on +[cve.mitre.org](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=traefik). + +### Report a Vulnerability + We want to keep Træfik safe for everyone. If you've discovered a security vulnerability in Træfik, we appreciate your help in disclosing it to us in a responsible manner, using [this form](https://security.traefik.io). \ No newline at end of file From 6bbac65f7e6ae28fb34805f364b2bd97d5b53859 Mon Sep 17 00:00:00 2001 From: Andrew Bruce Date: Wed, 22 Aug 2018 15:08:03 +0100 Subject: [PATCH 28/35] Include missing key in error message for KV store --- cluster/datastore.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cluster/datastore.go b/cluster/datastore.go index 081c1f5f2..168745e27 100644 --- a/cluster/datastore.go +++ b/cluster/datastore.go @@ -78,7 +78,7 @@ func (d *Datastore) watchChanges() error { stopCh := make(chan struct{}) kvCh, err := d.kv.Watch(d.lockKey, stopCh, nil) if err != nil { - return err + return fmt.Errorf("error while watching key %s: %v", d.lockKey, err) } safe.Go(func() { ctx, cancel := context.WithCancel(d.ctx) From e4a7375d342039650c5a69fc2e5ab2119de3c4f4 Mon Sep 17 00:00:00 2001 From: Daniel Tomcej Date: Thu, 23 Aug 2018 00:52:02 -0600 Subject: [PATCH 29/35] Update kubernetes docs to reflect https options --- docs/configuration/backends/kubernetes.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/configuration/backends/kubernetes.md b/docs/configuration/backends/kubernetes.md index fac33b231..8f1781314 100644 --- a/docs/configuration/backends/kubernetes.md +++ b/docs/configuration/backends/kubernetes.md @@ -127,7 +127,13 @@ This will give more flexibility in cloud/dynamic environments. Traefik automatically requests endpoint information based on the service provided in the ingress spec. Although traefik will connect directly to the endpoints (pods), it still checks the service port to see if TLS communication is required. -If the service port defined in the ingress spec is 443, then the backend communication protocol is assumed to be TLS, and will connect via TLS automatically. + +There are 2 ways to configure Traefik to use https to communicate with backend pods: + +1. If the service port defined in the ingress spec is 443 (note that you can still use `targetPort` to use a different port on your pod). +2. If the service port defined in the ingress spec has a name that starts with `https` (such as `https-api`, `https-web` or just `https`). + +If either of those configuration options exist, then the backend communication protocol is assumed to be TLS, and will connect via TLS automatically. !!! note Please note that by enabling TLS communication between traefik and your pods, you will have to have trusted certificates that have the proper trust chain and IP subject name. From 0861c59bec094e94ed7310f6c37c1990d6795686 Mon Sep 17 00:00:00 2001 From: Ilya Galimyanov Date: Thu, 23 Aug 2018 10:40:03 +0300 Subject: [PATCH 30/35] Remove unnecessary loop --- provider/kubernetes/kubernetes.go | 62 +++++++++++++++---------------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index d57834cc9..54af7caad 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -122,40 +122,36 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s pool.Go(func(stop chan bool) { operation := func() error { - for { - stopWatch := make(chan struct{}, 1) - defer close(stopWatch) - eventsChan, err := k8sClient.WatchAll(p.Namespaces, stopWatch) - if err != nil { - log.Errorf("Error watching kubernetes events: %v", err) - timer := time.NewTimer(1 * time.Second) - select { - case <-timer.C: - return err - case <-stop: - return nil - } + stopWatch := make(chan struct{}, 1) + defer close(stopWatch) + eventsChan, err := k8sClient.WatchAll(p.Namespaces, stopWatch) + if err != nil { + log.Errorf("Error watching kubernetes events: %v", err) + timer := time.NewTimer(1 * time.Second) + select { + case <-timer.C: + return err + case <-stop: + return nil } - for { - select { - case <-stop: - return nil - case event := <-eventsChan: - log.Debugf("Received Kubernetes event kind %T", event) - - templateObjects, err := p.loadIngresses(k8sClient) - if err != nil { - return err - } - - if reflect.DeepEqual(p.lastConfiguration.Get(), templateObjects) { - log.Debugf("Skipping Kubernetes event kind %T", event) - } else { - p.lastConfiguration.Set(templateObjects) - configurationChan <- types.ConfigMessage{ - ProviderName: "kubernetes", - Configuration: p.loadConfig(*templateObjects), - } + } + for { + select { + case <-stop: + return nil + case event := <-eventsChan: + log.Debugf("Received Kubernetes event kind %T", event) + templateObjects, err := p.loadIngresses(k8sClient) + if err != nil { + return err + } + if reflect.DeepEqual(p.lastConfiguration.Get(), templateObjects) { + log.Debugf("Skipping Kubernetes event kind %T", event) + } else { + p.lastConfiguration.Set(templateObjects) + configurationChan <- types.ConfigMessage{ + ProviderName: "kubernetes", + Configuration: p.loadConfig(*templateObjects), } } } From 157c796294a89e9f6d96c1700829e7acd26fd697 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 23 Aug 2018 10:44:03 +0200 Subject: [PATCH 31/35] Rename traefikproxy twitter account into traefik --- README.md | 2 +- docs/index.md | 2 +- mkdocs.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 178c936b6..7d1f3ccbb 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![](https://images.microbadger.com/badges/image/traefik.svg)](https://microbadger.com/images/traefik) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/containous/traefik/blob/master/LICENSE.md) [![Join the chat at https://traefik.herokuapp.com](https://img.shields.io/badge/style-register-green.svg?style=social&label=Slack)](https://traefik.herokuapp.com) -[![Twitter](https://img.shields.io/twitter/follow/traefikproxy.svg?style=social)](https://twitter.com/intent/follow?screen_name=traefikproxy) +[![Twitter](https://img.shields.io/twitter/follow/traefik.svg?style=social)](https://twitter.com/intent/follow?screen_name=traefik) Træfik is a modern HTTP reverse proxy and load balancer that makes deploying microservices easy. diff --git a/docs/index.md b/docs/index.md index 81c87b082..18a99ad8f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,7 +7,7 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/containous/traefik)](https://goreportcard.com/report/github.com/containous/traefik) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/containous/traefik/blob/master/LICENSE.md) [![Join the chat at https://traefik.herokuapp.com](https://img.shields.io/badge/style-register-green.svg?style=social&label=Slack)](https://traefik.herokuapp.com) -[![Twitter](https://img.shields.io/twitter/follow/traefikproxy.svg?style=social)](https://twitter.com/intent/follow?screen_name=traefikproxy) +[![Twitter](https://img.shields.io/twitter/follow/traefik.svg?style=social)](https://twitter.com/intent/follow?screen_name=traefik) Træfik is a modern HTTP reverse proxy and load balancer that makes deploying microservices easy. diff --git a/mkdocs.yml b/mkdocs.yml index f85f2c2fa..dfc88faa7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -42,7 +42,7 @@ google_analytics: # - type: 'slack' # link: 'https://traefik.herokuapp.com' # - type: 'twitter' -# link: 'https://twitter.com/traefikproxy' +# link: 'https://twitter.com/traefik' extra_css: - theme/styles/extra.css From ef753838e744cbe8fa7800dbde05f8a3d1279dd1 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 24 Aug 2018 06:14:03 +0100 Subject: [PATCH 32/35] Fix documentation for route53 acme provider --- docs/configuration/acme.md | 75 +++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/docs/configuration/acme.md b/docs/configuration/acme.md index b7990a765..7cb75f1da 100644 --- a/docs/configuration/acme.md +++ b/docs/configuration/acme.md @@ -251,44 +251,43 @@ Useful if internal networks block external DNS queries. Here is a list of supported `provider`s, that can automate the DNS verification, along with the required environment variables and their [wildcard & root domain support](/configuration/acme/#wildcard-domains) for each. Do not hesitate to complete it. -| Provider Name | Provider Code | Environment Variables | Wildcard & Root Domain Support | -|--------------------------------------------------------|----------------|-----------------------------------------------------------------------------------------------------------------------------|--------------------------------| -| [Auroradns](https://www.pcextreme.com/aurora/dns) | `auroradns` | `AURORA_USER_ID`, `AURORA_KEY`, `AURORA_ENDPOINT` | Not tested yet | -| [Azure](https://azure.microsoft.com/services/dns/) | `azure` | `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_SUBSCRIPTION_ID`, `AZURE_TENANT_ID`, `AZURE_RESOURCE_GROUP` | Not tested yet | -| [Blue Cat](https://www.bluecatnetworks.com/) | `bluecat` | `BLUECAT_SERVER_URL`, `BLUECAT_USER_NAME`, `BLUECAT_PASSWORD`, `BLUECAT_CONFIG_NAME`, `BLUECAT_DNS_VIEW` | Not tested yet | -| [Cloudflare](https://www.cloudflare.com) | `cloudflare` | `CLOUDFLARE_EMAIL`, `CLOUDFLARE_API_KEY` - The `Global API Key` needs to be used, not the `Origin CA Key` | YES | -| [CloudXNS](https://www.cloudxns.net) | `cloudxns` | `CLOUDXNS_API_KEY`, `CLOUDXNS_SECRET_KEY` | Not tested yet | -| [DigitalOcean](https://www.digitalocean.com) | `digitalocean` | `DO_AUTH_TOKEN` | YES | -| [DNSimple](https://dnsimple.com) | `dnsimple` | `DNSIMPLE_OAUTH_TOKEN`, `DNSIMPLE_BASE_URL` | Not tested yet | -| [DNS Made Easy](https://dnsmadeeasy.com) | `dnsmadeeasy` | `DNSMADEEASY_API_KEY`, `DNSMADEEASY_API_SECRET`, `DNSMADEEASY_SANDBOX` | Not tested yet | -| [DNSPod](http://www.dnspod.net/) | `dnspod` | `DNSPOD_API_KEY` | Not tested yet | -| [Duck DNS](https://www.duckdns.org/) | `duckdns` | `DUCKDNS_TOKEN` | Not tested yet | -| [Dyn](https://dyn.com) | `dyn` | `DYN_CUSTOMER_NAME`, `DYN_USER_NAME`, `DYN_PASSWORD` | Not tested yet | -| External Program | `exec` | `EXEC_PATH` | Not tested yet | -| [Exoscale](https://www.exoscale.ch) | `exoscale` | `EXOSCALE_API_KEY`, `EXOSCALE_API_SECRET`, `EXOSCALE_ENDPOINT` | YES | -| [Fast DNS](https://www.akamai.com/) | `fastdns` | `AKAMAI_CLIENT_TOKEN`, `AKAMAI_CLIENT_SECRET`, `AKAMAI_ACCESS_TOKEN` | Not tested yet | -| [Gandi](https://www.gandi.net) | `gandi` | `GANDI_API_KEY` | Not tested yet | -| [Gandi V5](http://doc.livedns.gandi.net) | `gandiv5` | `GANDIV5_API_KEY` | YES | -| [Glesys](https://glesys.com/) | `glesys` | `GLESYS_API_USER`, `GLESYS_API_KEY`, `GLESYS_DOMAIN` | Not tested yet | -| [GoDaddy](https://godaddy.com/domains) | `godaddy` | `GODADDY_API_KEY`, `GODADDY_API_SECRET` | Not tested yet | -| [Google Cloud DNS](https://cloud.google.com/dns/docs/) | `gcloud` | `GCE_PROJECT`, `GCE_SERVICE_ACCOUNT_FILE` | YES | -| [Lightsail](https://aws.amazon.com/lightsail/) | `lightsail` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `DNS_ZONE` | Not tested yet | -| [Linode](https://www.linode.com) | `linode` | `LINODE_API_KEY` | Not tested yet | -| manual | - | none, but you need to run Træfik interactively, turn on `acmeLogging` to see instructions and press Enter. | YES | -| [Namecheap](https://www.namecheap.com) | `namecheap` | `NAMECHEAP_API_USER`, `NAMECHEAP_API_KEY` | YES | -| [name.com](https://www.name.com/) | `namedotcom` | `NAMECOM_USERNAME`, `NAMECOM_API_TOKEN`, `NAMECOM_SERVER` | Not tested yet | -| [NIFCloud](https://cloud.nifty.com/service/dns.htm) | `nifcloud` | `NIFCLOUD_ACCESS_KEY_ID`, `NIFCLOUD_SECRET_ACCESS_KEY` | Not tested yet | -| [Ns1](https://ns1.com/) | `ns1` | `NS1_API_KEY` | Not tested yet | -| [Open Telekom Cloud](https://cloud.telekom.de) | `otc` | `OTC_DOMAIN_NAME`, `OTC_USER_NAME`, `OTC_PASSWORD`, `OTC_PROJECT_NAME`, `OTC_IDENTITY_ENDPOINT` | Not tested yet | -| [OVH](https://www.ovh.com) | `ovh` | `OVH_ENDPOINT`, `OVH_APPLICATION_KEY`, `OVH_APPLICATION_SECRET`, `OVH_CONSUMER_KEY` | YES | -| [PowerDNS](https://www.powerdns.com) | `pdns` | `PDNS_API_KEY`, `PDNS_API_URL` | Not tested yet | -| [Rackspace](https://www.rackspace.com/cloud/dns) | `rackspace` | `RACKSPACE_USER`, `RACKSPACE_API_KEY` | Not tested yet | -| [RFC2136](https://tools.ietf.org/html/rfc2136) | `rfc2136` | `RFC2136_TSIG_KEY`, `RFC2136_TSIG_SECRET`, `RFC2136_TSIG_ALGORITHM`, `RFC2136_NAMESERVER` | Not tested yet | -| [Route 53](https://aws.amazon.com/route53/) | `route53` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`, `AWS_HOSTED_ZONE_ID` or a configured user/instance IAM profile. | YES | -| [Sakura Cloud](https://cloud.sakura.ad.jp/) | `sakuracloud` | `SAKURACLOUD_ACCESS_TOKEN`, `SAKURACLOUD_ACCESS_TOKEN_SECRET` | Not tested yet | -| [VegaDNS](https://github.com/shupp/VegaDNS-API) | `vegadns` | `SECRET_VEGADNS_KEY`, `SECRET_VEGADNS_SECRET`, `VEGADNS_URL` | Not tested yet | -| [VULTR](https://www.vultr.com) | `vultr` | `VULTR_API_KEY` | Not tested yet | - +| Provider Name | Provider Code | Environment Variables | Wildcard & Root Domain Support | +|--------------------------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------|--------------------------------| +| [Auroradns](https://www.pcextreme.com/aurora/dns) | `auroradns` | `AURORA_USER_ID`, `AURORA_KEY`, `AURORA_ENDPOINT` | Not tested yet | +| [Azure](https://azure.microsoft.com/services/dns/) | `azure` | `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_SUBSCRIPTION_ID`, `AZURE_TENANT_ID`, `AZURE_RESOURCE_GROUP` | Not tested yet | +| [Blue Cat](https://www.bluecatnetworks.com/) | `bluecat` | `BLUECAT_SERVER_URL`, `BLUECAT_USER_NAME`, `BLUECAT_PASSWORD`, `BLUECAT_CONFIG_NAME`, `BLUECAT_DNS_VIEW` | Not tested yet | +| [Cloudflare](https://www.cloudflare.com) | `cloudflare` | `CLOUDFLARE_EMAIL`, `CLOUDFLARE_API_KEY` - The `Global API Key` needs to be used, not the `Origin CA Key` | YES | +| [CloudXNS](https://www.cloudxns.net) | `cloudxns` | `CLOUDXNS_API_KEY`, `CLOUDXNS_SECRET_KEY` | Not tested yet | +| [DigitalOcean](https://www.digitalocean.com) | `digitalocean` | `DO_AUTH_TOKEN` | YES | +| [DNSimple](https://dnsimple.com) | `dnsimple` | `DNSIMPLE_OAUTH_TOKEN`, `DNSIMPLE_BASE_URL` | Not tested yet | +| [DNS Made Easy](https://dnsmadeeasy.com) | `dnsmadeeasy` | `DNSMADEEASY_API_KEY`, `DNSMADEEASY_API_SECRET`, `DNSMADEEASY_SANDBOX` | Not tested yet | +| [DNSPod](http://www.dnspod.net/) | `dnspod` | `DNSPOD_API_KEY` | Not tested yet | +| [Duck DNS](https://www.duckdns.org/) | `duckdns` | `DUCKDNS_TOKEN` | Not tested yet | +| [Dyn](https://dyn.com) | `dyn` | `DYN_CUSTOMER_NAME`, `DYN_USER_NAME`, `DYN_PASSWORD` | Not tested yet | +| External Program | `exec` | `EXEC_PATH` | Not tested yet | +| [Exoscale](https://www.exoscale.ch) | `exoscale` | `EXOSCALE_API_KEY`, `EXOSCALE_API_SECRET`, `EXOSCALE_ENDPOINT` | YES | +| [Fast DNS](https://www.akamai.com/) | `fastdns` | `AKAMAI_CLIENT_TOKEN`, `AKAMAI_CLIENT_SECRET`, `AKAMAI_ACCESS_TOKEN` | Not tested yet | +| [Gandi](https://www.gandi.net) | `gandi` | `GANDI_API_KEY` | Not tested yet | +| [Gandi V5](http://doc.livedns.gandi.net) | `gandiv5` | `GANDIV5_API_KEY` | YES | +| [Glesys](https://glesys.com/) | `glesys` | `GLESYS_API_USER`, `GLESYS_API_KEY`, `GLESYS_DOMAIN` | Not tested yet | +| [GoDaddy](https://godaddy.com/domains) | `godaddy` | `GODADDY_API_KEY`, `GODADDY_API_SECRET` | Not tested yet | +| [Google Cloud DNS](https://cloud.google.com/dns/docs/) | `gcloud` | `GCE_PROJECT`, `GCE_SERVICE_ACCOUNT_FILE` | YES | +| [Lightsail](https://aws.amazon.com/lightsail/) | `lightsail` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `DNS_ZONE` | Not tested yet | +| [Linode](https://www.linode.com) | `linode` | `LINODE_API_KEY` | Not tested yet | +| manual | - | none, but you need to run Træfik interactively, turn on `acmeLogging` to see instructions and press Enter. | YES | +| [Namecheap](https://www.namecheap.com) | `namecheap` | `NAMECHEAP_API_USER`, `NAMECHEAP_API_KEY` | YES | +| [name.com](https://www.name.com/) | `namedotcom` | `NAMECOM_USERNAME`, `NAMECOM_API_TOKEN`, `NAMECOM_SERVER` | Not tested yet | +| [NIFCloud](https://cloud.nifty.com/service/dns.htm) | `nifcloud` | `NIFCLOUD_ACCESS_KEY_ID`, `NIFCLOUD_SECRET_ACCESS_KEY` | Not tested yet | +| [Ns1](https://ns1.com/) | `ns1` | `NS1_API_KEY` | Not tested yet | +| [Open Telekom Cloud](https://cloud.telekom.de) | `otc` | `OTC_DOMAIN_NAME`, `OTC_USER_NAME`, `OTC_PASSWORD`, `OTC_PROJECT_NAME`, `OTC_IDENTITY_ENDPOINT` | Not tested yet | +| [OVH](https://www.ovh.com) | `ovh` | `OVH_ENDPOINT`, `OVH_APPLICATION_KEY`, `OVH_APPLICATION_SECRET`, `OVH_CONSUMER_KEY` | YES | +| [PowerDNS](https://www.powerdns.com) | `pdns` | `PDNS_API_KEY`, `PDNS_API_URL` | Not tested yet | +| [Rackspace](https://www.rackspace.com/cloud/dns) | `rackspace` | `RACKSPACE_USER`, `RACKSPACE_API_KEY` | Not tested yet | +| [RFC2136](https://tools.ietf.org/html/rfc2136) | `rfc2136` | `RFC2136_TSIG_KEY`, `RFC2136_TSIG_SECRET`, `RFC2136_TSIG_ALGORITHM`, `RFC2136_NAMESERVER` | Not tested yet | +| [Route 53](https://aws.amazon.com/route53/) | `route53` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `[AWS_REGION]`, `[AWS_HOSTED_ZONE_ID]` or a configured user/instance IAM profile. | YES | +| [Sakura Cloud](https://cloud.sakura.ad.jp/) | `sakuracloud` | `SAKURACLOUD_ACCESS_TOKEN`, `SAKURACLOUD_ACCESS_TOKEN_SECRET` | Not tested yet | +| [VegaDNS](https://github.com/shupp/VegaDNS-API) | `vegadns` | `SECRET_VEGADNS_KEY`, `SECRET_VEGADNS_SECRET`, `VEGADNS_URL` | Not tested yet | +| [VULTR](https://www.vultr.com) | `vultr` | `VULTR_API_KEY` | Not tested yet | ### `domains` From a302731cd1e51d6c3aecf444beda2d7ec950b93e Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 27 Aug 2018 16:32:05 +0200 Subject: [PATCH 33/35] Add segment support for ECS --- autogen/gentemplates/gen.go | 70 +-- docs/configuration/backends/ecs.md | 82 +++ provider/ecs/builder_test.go | 85 +++ provider/ecs/config.go | 115 +++- provider/ecs/config_segment_test.go | 859 ++++++++++++++++++++++++++++ provider/ecs/config_test.go | 702 ++++++++++++----------- provider/ecs/deprecated_config.go | 12 +- provider/ecs/ecs.go | 2 + templates/ecs.tmpl | 70 +-- 9 files changed, 1559 insertions(+), 438 deletions(-) create mode 100644 provider/ecs/builder_test.go create mode 100644 provider/ecs/config_segment_test.go diff --git a/autogen/gentemplates/gen.go b/autogen/gentemplates/gen.go index d07d4440d..b6abce863 100644 --- a/autogen/gentemplates/gen.go +++ b/autogen/gentemplates/gen.go @@ -887,13 +887,13 @@ var _templatesEcsTmpl = []byte(`[backends] {{range $serviceName, $instances := .Services }} {{ $firstInstance := index $instances 0 }} - {{ $circuitBreaker := getCircuitBreaker $firstInstance.TraefikLabels }} + {{ $circuitBreaker := getCircuitBreaker $firstInstance.SegmentLabels }} {{if $circuitBreaker }} [backends."backend-{{ $serviceName }}".circuitBreaker] expression = "{{ $circuitBreaker.Expression }}" {{end}} - {{ $loadBalancer := getLoadBalancer $firstInstance.TraefikLabels }} + {{ $loadBalancer := getLoadBalancer $firstInstance.SegmentLabels }} {{if $loadBalancer }} [backends."backend-{{ $serviceName }}".loadBalancer] method = "{{ $loadBalancer.Method }}" @@ -904,14 +904,14 @@ var _templatesEcsTmpl = []byte(`[backends] {{end}} {{end}} - {{ $maxConn := getMaxConn $firstInstance.TraefikLabels }} + {{ $maxConn := getMaxConn $firstInstance.SegmentLabels }} {{if $maxConn }} [backends."backend-{{ $serviceName }}".maxConn] extractorFunc = "{{ $maxConn.ExtractorFunc }}" amount = {{ $maxConn.Amount }} {{end}} - {{ $healthCheck := getHealthCheck $firstInstance.TraefikLabels }} + {{ $healthCheck := getHealthCheck $firstInstance.SegmentLabels }} {{if $healthCheck }} [backends."backend-{{ $serviceName }}".healthCheck] scheme = "{{ $healthCheck.Scheme }}" @@ -927,7 +927,7 @@ var _templatesEcsTmpl = []byte(`[backends] {{end}} {{end}} - {{ $buffering := getBuffering $firstInstance.TraefikLabels }} + {{ $buffering := getBuffering $firstInstance.SegmentLabels }} {{if $buffering }} [backends."backend-{{ $serviceName }}".buffering] maxRequestBodyBytes = {{ $buffering.MaxRequestBodyBytes }} @@ -949,28 +949,30 @@ var _templatesEcsTmpl = []byte(`[backends] {{range $serviceName, $instances := .Services }} {{range $instance := filterFrontends $instances }} - [frontends."frontend-{{ $serviceName }}"] - backend = "backend-{{ $serviceName }}" - priority = {{ getPriority $instance.TraefikLabels }} - passHostHeader = {{ getPassHostHeader $instance.TraefikLabels }} - passTLSCert = {{ getPassTLSCert $instance.TraefikLabels }} + {{ $frontendName := getFrontendName $instance }} - entryPoints = [{{range getEntryPoints $instance.TraefikLabels }} + [frontends."frontend-{{ $frontendName }}"] + backend = "backend-{{ $serviceName }}" + priority = {{ getPriority $instance.SegmentLabels }} + passHostHeader = {{ getPassHostHeader $instance.SegmentLabels }} + passTLSCert = {{ getPassTLSCert $instance.SegmentLabels }} + + entryPoints = [{{range getEntryPoints $instance.SegmentLabels }} "{{.}}", {{end}}] - {{ $auth := getAuth $instance.TraefikLabels }} + {{ $auth := getAuth $instance.SegmentLabels }} {{if $auth }} - [frontends."frontend-{{ $serviceName }}".auth] + [frontends."frontend-{{ $frontendName }}".auth] headerField = "{{ $auth.HeaderField }}" {{if $auth.Forward }} - [frontends."frontend-{{ $serviceName }}".auth.forward] + [frontends."frontend-{{ $frontendName }}".auth.forward] address = "{{ $auth.Forward.Address }}" trustForwardHeader = {{ $auth.Forward.TrustForwardHeader }} {{if $auth.Forward.TLS }} - [frontends."frontend-{{ $serviceName }}".auth.forward.tls] + [frontends."frontend-{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} cert = """{{ $auth.Forward.TLS.Cert }}""" @@ -980,7 +982,7 @@ var _templatesEcsTmpl = []byte(`[backends] {{end}} {{if $auth.Basic }} - [frontends."frontend-{{ $serviceName }}".auth.basic] + [frontends."frontend-{{ $frontendName }}".auth.basic] removeHeader = {{ $auth.Basic.RemoveHeader }} {{if $auth.Basic.Users }} users = [{{range $auth.Basic.Users }} @@ -991,7 +993,7 @@ var _templatesEcsTmpl = []byte(`[backends] {{end}} {{if $auth.Digest }} - [frontends."frontend-{{ $serviceName }}".auth.digest] + [frontends."frontend-{{ $frontendName }}".auth.digest] removeHeader = {{ $auth.Digest.RemoveHeader }} {{if $auth.Digest.Users }} users = [{{range $auth.Digest.Users }} @@ -1002,29 +1004,29 @@ var _templatesEcsTmpl = []byte(`[backends] {{end}} {{end}} - {{ $whitelist := getWhiteList $instance.TraefikLabels }} + {{ $whitelist := getWhiteList $instance.SegmentLabels }} {{if $whitelist }} - [frontends."frontend-{{ $serviceName }}".whiteList] + [frontends."frontend-{{ $frontendName }}".whiteList] sourceRange = [{{range $whitelist.SourceRange }} "{{.}}", {{end}}] useXForwardedFor = {{ $whitelist.UseXForwardedFor }} {{end}} - {{ $redirect := getRedirect $instance.TraefikLabels }} + {{ $redirect := getRedirect $instance.SegmentLabels }} {{if $redirect }} - [frontends."frontend-{{ $serviceName }}".redirect] + [frontends."frontend-{{ $frontendName }}".redirect] entryPoint = "{{ $redirect.EntryPoint }}" regex = "{{ $redirect.Regex }}" replacement = "{{ $redirect.Replacement }}" permanent = {{ $redirect.Permanent }} {{end}} - {{ $errorPages := getErrorPages $instance.TraefikLabels }} + {{ $errorPages := getErrorPages $instance.SegmentLabels }} {{if $errorPages }} - [frontends."frontend-{{ $serviceName }}".errors] + [frontends."frontend-{{ $frontendName }}".errors] {{range $pageName, $page := $errorPages }} - [frontends."frontend-{{ $serviceName }}".errors."{{ $pageName }}"] + [frontends."frontend-{{ $frontendName }}".errors."{{ $pageName }}"] status = [{{range $page.Status }} "{{.}}", {{end}}] @@ -1033,22 +1035,22 @@ var _templatesEcsTmpl = []byte(`[backends] {{end}} {{end}} - {{ $rateLimit := getRateLimit $instance.TraefikLabels }} + {{ $rateLimit := getRateLimit $instance.SegmentLabels }} {{if $rateLimit }} - [frontends."frontend-{{ $serviceName }}".rateLimit] + [frontends."frontend-{{ $frontendName }}".rateLimit] extractorFunc = "{{ $rateLimit.ExtractorFunc }}" - [frontends."frontend-{{ $serviceName }}".rateLimit.rateSet] + [frontends."frontend-{{ $frontendName }}".rateLimit.rateSet] {{ range $limitName, $limit := $rateLimit.RateSet }} - [frontends."frontend-{{ $serviceName }}".rateLimit.rateSet."{{ $limitName }}"] + [frontends."frontend-{{ $frontendName }}".rateLimit.rateSet."{{ $limitName }}"] period = "{{ $limit.Period }}" average = {{ $limit.Average }} burst = {{ $limit.Burst }} {{end}} {{end}} - {{ $headers := getHeaders $instance.TraefikLabels }} + {{ $headers := getHeaders $instance.SegmentLabels }} {{if $headers }} - [frontends."frontend-{{ $serviceName }}".headers] + [frontends."frontend-{{ $frontendName }}".headers] SSLRedirect = {{ $headers.SSLRedirect }} SSLTemporaryRedirect = {{ $headers.SSLTemporaryRedirect }} SSLHost = "{{ $headers.SSLHost }}" @@ -1080,28 +1082,28 @@ var _templatesEcsTmpl = []byte(`[backends] {{end}} {{if $headers.CustomRequestHeaders }} - [frontends."frontend-{{ $serviceName }}".headers.customRequestHeaders] + [frontends."frontend-{{ $frontendName }}".headers.customRequestHeaders] {{range $k, $v := $headers.CustomRequestHeaders }} {{$k}} = "{{$v}}" {{end}} {{end}} {{if $headers.CustomResponseHeaders }} - [frontends."frontend-{{ $serviceName }}".headers.customResponseHeaders] + [frontends."frontend-{{ $frontendName }}".headers.customResponseHeaders] {{range $k, $v := $headers.CustomResponseHeaders }} {{$k}} = "{{$v}}" {{end}} {{end}} {{if $headers.SSLProxyHeaders }} - [frontends."frontend-{{ $serviceName }}".headers.SSLProxyHeaders] + [frontends."frontend-{{ $frontendName }}".headers.SSLProxyHeaders] {{range $k, $v := $headers.SSLProxyHeaders }} {{$k}} = "{{$v}}" {{end}} {{end}} {{end}} - [frontends."frontend-{{ $serviceName }}".routes."route-frontend-{{ $serviceName }}"] + [frontends."frontend-{{ $frontendName }}".routes."route-frontend-{{ $frontendName }}"] rule = "{{ getFrontendRule $instance }}" {{end}} diff --git a/docs/configuration/backends/ecs.md b/docs/configuration/backends/ecs.md index 51e379c20..b1dbbbe49 100644 --- a/docs/configuration/backends/ecs.md +++ b/docs/configuration/backends/ecs.md @@ -228,3 +228,85 @@ Labels can be used on task containers to override default behaviour: | `traefik.frontend.headers.STSSeconds=315360000` | Sets the max-age of the STS header. | | `traefik.frontend.headers.STSIncludeSubdomains=true` | Adds the `IncludeSubdomains` section of the STS header. | | `traefik.frontend.headers.STSPreload=true` | Adds the preload flag to the STS header. | + +### Containers with Multiple Ports (segment labels) + +Segment labels are used to define routes to an application exposing multiple ports. +A segment is a group of labels that apply to a port exposed by an application. +You can define as many segments as ports exposed in an application. + +Segment labels override the default behavior. + +| Label | Description | +|---------------------------------------------------------------------------|----------------------------------------------------------------| +| `traefik..backend=BACKEND` | Same as `traefik.backend` | +| `traefik..domain=DOMAIN` | Same as `traefik.domain` | +| `traefik..port=PORT` | Same as `traefik.port` | +| `traefik..protocol=http` | Same as `traefik.protocol` | +| `traefik..weight=10` | Same as `traefik.weight` | +| `traefik..frontend.auth.basic=EXPR` | Same as `traefik.frontend.auth.basic` | +| `traefik..frontend.auth.basic.removeHeader=true` | Same as `traefik.frontend.auth.basic.removeHeader` | +| `traefik..frontend.auth.basic.users=EXPR` | Same as `traefik.frontend.auth.basic.users` | +| `traefik..frontend.auth.basic.usersFile=/path/.htpasswd` | Same as `traefik.frontend.auth.basic.usersFile` | +| `traefik..frontend.auth.digest.removeHeader=true` | Same as `traefik.frontend.auth.digest.removeHeader` | +| `traefik..frontend.auth.digest.users=EXPR` | Same as `traefik.frontend.auth.digest.users` | +| `traefik..frontend.auth.digest.usersFile=/path/.htdigest` | Same as `traefik.frontend.auth.digest.usersFile` | +| `traefik..frontend.auth.forward.address=https://example.com`| Same as `traefik.frontend.auth.forward.address` | +| `traefik..frontend.auth.forward.tls.ca=/path/ca.pem` | Same as `traefik.frontend.auth.forward.tls.ca` | +| `traefik..frontend.auth.forward.tls.caOptional=true` | Same as `traefik.frontend.auth.forward.tls.caOptional` | +| `traefik..frontend.auth.forward.tls.cert=/path/server.pem` | Same as `traefik.frontend.auth.forward.tls.cert` | +| `traefik..frontend.auth.forward.tls.insecureSkipVerify=true`| Same as `traefik.frontend.auth.forward.tls.insecureSkipVerify` | +| `traefik..frontend.auth.forward.tls.key=/path/server.key` | Same as `traefik.frontend.auth.forward.tls.key` | +| `traefik..frontend.auth.forward.trustForwardHeader=true` | Same as `traefik.frontend.auth.forward.trustForwardHeader` | +| `traefik..frontend.auth.headerField=X-WebAuth-User` | Same as `traefik.frontend.auth.headerField` | +| `traefik..frontend.auth.removeHeader=true` | Same as `traefik.frontend.auth.removeHeader` | +| `traefik..frontend.entryPoints=https` | Same as `traefik.frontend.entryPoints` | +| `traefik..frontend.errors..backend=NAME` | Same as `traefik.frontend.errors..backend` | +| `traefik..frontend.errors..query=PATH` | Same as `traefik.frontend.errors..query` | +| `traefik..frontend.errors..status=RANGE` | Same as `traefik.frontend.errors..status` | +| `traefik..frontend.passHostHeader=true` | Same as `traefik.frontend.passHostHeader` | +| `traefik..frontend.passTLSCert=true` | Same as `traefik.frontend.passTLSCert` | +| `traefik..frontend.priority=10` | Same as `traefik.frontend.priority` | +| `traefik..frontend.rateLimit.extractorFunc=EXP` | Same as `traefik.frontend.rateLimit.extractorFunc` | +| `traefik..frontend.rateLimit.rateSet..period=6` | Same as `traefik.frontend.rateLimit.rateSet..period` | +| `traefik..frontend.rateLimit.rateSet..average=6` | Same as `traefik.frontend.rateLimit.rateSet..average` | +| `traefik..frontend.rateLimit.rateSet..burst=6` | Same as `traefik.frontend.rateLimit.rateSet..burst` | +| `traefik..frontend.redirect.entryPoint=https` | Same as `traefik.frontend.redirect.entryPoint` | +| `traefik..frontend.redirect.regex=^http://localhost/(.*)` | Same as `traefik.frontend.redirect.regex` | +| `traefik..frontend.redirect.replacement=http://mydomain/$1` | Same as `traefik.frontend.redirect.replacement` | +| `traefik..frontend.redirect.permanent=true` | Same as `traefik.frontend.redirect.permanent` | +| `traefik..frontend.rule=EXP` | Same as `traefik.frontend.rule` | +| `traefik..frontend.whiteList.sourceRange=RANGE` | Same as `traefik.frontend.whiteList.sourceRange` | +| `traefik..frontend.whiteList.useXForwardedFor=true` | Same as `traefik.frontend.whiteList.useXForwardedFor` | + +#### Custom Headers + +| Label | Description | +|----------------------------------------------------------------------|----------------------------------------------------------| +| `traefik..frontend.headers.customRequestHeaders=EXPR ` | Same as `traefik.frontend.headers.customRequestHeaders` | +| `traefik..frontend.headers.customResponseHeaders=EXPR` | Same as `traefik.frontend.headers.customResponseHeaders` | + +#### Security Headers + +| Label | Description | +|-------------------------------------------------------------------------|--------------------------------------------------------------| +| `traefik..frontend.headers.allowedHosts=EXPR` | Same as `traefik.frontend.headers.allowedHosts` | +| `traefik..frontend.headers.browserXSSFilter=true` | Same as `traefik.frontend.headers.browserXSSFilter` | +| `traefik..frontend.headers.contentSecurityPolicy=VALUE` | Same as `traefik.frontend.headers.contentSecurityPolicy` | +| `traefik..frontend.headers.contentTypeNosniff=true` | Same as `traefik.frontend.headers.contentTypeNosniff` | +| `traefik..frontend.headers.customBrowserXSSValue=VALUE` | Same as `traefik.frontend.headers.customBrowserXSSValue` | +| `traefik..frontend.headers.customFrameOptionsValue=VALUE` | Same as `traefik.frontend.headers.customFrameOptionsValue` | +| `traefik..frontend.headers.forceSTSHeader=false` | Same as `traefik.frontend.headers.forceSTSHeader` | +| `traefik..frontend.headers.frameDeny=false` | Same as `traefik.frontend.headers.frameDeny` | +| `traefik..frontend.headers.hostsProxyHeaders=EXPR` | Same as `traefik.frontend.headers.hostsProxyHeaders` | +| `traefik..frontend.headers.isDevelopment=false` | Same as `traefik.frontend.headers.isDevelopment` | +| `traefik..frontend.headers.publicKey=VALUE` | Same as `traefik.frontend.headers.publicKey` | +| `traefik..frontend.headers.referrerPolicy=VALUE` | Same as `traefik.frontend.headers.referrerPolicy` | +| `traefik..frontend.headers.SSLRedirect=true` | Same as `traefik.frontend.headers.SSLRedirect` | +| `traefik..frontend.headers.SSLTemporaryRedirect=true` | Same as `traefik.frontend.headers.SSLTemporaryRedirect` | +| `traefik..frontend.headers.SSLHost=HOST` | Same as `traefik.frontend.headers.SSLHost` | +| `traefik..frontend.headers.SSLForceHost=true` | Same as `traefik.frontend.headers.SSLForceHost` | +| `traefik..frontend.headers.SSLProxyHeaders=EXPR` | Same as `traefik.frontend.headers.SSLProxyHeaders=EXPR` | +| `traefik..frontend.headers.STSSeconds=315360000` | Same as `traefik.frontend.headers.STSSeconds=315360000` | +| `traefik..frontend.headers.STSIncludeSubdomains=true` | Same as `traefik.frontend.headers.STSIncludeSubdomains=true` | +| `traefik..frontend.headers.STSPreload=true` | Same as `traefik.frontend.headers.STSPreload=true` | diff --git a/provider/ecs/builder_test.go b/provider/ecs/builder_test.go new file mode 100644 index 000000000..0cf894855 --- /dev/null +++ b/provider/ecs/builder_test.go @@ -0,0 +1,85 @@ +package ecs + +import ( + "github.com/aws/aws-sdk-go/service/ecs" +) + +func instance(ops ...func(*ecsInstance)) ecsInstance { + e := &ecsInstance{ + containerDefinition: &ecs.ContainerDefinition{}, + } + + for _, op := range ops { + op(e) + } + + return *e +} + +func name(name string) func(*ecsInstance) { + return func(e *ecsInstance) { + e.Name = name + } +} + +func ID(ID string) func(*ecsInstance) { + return func(e *ecsInstance) { + e.ID = ID + } +} + +func iMachine(opts ...func(*machine)) func(*ecsInstance) { + return func(e *ecsInstance) { + e.machine = &machine{} + + for _, opt := range opts { + opt(e.machine) + } + } +} + +func mState(state string) func(*machine) { + return func(m *machine) { + m.state = state + } +} + +func mName(name string) func(*machine) { + return func(m *machine) { + m.name = name + } +} +func mPrivateIP(ip string) func(*machine) { + return func(m *machine) { + m.privateIP = ip + } +} + +func mPorts(opts ...func(*portMapping)) func(*machine) { + return func(m *machine) { + for _, opt := range opts { + p := &portMapping{} + opt(p) + m.ports = append(m.ports, *p) + } + } +} + +func mPort(containerPort int32, hostPort int32) func(*portMapping) { + return func(pm *portMapping) { + pm.containerPort = int64(containerPort) + pm.hostPort = int64(hostPort) + } +} + +func labels(labels map[string]string) func(*ecsInstance) { + return func(c *ecsInstance) { + c.TraefikLabels = labels + } +} + +func dockerLabels(labels map[string]*string) func(*ecsInstance) { + return func(c *ecsInstance) { + c.containerDefinition.DockerLabels = labels + } +} diff --git a/provider/ecs/config.go b/provider/ecs/config.go index 09dd8c2a9..c22ea3f45 100644 --- a/provider/ecs/config.go +++ b/provider/ecs/config.go @@ -1,6 +1,8 @@ package ecs import ( + "crypto/md5" + "encoding/hex" "fmt" "net" "strconv" @@ -17,18 +19,6 @@ import ( // buildConfiguration fills the config template with the given instances func (p *Provider) buildConfigurationV2(instances []ecsInstance) (*types.Configuration, error) { - services := make(map[string][]ecsInstance) - for _, instance := range instances { - backendName := getBackendName(instance) - if p.filterInstance(instance) { - if serviceInstances, ok := services[backendName]; ok { - services[backendName] = append(serviceInstances, instance) - } else { - services[backendName] = []ecsInstance{instance} - } - } - } - var ecsFuncMap = template.FuncMap{ // Backend functions "getHost": getHost, @@ -43,6 +33,7 @@ func (p *Provider) buildConfigurationV2(instances []ecsInstance) (*types.Configu // Frontend functions "filterFrontends": filterFrontends, "getFrontendRule": p.getFrontendRule, + "getFrontendName": p.getFrontendName, "getPassHostHeader": label.GetFuncBool(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeader), "getPassTLSCert": label.GetFuncBool(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), "getPriority": label.GetFuncInt(label.TraefikFrontendPriority, label.DefaultFrontendPriority), @@ -56,6 +47,25 @@ func (p *Provider) buildConfigurationV2(instances []ecsInstance) (*types.Configu "getWhiteList": label.GetWhiteList, } + services := make(map[string][]ecsInstance) + for _, instance := range instances { + segmentProperties := label.ExtractTraefikLabels(instance.TraefikLabels) + + for segmentName, labels := range segmentProperties { + instance.SegmentLabels = labels + instance.SegmentName = segmentName + + backendName := getBackendName(instance) + if p.filterInstance(instance) { + if serviceInstances, ok := services[backendName]; ok { + services[backendName] = append(serviceInstances, instance) + } else { + services[backendName] = []ecsInstance{instance} + } + } + } + } + return p.GetConfiguration("templates/ecs.tmpl", ecsFuncMap, struct { Services map[string][]ecsInstance }{ @@ -101,25 +111,61 @@ func (p *Provider) filterInstance(i ecsInstance) bool { } func getBackendName(i ecsInstance) string { - if value := label.GetStringValue(i.TraefikLabels, label.TraefikBackend, ""); len(value) > 0 { - return value + if len(i.SegmentName) > 0 { + return getSegmentBackendName(i) } - return i.Name + + return getDefaultBackendName(i) +} + +func getSegmentBackendName(i ecsInstance) string { + if value := label.GetStringValue(i.SegmentLabels, label.TraefikBackend, ""); len(value) > 0 { + return provider.Normalize(i.Name + "-" + value) + } + + return provider.Normalize(i.Name + "-" + i.SegmentName) +} + +func getDefaultBackendName(i ecsInstance) string { + if value := label.GetStringValue(i.SegmentLabels, label.TraefikBackend, ""); len(value) != 0 { + return provider.Normalize(value) + } + + return provider.Normalize(i.Name) } func (p *Provider) getFrontendRule(i ecsInstance) string { - domain := label.GetStringValue(i.TraefikLabels, label.TraefikDomain, p.Domain) + if value := label.GetStringValue(i.SegmentLabels, label.TraefikFrontendRule, ""); len(value) != 0 { + return value + } + + domain := label.GetStringValue(i.SegmentLabels, label.TraefikDomain, p.Domain) defaultRule := "Host:" + strings.ToLower(strings.Replace(i.Name, "_", "-", -1)) + "." + domain return label.GetStringValue(i.TraefikLabels, label.TraefikFrontendRule, defaultRule) } +func (p *Provider) getFrontendName(instance ecsInstance) string { + name := getBackendName(instance) + if len(instance.SegmentName) > 0 { + name = instance.SegmentName + "-" + name + } + + return provider.Normalize(name) +} + func getHost(i ecsInstance) string { return i.machine.privateIP } func getPort(i ecsInstance) string { - if value := label.GetStringValue(i.TraefikLabels, label.TraefikPort, ""); len(value) > 0 { + value := label.GetStringValue(i.SegmentLabels, label.TraefikPort, "") + + if len(value) == 0 { + value = label.GetStringValue(i.TraefikLabels, label.TraefikPort, "") + } + + if len(value) > 0 { port, err := strconv.ParseInt(value, 10, 64) if err == nil { for _, mapping := range i.machine.ports { @@ -138,6 +184,11 @@ func filterFrontends(instances []ecsInstance) []ecsInstance { return fun.Filter(func(i ecsInstance) bool { backendName := getBackendName(i) + + if len(i.SegmentName) > 0 { + backendName = backendName + "-" + i.SegmentName + } + _, found := byName[backendName] if !found { byName[backendName] = struct{}{} @@ -154,14 +205,21 @@ func getServers(instances []ecsInstance) map[string]types.Server { servers = make(map[string]types.Server) } - protocol := label.GetStringValue(instance.TraefikLabels, label.TraefikProtocol, label.DefaultProtocol) + protocol := label.GetStringValue(instance.SegmentLabels, label.TraefikProtocol, label.DefaultProtocol) host := getHost(instance) port := getPort(instance) - serverName := provider.Normalize(fmt.Sprintf("server-%s-%s", instance.Name, instance.ID)) + serverURL := fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(host, port)) + serverName := getServerName(instance, serverURL) + + if _, exist := servers[serverName]; exist { + log.Debugf("Skipping server %q with the same URL.", serverName) + continue + } + servers[serverName] = types.Server{ - URL: fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(host, port)), - Weight: label.GetIntValue(instance.TraefikLabels, label.TraefikWeight, label.DefaultWeight), + URL: serverURL, + Weight: label.GetIntValue(instance.SegmentLabels, label.TraefikWeight, label.DefaultWeight), } } @@ -171,3 +229,18 @@ func getServers(instances []ecsInstance) map[string]types.Server { func isEnabled(i ecsInstance, exposedByDefault bool) bool { return label.GetBoolValue(i.TraefikLabels, label.TraefikEnable, exposedByDefault) } + +func getServerName(instance ecsInstance, url string) string { + hash := md5.New() + _, err := hash.Write([]byte(url)) + if err != nil { + // Impossible case + log.Errorf("Fail to hash server URL %q", url) + } + + if len(instance.SegmentName) > 0 { + return provider.Normalize(fmt.Sprintf("server-%s-%s-%s", instance.Name, instance.ID, hex.EncodeToString(hash.Sum(nil)))) + } + + return provider.Normalize(fmt.Sprintf("server-%s-%s", instance.Name, instance.ID)) +} diff --git a/provider/ecs/config_segment_test.go b/provider/ecs/config_segment_test.go new file mode 100644 index 000000000..bd6efa29a --- /dev/null +++ b/provider/ecs/config_segment_test.go @@ -0,0 +1,859 @@ +package ecs + +import ( + "testing" + "time" + + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/containous/flaeg/parse" + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSegmentBuildConfiguration(t *testing.T) { + testCases := []struct { + desc string + instanceInfo []ecsInstance + expectedFrontends map[string]*types.Frontend + expectedBackends map[string]*types.Backend + }{ + { + desc: "when no container", + instanceInfo: []ecsInstance{}, + expectedFrontends: map[string]*types.Frontend{}, + expectedBackends: map[string]*types.Backend{}, + }, + { + desc: "simple configuration", + instanceInfo: []ecsInstance{ + instance( + ID("123456789abc"), + name("foo"), + labels(map[string]string{ + "traefik.sauternes.port": "2503", + "traefik.sauternes.frontend.entryPoints": "http,https", + }), + iMachine( + mName("machine1"), + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(80, 2503), + ), + ), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-sauternes-foo-sauternes": { + Backend: "backend-foo-sauternes", + PassHostHeader: true, + EntryPoints: []string{"http", "https"}, + Routes: map[string]types.Route{ + "route-frontend-sauternes-foo-sauternes": { + Rule: "Host:foo.ecs.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foo-sauternes": { + Servers: map[string]types.Server{ + "server-foo-123456789abc-863563a2e23c95502862016417ee95ea": { + URL: "http://127.0.0.1:2503", + Weight: label.DefaultWeight, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "auth basic", + instanceInfo: []ecsInstance{ + instance( + ID("123456789abc"), + name("foo"), + labels(map[string]string{ + "traefik.sauternes.port": "2503", + "traefik.sauternes.frontend.entryPoints": "http,https", + label.Prefix + "sauternes." + label.SuffixFrontendAuthHeaderField: "X-WebAuth-User", + label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicUsers: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicUsersFile: ".htpasswd", + label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicRemoveHeader: "true", + }), + iMachine( + mName("machine1"), + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(80, 2503), + ), + ), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-sauternes-foo-sauternes": { + Backend: "backend-foo-sauternes", + PassHostHeader: true, + EntryPoints: []string{"http", "https"}, + Routes: map[string]types.Route{ + "route-frontend-sauternes-foo-sauternes": { + Rule: "Host:foo.ecs.localhost", + }, + }, + Auth: &types.Auth{ + HeaderField: "X-WebAuth-User", + Basic: &types.Basic{ + RemoveHeader: true, + Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, + UsersFile: ".htpasswd", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foo-sauternes": { + Servers: map[string]types.Server{ + "server-foo-123456789abc-863563a2e23c95502862016417ee95ea": { + URL: "http://127.0.0.1:2503", + Weight: label.DefaultWeight, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "auth basic backward compatibility", + instanceInfo: []ecsInstance{ + instance( + ID("123456789abc"), + name("foo"), + labels(map[string]string{ + "traefik.sauternes.port": "2503", + "traefik.sauternes.frontend.entryPoints": "http,https", + label.Prefix + "sauternes." + label.SuffixFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + }), + iMachine( + mName("machine1"), + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(80, 2503), + ), + ), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-sauternes-foo-sauternes": { + Backend: "backend-foo-sauternes", + PassHostHeader: true, + EntryPoints: []string{"http", "https"}, + Routes: map[string]types.Route{ + "route-frontend-sauternes-foo-sauternes": { + Rule: "Host:foo.ecs.localhost", + }, + }, + Auth: &types.Auth{ + Basic: &types.Basic{ + Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foo-sauternes": { + Servers: map[string]types.Server{ + "server-foo-123456789abc-863563a2e23c95502862016417ee95ea": { + URL: "http://127.0.0.1:2503", + Weight: label.DefaultWeight, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "auth digest", + instanceInfo: []ecsInstance{ + instance( + ID("123456789abc"), + name("foo"), + labels(map[string]string{ + "traefik.sauternes.port": "2503", + "traefik.sauternes.frontend.entryPoints": "http,https", + label.Prefix + "sauternes." + label.SuffixFrontendAuthHeaderField: "X-WebAuth-User", + label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestUsers: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestUsersFile: ".htpasswd", + label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestRemoveHeader: "true", + }), + iMachine( + mName("machine1"), + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(80, 2503), + ), + ), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-sauternes-foo-sauternes": { + Backend: "backend-foo-sauternes", + PassHostHeader: true, + EntryPoints: []string{"http", "https"}, + Routes: map[string]types.Route{ + "route-frontend-sauternes-foo-sauternes": { + Rule: "Host:foo.ecs.localhost", + }, + }, + Auth: &types.Auth{ + HeaderField: "X-WebAuth-User", + Digest: &types.Digest{ + RemoveHeader: true, + Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, + UsersFile: ".htpasswd", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foo-sauternes": { + Servers: map[string]types.Server{ + "server-foo-123456789abc-863563a2e23c95502862016417ee95ea": { + URL: "http://127.0.0.1:2503", + Weight: label.DefaultWeight, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "auth forward", + instanceInfo: []ecsInstance{ + instance( + ID("123456789abc"), + name("foo"), + labels(map[string]string{ + "traefik.sauternes.port": "2503", + "traefik.sauternes.frontend.entryPoints": "http,https", + label.Prefix + "sauternes." + label.SuffixFrontendAuthHeaderField: "X-WebAuth-User", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardAddress: "auth.server", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTrustForwardHeader: "true", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCa: "ca.crt", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCaOptional: "true", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCert: "server.crt", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSKey: "server.key", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSInsecureSkipVerify: "true", + }), + iMachine( + mName("machine1"), + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(80, 2503), + ), + ), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-sauternes-foo-sauternes": { + Backend: "backend-foo-sauternes", + PassHostHeader: true, + EntryPoints: []string{"http", "https"}, + Routes: map[string]types.Route{ + "route-frontend-sauternes-foo-sauternes": { + Rule: "Host:foo.ecs.localhost", + }, + }, + Auth: &types.Auth{ + HeaderField: "X-WebAuth-User", + Forward: &types.Forward{ + Address: "auth.server", + TrustForwardHeader: true, + TLS: &types.ClientTLS{ + CA: "ca.crt", + CAOptional: true, + Cert: "server.crt", + Key: "server.key", + InsecureSkipVerify: true, + }, + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foo-sauternes": { + Servers: map[string]types.Server{ + "server-foo-123456789abc-863563a2e23c95502862016417ee95ea": { + URL: "http://127.0.0.1:2503", + Weight: label.DefaultWeight, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "when all labels are set", + instanceInfo: []ecsInstance{ + instance( + ID("123456789abc"), + name("foo"), + labels(map[string]string{ + label.Prefix + "sauternes." + label.SuffixPort: "666", + label.Prefix + "sauternes." + label.SuffixProtocol: "https", + label.Prefix + "sauternes." + label.SuffixWeight: "12", + + label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicRemoveHeader: "true", + label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicUsers: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicUsersFile: ".htpasswd", + label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestRemoveHeader: "true", + label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestUsers: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestUsersFile: ".htpasswd", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardAddress: "auth.server", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTrustForwardHeader: "true", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCa: "ca.crt", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCaOptional: "true", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCert: "server.crt", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSKey: "server.key", + label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSInsecureSkipVerify: "true", + label.Prefix + "sauternes." + label.SuffixFrontendAuthHeaderField: "X-WebAuth-User", + + label.Prefix + "sauternes." + label.SuffixFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + label.Prefix + "sauternes." + label.SuffixFrontendEntryPoints: "http,https", + label.Prefix + "sauternes." + label.SuffixFrontendPassHostHeader: "true", + label.Prefix + "sauternes." + label.SuffixFrontendPassTLSCert: "true", + label.Prefix + "sauternes." + label.SuffixFrontendPriority: "666", + label.Prefix + "sauternes." + label.SuffixFrontendRedirectEntryPoint: "https", + label.Prefix + "sauternes." + label.SuffixFrontendRedirectRegex: "nope", + label.Prefix + "sauternes." + label.SuffixFrontendRedirectReplacement: "nope", + label.Prefix + "sauternes." + label.SuffixFrontendRedirectPermanent: "true", + label.Prefix + "sauternes." + label.SuffixFrontendWhiteListSourceRange: "10.10.10.10", + label.Prefix + "sauternes." + label.SuffixFrontendWhiteListUseXForwardedFor: "true", + + label.Prefix + "sauternes." + label.SuffixFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.Prefix + "sauternes." + label.SuffixFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLProxyHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersAllowedHosts: "foo,bar,bor", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersHostsProxyHeaders: "foo,bar,bor", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLHost: "foo", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersCustomFrameOptionsValue: "foo", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersContentSecurityPolicy: "foo", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersPublicKey: "foo", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersReferrerPolicy: "foo", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersCustomBrowserXSSValue: "foo", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSTSSeconds: "666", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLForceHost: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLRedirect: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLTemporaryRedirect: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSTSIncludeSubdomains: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersSTSPreload: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersForceSTSHeader: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersFrameDeny: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersContentTypeNosniff: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersBrowserXSSFilter: "true", + label.Prefix + "sauternes." + label.SuffixFrontendHeadersIsDevelopment: "true", + + label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: "404", + label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: "foobar", + label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: "foo_query", + label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: "500,600", + label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: "foobar", + label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: "bar_query", + + label.Prefix + "sauternes." + label.SuffixFrontendRateLimitExtractorFunc: "client.ip", + label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: "6", + label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: "12", + label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: "18", + label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: "3", + label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: "6", + label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: "9", + }), + iMachine( + mName("machine1"), + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(80, 666), + ), + ), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-sauternes-foo-sauternes": { + Backend: "backend-foo-sauternes", + EntryPoints: []string{ + "http", + "https", + }, + PassHostHeader: true, + PassTLSCert: true, + Priority: 666, + Auth: &types.Auth{ + HeaderField: "X-WebAuth-User", + Basic: &types.Basic{ + RemoveHeader: true, + Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, + UsersFile: ".htpasswd", + }, + }, + WhiteList: &types.WhiteList{ + SourceRange: []string{"10.10.10.10"}, + UseXForwardedFor: true, + }, + Headers: &types.Headers{ + CustomRequestHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + CustomResponseHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + AllowedHosts: []string{ + "foo", + "bar", + "bor", + }, + HostsProxyHeaders: []string{ + "foo", + "bar", + "bor", + }, + SSLRedirect: true, + SSLTemporaryRedirect: true, + SSLForceHost: true, + SSLHost: "foo", + SSLProxyHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + STSSeconds: 666, + STSIncludeSubdomains: true, + STSPreload: true, + ForceSTSHeader: true, + FrameDeny: true, + CustomFrameOptionsValue: "foo", + ContentTypeNosniff: true, + BrowserXSSFilter: true, + CustomBrowserXSSValue: "foo", + ContentSecurityPolicy: "foo", + PublicKey: "foo", + ReferrerPolicy: "foo", + IsDevelopment: true, + }, + Errors: map[string]*types.ErrorPage{ + "foo": { + Status: []string{"404"}, + Query: "foo_query", + Backend: "backend-foobar", + }, + "bar": { + Status: []string{"500", "600"}, + Query: "bar_query", + Backend: "backend-foobar", + }, + }, + RateLimit: &types.RateLimit{ + ExtractorFunc: "client.ip", + RateSet: map[string]*types.Rate{ + "foo": { + Period: parse.Duration(6 * time.Second), + Average: 12, + Burst: 18, + }, + "bar": { + Period: parse.Duration(3 * time.Second), + Average: 6, + Burst: 9, + }, + }, + }, + Redirect: &types.Redirect{ + EntryPoint: "https", + Regex: "", + Replacement: "", + Permanent: true, + }, + + Routes: map[string]types.Route{ + "route-frontend-sauternes-foo-sauternes": { + Rule: "Host:foo.ecs.localhost", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foo-sauternes": { + Servers: map[string]types.Server{ + "server-foo-123456789abc-7f6444e0dff3330c8b0ad2bbbd383b0f": { + URL: "https://127.0.0.1:666", + Weight: 12, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "several containers", + instanceInfo: []ecsInstance{ + instance( + ID("123456789abc"), + name("test1"), + labels(map[string]string{ + "traefik.sauternes.port": "2503", + "traefik.sauternes.protocol": "https", + "traefik.sauternes.weight": "80", + "traefik.sauternes.backend": "foobar", + "traefik.sauternes.frontend.passHostHeader": "false", + "traefik.sauternes.frontend.rule": "Path:/mypath", + "traefik.sauternes.frontend.priority": "5000", + "traefik.sauternes.frontend.entryPoints": "http,https,ws", + "traefik.sauternes.frontend.auth.basic": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + "traefik.sauternes.frontend.redirect.entryPoint": "https", + }), + iMachine( + mName("machine1"), + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(80, 2503), + ), + ), + ), + instance( + ID("abc987654321"), + name("test2"), + labels(map[string]string{ + "traefik.anothersauternes.port": "8079", + "traefik.anothersauternes.weight": "33", + "traefik.anothersauternes.frontend.rule": "Path:/anotherpath", + }), + iMachine( + mName("machine1"), + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.2"), + mPorts( + mPort(80, 8079), + ), + ), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-sauternes-test1-foobar": { + Backend: "backend-test1-foobar", + PassHostHeader: false, + Priority: 5000, + EntryPoints: []string{"http", "https", "ws"}, + Auth: &types.Auth{ + Basic: &types.Basic{ + Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, + }, + }, + Redirect: &types.Redirect{ + EntryPoint: "https", + }, + Routes: map[string]types.Route{ + "route-frontend-sauternes-test1-foobar": { + Rule: "Path:/mypath", + }, + }, + }, + "frontend-anothersauternes-test2-anothersauternes": { + Backend: "backend-test2-anothersauternes", + PassHostHeader: true, + EntryPoints: []string{}, + Routes: map[string]types.Route{ + "route-frontend-anothersauternes-test2-anothersauternes": { + Rule: "Path:/anotherpath", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-test1-foobar": { + Servers: map[string]types.Server{ + "server-test1-123456789abc-79533a101142718f0fdf84c42593c41e": { + URL: "https://127.0.0.1:2503", + Weight: 80, + }, + }, + CircuitBreaker: nil, + }, + "backend-test2-anothersauternes": { + Servers: map[string]types.Server{ + "server-test2-abc987654321-045e3e4aa5a744a325c099b803700a93": { + URL: "http://127.0.0.2:8079", + Weight: 33, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "several segments with the same backend name and same port", + instanceInfo: []ecsInstance{ + instance( + ID("123456789abc"), + name("test1"), + labels(map[string]string{ + "traefik.port": "2503", + "traefik.protocol": "https", + "traefik.weight": "80", + "traefik.frontend.entryPoints": "http,https", + "traefik.frontend.redirect.entryPoint": "https", + + "traefik.sauternes.backend": "foobar", + "traefik.sauternes.frontend.rule": "Path:/sauternes", + "traefik.sauternes.frontend.priority": "5000", + + "traefik.arbois.backend": "foobar", + "traefik.arbois.frontend.rule": "Path:/arbois", + "traefik.arbois.frontend.priority": "3000", + }), + + iMachine( + mName("machine1"), + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(80, 2503), + ), + ), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-sauternes-test1-foobar": { + Backend: "backend-test1-foobar", + PassHostHeader: true, + Priority: 5000, + EntryPoints: []string{"http", "https"}, + Redirect: &types.Redirect{ + EntryPoint: "https", + }, + Routes: map[string]types.Route{ + "route-frontend-sauternes-test1-foobar": { + Rule: "Path:/sauternes", + }, + }, + }, + "frontend-arbois-test1-foobar": { + Backend: "backend-test1-foobar", + PassHostHeader: true, + Priority: 3000, + EntryPoints: []string{"http", "https"}, + Redirect: &types.Redirect{ + EntryPoint: "https", + }, + Routes: map[string]types.Route{ + "route-frontend-arbois-test1-foobar": { + Rule: "Path:/arbois", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-test1-foobar": { + Servers: map[string]types.Server{ + "server-test1-123456789abc-79533a101142718f0fdf84c42593c41e": { + URL: "https://127.0.0.1:2503", + Weight: 80, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "several segments with the same backend name and different port (wrong behavior)", + instanceInfo: []ecsInstance{ + instance( + ID("123456789abc"), + name("test1"), + labels(map[string]string{ + "traefik.protocol": "https", + "traefik.frontend.entryPoints": "http,https", + "traefik.frontend.redirect.entryPoint": "https", + + "traefik.sauternes.port": "2503", + "traefik.sauternes.weight": "80", + "traefik.sauternes.backend": "foobar", + "traefik.sauternes.frontend.rule": "Path:/sauternes", + "traefik.sauternes.frontend.priority": "5000", + + "traefik.arbois.port": "2504", + "traefik.arbois.weight": "90", + "traefik.arbois.backend": "foobar", + "traefik.arbois.frontend.rule": "Path:/arbois", + "traefik.arbois.frontend.priority": "3000", + }), + iMachine( + mName("machine1"), + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(80, 2503), + mPort(80, 2504), + ), + ), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-sauternes-test1-foobar": { + Backend: "backend-test1-foobar", + PassHostHeader: true, + Priority: 5000, + EntryPoints: []string{"http", "https"}, + Redirect: &types.Redirect{ + EntryPoint: "https", + }, + Routes: map[string]types.Route{ + "route-frontend-sauternes-test1-foobar": { + Rule: "Path:/sauternes", + }, + }, + }, + "frontend-arbois-test1-foobar": { + Backend: "backend-test1-foobar", + PassHostHeader: true, + Priority: 3000, + EntryPoints: []string{"http", "https"}, + Redirect: &types.Redirect{ + EntryPoint: "https", + }, + Routes: map[string]types.Route{ + "route-frontend-arbois-test1-foobar": { + Rule: "Path:/arbois", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-test1-foobar": { + Servers: map[string]types.Server{ + "server-test1-123456789abc-79533a101142718f0fdf84c42593c41e": { + URL: "https://127.0.0.1:2503", + Weight: 80, + }, + "server-test1-123456789abc-315a41140f1bd825b066e39686c18482": { + URL: "https://127.0.0.1:2504", + Weight: 90, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + { + desc: "several segments with the same backend name and different port binding", + instanceInfo: []ecsInstance{ + instance( + ID("123456789abc"), + name("test1"), + labels(map[string]string{ + "traefik.protocol": "https", + "traefik.frontend.entryPoints": "http,https", + "traefik.frontend.redirect.entryPoint": "https", + + "traefik.sauternes.port": "2503", + "traefik.sauternes.weight": "80", + "traefik.sauternes.backend": "foobar", + "traefik.sauternes.frontend.rule": "Path:/sauternes", + "traefik.sauternes.frontend.priority": "5000", + + "traefik.arbois.port": "8080", + "traefik.arbois.weight": "90", + "traefik.arbois.backend": "foobar", + "traefik.arbois.frontend.rule": "Path:/arbois", + "traefik.arbois.frontend.priority": "3000", + }), + iMachine( + mName("machine1"), + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(80, 2503), + mPort(8080, 2504), + ), + ), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-sauternes-test1-foobar": { + Backend: "backend-test1-foobar", + PassHostHeader: true, + Priority: 5000, + EntryPoints: []string{"http", "https"}, + Redirect: &types.Redirect{ + EntryPoint: "https", + }, + Routes: map[string]types.Route{ + "route-frontend-sauternes-test1-foobar": { + Rule: "Path:/sauternes", + }, + }, + }, + "frontend-arbois-test1-foobar": { + Backend: "backend-test1-foobar", + PassHostHeader: true, + Priority: 3000, + EntryPoints: []string{"http", "https"}, + Redirect: &types.Redirect{ + EntryPoint: "https", + }, + Routes: map[string]types.Route{ + "route-frontend-arbois-test1-foobar": { + Rule: "Path:/arbois", + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-test1-foobar": { + Servers: map[string]types.Server{ + "server-test1-123456789abc-79533a101142718f0fdf84c42593c41e": { + URL: "https://127.0.0.1:2503", + Weight: 80, + }, + "server-test1-123456789abc-315a41140f1bd825b066e39686c18482": { + URL: "https://127.0.0.1:2504", + Weight: 90, + }, + }, + CircuitBreaker: nil, + }, + }, + }, + } + + provider := &Provider{ + Domain: "ecs.localhost", + ExposedByDefault: true, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actualConfig, err := provider.buildConfiguration(test.instanceInfo) + + assert.NoError(t, err) + require.NotNil(t, actualConfig, "actualConfig") + + assert.EqualValues(t, test.expectedBackends, actualConfig.Backends) + assert.EqualValues(t, test.expectedFrontends, actualConfig.Frontends) + }) + } +} diff --git a/provider/ecs/config_test.go b/provider/ecs/config_test.go index 2e0b620c7..0d9ff28a6 100644 --- a/provider/ecs/config_test.go +++ b/provider/ecs/config_test.go @@ -23,18 +23,18 @@ func TestBuildConfiguration(t *testing.T) { { desc: "config parsed successfully", instances: []ecsInstance{ - { - Name: "instance", - ID: "1", - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{}, - }, - machine: &machine{ - state: ec2.InstanceStateNameRunning, - privateIP: "10.0.0.1", - ports: []portMapping{{hostPort: 1337}}, - }, - }, + instance( + name("instance"), + ID("1"), + dockerLabels(map[string]*string{}), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("10.0.0.1"), + mPorts( + mPort(0, 1337), + ), + ), + ), }, expected: &types.Configuration{ Backends: map[string]*types.Backend{ @@ -63,20 +63,21 @@ func TestBuildConfiguration(t *testing.T) { { desc: "config parsed successfully with health check labels", instances: []ecsInstance{ - { - Name: "instance", - ID: "1", - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikBackendHealthCheckPath: aws.String("/health"), - label.TraefikBackendHealthCheckInterval: aws.String("1s"), - }}, - machine: &machine{ - state: ec2.InstanceStateNameRunning, - privateIP: "10.0.0.1", - ports: []portMapping{{hostPort: 1337}}, - }, - }, + instance( + name("instance"), + ID("1"), + dockerLabels(map[string]*string{ + label.TraefikBackendHealthCheckPath: aws.String("/health"), + label.TraefikBackendHealthCheckInterval: aws.String("1s"), + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("10.0.0.1"), + mPorts( + mPort(0, 1337), + ), + ), + ), }, expected: &types.Configuration{ Backends: map[string]*types.Backend{ @@ -109,22 +110,23 @@ func TestBuildConfiguration(t *testing.T) { { desc: "config parsed successfully with basic auth labels", instances: []ecsInstance{ - { - Name: "instance", - ID: "1", - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikFrontendAuthBasicUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - label.TraefikFrontendAuthBasicUsersFile: aws.String(".htpasswd"), - label.TraefikFrontendAuthBasicRemoveHeader: aws.String("true"), - label.TraefikFrontendAuthHeaderField: aws.String("X-WebAuth-User"), - }}, - machine: &machine{ - state: ec2.InstanceStateNameRunning, - privateIP: "10.0.0.1", - ports: []portMapping{{hostPort: 1337}}, - }, - }, + instance( + name("instance"), + ID("1"), + dockerLabels(map[string]*string{ + label.TraefikFrontendAuthBasicUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), + label.TraefikFrontendAuthBasicUsersFile: aws.String(".htpasswd"), + label.TraefikFrontendAuthBasicRemoveHeader: aws.String("true"), + label.TraefikFrontendAuthHeaderField: aws.String("X-WebAuth-User"), + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("10.0.0.1"), + mPorts( + mPort(0, 1337), + ), + ), + ), }, expected: &types.Configuration{ Backends: map[string]*types.Backend{ @@ -162,19 +164,20 @@ func TestBuildConfiguration(t *testing.T) { { desc: "config parsed successfully with basic auth (backward compatibility) labels", instances: []ecsInstance{ - { - Name: "instance", - ID: "1", - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikFrontendAuthBasic: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - }}, - machine: &machine{ - state: ec2.InstanceStateNameRunning, - privateIP: "10.0.0.1", - ports: []portMapping{{hostPort: 1337}}, - }, - }, + instance( + name("instance"), + ID("1"), + dockerLabels(map[string]*string{ + label.TraefikFrontendAuthBasic: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("10.0.0.1"), + mPorts( + mPort(0, 1337), + ), + ), + ), }, expected: &types.Configuration{ Backends: map[string]*types.Backend{ @@ -209,22 +212,23 @@ func TestBuildConfiguration(t *testing.T) { { desc: "config parsed successfully with digest auth labels", instances: []ecsInstance{ - { - Name: "instance", - ID: "1", - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikFrontendAuthDigestRemoveHeader: aws.String("true"), - label.TraefikFrontendAuthDigestUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - label.TraefikFrontendAuthDigestUsersFile: aws.String(".htpasswd"), - label.TraefikFrontendAuthHeaderField: aws.String("X-WebAuth-User"), - }}, - machine: &machine{ - state: ec2.InstanceStateNameRunning, - privateIP: "10.0.0.1", - ports: []portMapping{{hostPort: 1337}}, - }, - }, + instance( + name("instance"), + ID("1"), + dockerLabels(map[string]*string{ + label.TraefikFrontendAuthDigestRemoveHeader: aws.String("true"), + label.TraefikFrontendAuthDigestUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), + label.TraefikFrontendAuthDigestUsersFile: aws.String(".htpasswd"), + label.TraefikFrontendAuthHeaderField: aws.String("X-WebAuth-User"), + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("10.0.0.1"), + mPorts( + mPort(0, 1337), + ), + ), + ), }, expected: &types.Configuration{ Backends: map[string]*types.Backend{ @@ -262,25 +266,26 @@ func TestBuildConfiguration(t *testing.T) { { desc: "config parsed successfully with forward auth labels", instances: []ecsInstance{ - { - Name: "instance", - ID: "1", - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikFrontendAuthForwardAddress: aws.String("auth.server"), - label.TraefikFrontendAuthForwardTrustForwardHeader: aws.String("true"), - label.TraefikFrontendAuthForwardTLSCa: aws.String("ca.crt"), - label.TraefikFrontendAuthForwardTLSCaOptional: aws.String("true"), - label.TraefikFrontendAuthForwardTLSCert: aws.String("server.crt"), - label.TraefikFrontendAuthForwardTLSKey: aws.String("server.key"), - label.TraefikFrontendAuthForwardTLSInsecureSkipVerify: aws.String("true"), label.TraefikFrontendAuthHeaderField: aws.String("X-WebAuth-User"), - }}, - machine: &machine{ - state: ec2.InstanceStateNameRunning, - privateIP: "10.0.0.1", - ports: []portMapping{{hostPort: 1337}}, - }, - }, + instance( + name("instance"), + ID("1"), + dockerLabels(map[string]*string{ + label.TraefikFrontendAuthForwardAddress: aws.String("auth.server"), + label.TraefikFrontendAuthForwardTrustForwardHeader: aws.String("true"), + label.TraefikFrontendAuthForwardTLSCa: aws.String("ca.crt"), + label.TraefikFrontendAuthForwardTLSCaOptional: aws.String("true"), + label.TraefikFrontendAuthForwardTLSCert: aws.String("server.crt"), + label.TraefikFrontendAuthForwardTLSKey: aws.String("server.key"), + label.TraefikFrontendAuthForwardTLSInsecureSkipVerify: aws.String("true"), label.TraefikFrontendAuthHeaderField: aws.String("X-WebAuth-User"), + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("10.0.0.1"), + mPorts( + mPort(0, 1337), + ), + ), + ), }, expected: &types.Configuration{ Backends: map[string]*types.Backend{ @@ -323,108 +328,109 @@ func TestBuildConfiguration(t *testing.T) { { desc: "when all labels are set", instances: []ecsInstance{ - { - Name: "testing-instance", - ID: "6", - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikPort: aws.String("666"), - label.TraefikProtocol: aws.String("https"), - label.TraefikWeight: aws.String("12"), + instance( + name("testing-instance"), + ID("6"), + dockerLabels(map[string]*string{ + label.TraefikPort: aws.String("666"), + label.TraefikProtocol: aws.String("https"), + label.TraefikWeight: aws.String("12"), - label.TraefikBackend: aws.String("foobar"), + label.TraefikBackend: aws.String("foobar"), - label.TraefikBackendCircuitBreakerExpression: aws.String("NetworkErrorRatio() > 0.5"), - label.TraefikBackendHealthCheckScheme: aws.String("http"), - label.TraefikBackendHealthCheckPath: aws.String("/health"), - label.TraefikBackendHealthCheckPort: aws.String("880"), - label.TraefikBackendHealthCheckInterval: aws.String("6"), - label.TraefikBackendHealthCheckHostname: aws.String("foo.com"), - label.TraefikBackendHealthCheckHeaders: aws.String("Foo:bar || Bar:foo"), - label.TraefikBackendLoadBalancerMethod: aws.String("drr"), - label.TraefikBackendLoadBalancerSticky: aws.String("true"), - label.TraefikBackendLoadBalancerStickiness: aws.String("true"), - label.TraefikBackendLoadBalancerStickinessCookieName: aws.String("chocolate"), - label.TraefikBackendMaxConnAmount: aws.String("666"), - label.TraefikBackendMaxConnExtractorFunc: aws.String("client.ip"), - label.TraefikBackendBufferingMaxResponseBodyBytes: aws.String("10485760"), - label.TraefikBackendBufferingMemResponseBodyBytes: aws.String("2097152"), - label.TraefikBackendBufferingMaxRequestBodyBytes: aws.String("10485760"), - label.TraefikBackendBufferingMemRequestBodyBytes: aws.String("2097152"), - label.TraefikBackendBufferingRetryExpression: aws.String("IsNetworkError() && Attempts() <= 2"), + label.TraefikBackendCircuitBreakerExpression: aws.String("NetworkErrorRatio() > 0.5"), + label.TraefikBackendHealthCheckScheme: aws.String("http"), + label.TraefikBackendHealthCheckPath: aws.String("/health"), + label.TraefikBackendHealthCheckPort: aws.String("880"), + label.TraefikBackendHealthCheckInterval: aws.String("6"), + label.TraefikBackendHealthCheckHostname: aws.String("foo.com"), + label.TraefikBackendHealthCheckHeaders: aws.String("Foo:bar || Bar:foo"), + label.TraefikBackendLoadBalancerMethod: aws.String("drr"), + label.TraefikBackendLoadBalancerSticky: aws.String("true"), + label.TraefikBackendLoadBalancerStickiness: aws.String("true"), + label.TraefikBackendLoadBalancerStickinessCookieName: aws.String("chocolate"), + label.TraefikBackendMaxConnAmount: aws.String("666"), + label.TraefikBackendMaxConnExtractorFunc: aws.String("client.ip"), + label.TraefikBackendBufferingMaxResponseBodyBytes: aws.String("10485760"), + label.TraefikBackendBufferingMemResponseBodyBytes: aws.String("2097152"), + label.TraefikBackendBufferingMaxRequestBodyBytes: aws.String("10485760"), + label.TraefikBackendBufferingMemRequestBodyBytes: aws.String("2097152"), + label.TraefikBackendBufferingRetryExpression: aws.String("IsNetworkError() && Attempts() <= 2"), - label.TraefikFrontendAuthBasic: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - label.TraefikFrontendAuthBasicRemoveHeader: aws.String("true"), - label.TraefikFrontendAuthBasicUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - label.TraefikFrontendAuthBasicUsersFile: aws.String(".htpasswd"), - label.TraefikFrontendAuthDigestRemoveHeader: aws.String("true"), - label.TraefikFrontendAuthDigestUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - label.TraefikFrontendAuthDigestUsersFile: aws.String(".htpasswd"), - label.TraefikFrontendAuthForwardAddress: aws.String("auth.server"), - label.TraefikFrontendAuthForwardTrustForwardHeader: aws.String("true"), - label.TraefikFrontendAuthForwardTLSCa: aws.String("ca.crt"), - label.TraefikFrontendAuthForwardTLSCaOptional: aws.String("true"), - label.TraefikFrontendAuthForwardTLSCert: aws.String("server.crt"), - label.TraefikFrontendAuthForwardTLSKey: aws.String("server.key"), - label.TraefikFrontendAuthForwardTLSInsecureSkipVerify: aws.String("true"), - label.TraefikFrontendAuthHeaderField: aws.String("X-WebAuth-User"), + label.TraefikFrontendAuthBasic: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), + label.TraefikFrontendAuthBasicRemoveHeader: aws.String("true"), + label.TraefikFrontendAuthBasicUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), + label.TraefikFrontendAuthBasicUsersFile: aws.String(".htpasswd"), + label.TraefikFrontendAuthDigestRemoveHeader: aws.String("true"), + label.TraefikFrontendAuthDigestUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), + label.TraefikFrontendAuthDigestUsersFile: aws.String(".htpasswd"), + label.TraefikFrontendAuthForwardAddress: aws.String("auth.server"), + label.TraefikFrontendAuthForwardTrustForwardHeader: aws.String("true"), + label.TraefikFrontendAuthForwardTLSCa: aws.String("ca.crt"), + label.TraefikFrontendAuthForwardTLSCaOptional: aws.String("true"), + label.TraefikFrontendAuthForwardTLSCert: aws.String("server.crt"), + label.TraefikFrontendAuthForwardTLSKey: aws.String("server.key"), + label.TraefikFrontendAuthForwardTLSInsecureSkipVerify: aws.String("true"), + label.TraefikFrontendAuthHeaderField: aws.String("X-WebAuth-User"), - label.TraefikFrontendEntryPoints: aws.String("http,https"), - label.TraefikFrontendPassHostHeader: aws.String("true"), - label.TraefikFrontendPassTLSCert: aws.String("true"), - label.TraefikFrontendPriority: aws.String("666"), - label.TraefikFrontendRedirectEntryPoint: aws.String("https"), - label.TraefikFrontendRedirectRegex: aws.String("nope"), - label.TraefikFrontendRedirectReplacement: aws.String("nope"), - label.TraefikFrontendRedirectPermanent: aws.String("true"), - label.TraefikFrontendRule: aws.String("Host:traefik.io"), - label.TraefikFrontendWhiteListSourceRange: aws.String("10.10.10.10"), - label.TraefikFrontendWhiteListUseXForwardedFor: aws.String("true"), + label.TraefikFrontendEntryPoints: aws.String("http,https"), + label.TraefikFrontendPassHostHeader: aws.String("true"), + label.TraefikFrontendPassTLSCert: aws.String("true"), + label.TraefikFrontendPriority: aws.String("666"), + label.TraefikFrontendRedirectEntryPoint: aws.String("https"), + label.TraefikFrontendRedirectRegex: aws.String("nope"), + label.TraefikFrontendRedirectReplacement: aws.String("nope"), + label.TraefikFrontendRedirectPermanent: aws.String("true"), + label.TraefikFrontendRule: aws.String("Host:traefik.io"), + label.TraefikFrontendWhiteListSourceRange: aws.String("10.10.10.10"), + label.TraefikFrontendWhiteListUseXForwardedFor: aws.String("true"), - label.TraefikFrontendRequestHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - label.TraefikFrontendResponseHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - label.TraefikFrontendSSLProxyHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - label.TraefikFrontendAllowedHosts: aws.String("foo,bar,bor"), - label.TraefikFrontendHostsProxyHeaders: aws.String("foo,bar,bor"), - label.TraefikFrontendSSLHost: aws.String("foo"), - label.TraefikFrontendCustomFrameOptionsValue: aws.String("foo"), - label.TraefikFrontendContentSecurityPolicy: aws.String("foo"), - label.TraefikFrontendPublicKey: aws.String("foo"), - label.TraefikFrontendReferrerPolicy: aws.String("foo"), - label.TraefikFrontendCustomBrowserXSSValue: aws.String("foo"), - label.TraefikFrontendSTSSeconds: aws.String("666"), - label.TraefikFrontendSSLForceHost: aws.String("true"), - label.TraefikFrontendSSLRedirect: aws.String("true"), - label.TraefikFrontendSSLTemporaryRedirect: aws.String("true"), - label.TraefikFrontendSTSIncludeSubdomains: aws.String("true"), - label.TraefikFrontendSTSPreload: aws.String("true"), - label.TraefikFrontendForceSTSHeader: aws.String("true"), - label.TraefikFrontendFrameDeny: aws.String("true"), - label.TraefikFrontendContentTypeNosniff: aws.String("true"), - label.TraefikFrontendBrowserXSSFilter: aws.String("true"), - label.TraefikFrontendIsDevelopment: aws.String("true"), + label.TraefikFrontendRequestHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + label.TraefikFrontendResponseHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + label.TraefikFrontendSSLProxyHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + label.TraefikFrontendAllowedHosts: aws.String("foo,bar,bor"), + label.TraefikFrontendHostsProxyHeaders: aws.String("foo,bar,bor"), + label.TraefikFrontendSSLHost: aws.String("foo"), + label.TraefikFrontendCustomFrameOptionsValue: aws.String("foo"), + label.TraefikFrontendContentSecurityPolicy: aws.String("foo"), + label.TraefikFrontendPublicKey: aws.String("foo"), + label.TraefikFrontendReferrerPolicy: aws.String("foo"), + label.TraefikFrontendCustomBrowserXSSValue: aws.String("foo"), + label.TraefikFrontendSTSSeconds: aws.String("666"), + label.TraefikFrontendSSLForceHost: aws.String("true"), + label.TraefikFrontendSSLRedirect: aws.String("true"), + label.TraefikFrontendSSLTemporaryRedirect: aws.String("true"), + label.TraefikFrontendSTSIncludeSubdomains: aws.String("true"), + label.TraefikFrontendSTSPreload: aws.String("true"), + label.TraefikFrontendForceSTSHeader: aws.String("true"), + label.TraefikFrontendFrameDeny: aws.String("true"), + label.TraefikFrontendContentTypeNosniff: aws.String("true"), + label.TraefikFrontendBrowserXSSFilter: aws.String("true"), + label.TraefikFrontendIsDevelopment: aws.String("true"), - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: aws.String("404"), - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: aws.String("foobar"), - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: aws.String("foo_query"), - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: aws.String("500,600"), - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: aws.String("foobar"), - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: aws.String("bar_query"), + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: aws.String("404"), + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: aws.String("foobar"), + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: aws.String("foo_query"), + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: aws.String("500,600"), + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: aws.String("foobar"), + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: aws.String("bar_query"), - label.TraefikFrontendRateLimitExtractorFunc: aws.String("client.ip"), - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: aws.String("6"), - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: aws.String("12"), - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: aws.String("18"), - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: aws.String("3"), - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: aws.String("6"), - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: aws.String("9"), - }}, - machine: &machine{ - state: ec2.InstanceStateNameRunning, - privateIP: "10.0.0.1", - ports: []portMapping{{hostPort: 1337}}, - }, - }, + label.TraefikFrontendRateLimitExtractorFunc: aws.String("client.ip"), + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: aws.String("6"), + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: aws.String("12"), + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: aws.String("18"), + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: aws.String("3"), + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: aws.String("6"), + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: aws.String("9"), + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("10.0.0.1"), + mPorts( + mPort(0, 1337), + ), + ), + ), }, expected: &types.Configuration{ Backends: map[string]*types.Backend{ @@ -583,180 +589,182 @@ func TestBuildConfiguration(t *testing.T) { { desc: "Containers with same backend name", instances: []ecsInstance{ - { - Name: "testing-instance-v1", - ID: "6", - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikPort: aws.String("666"), - label.TraefikProtocol: aws.String("https"), - label.TraefikWeight: aws.String("12"), + instance( + name("testing-instance-v1"), + ID("6"), + dockerLabels(map[string]*string{ + label.TraefikPort: aws.String("666"), + label.TraefikProtocol: aws.String("https"), + label.TraefikWeight: aws.String("12"), - label.TraefikBackend: aws.String("foobar"), + label.TraefikBackend: aws.String("foobar"), - label.TraefikBackendCircuitBreakerExpression: aws.String("NetworkErrorRatio() > 0.5"), - label.TraefikBackendHealthCheckScheme: aws.String("http"), - label.TraefikBackendHealthCheckPath: aws.String("/health"), - label.TraefikBackendHealthCheckPort: aws.String("880"), - label.TraefikBackendHealthCheckInterval: aws.String("6"), - label.TraefikBackendHealthCheckHostname: aws.String("foo.com"), - label.TraefikBackendHealthCheckHeaders: aws.String("Foo:bar || Bar:foo"), - label.TraefikBackendLoadBalancerMethod: aws.String("drr"), - label.TraefikBackendLoadBalancerSticky: aws.String("true"), - label.TraefikBackendLoadBalancerStickiness: aws.String("true"), - label.TraefikBackendLoadBalancerStickinessCookieName: aws.String("chocolate"), - label.TraefikBackendMaxConnAmount: aws.String("666"), - label.TraefikBackendMaxConnExtractorFunc: aws.String("client.ip"), - label.TraefikBackendBufferingMaxResponseBodyBytes: aws.String("10485760"), - label.TraefikBackendBufferingMemResponseBodyBytes: aws.String("2097152"), - label.TraefikBackendBufferingMaxRequestBodyBytes: aws.String("10485760"), - label.TraefikBackendBufferingMemRequestBodyBytes: aws.String("2097152"), - label.TraefikBackendBufferingRetryExpression: aws.String("IsNetworkError() && Attempts() <= 2"), + label.TraefikBackendCircuitBreakerExpression: aws.String("NetworkErrorRatio() > 0.5"), + label.TraefikBackendHealthCheckScheme: aws.String("http"), + label.TraefikBackendHealthCheckPath: aws.String("/health"), + label.TraefikBackendHealthCheckPort: aws.String("880"), + label.TraefikBackendHealthCheckInterval: aws.String("6"), + label.TraefikBackendHealthCheckHostname: aws.String("foo.com"), + label.TraefikBackendHealthCheckHeaders: aws.String("Foo:bar || Bar:foo"), + label.TraefikBackendLoadBalancerMethod: aws.String("drr"), + label.TraefikBackendLoadBalancerSticky: aws.String("true"), + label.TraefikBackendLoadBalancerStickiness: aws.String("true"), + label.TraefikBackendLoadBalancerStickinessCookieName: aws.String("chocolate"), + label.TraefikBackendMaxConnAmount: aws.String("666"), + label.TraefikBackendMaxConnExtractorFunc: aws.String("client.ip"), + label.TraefikBackendBufferingMaxResponseBodyBytes: aws.String("10485760"), + label.TraefikBackendBufferingMemResponseBodyBytes: aws.String("2097152"), + label.TraefikBackendBufferingMaxRequestBodyBytes: aws.String("10485760"), + label.TraefikBackendBufferingMemRequestBodyBytes: aws.String("2097152"), + label.TraefikBackendBufferingRetryExpression: aws.String("IsNetworkError() && Attempts() <= 2"), - label.TraefikFrontendAuthBasicUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - label.TraefikFrontendEntryPoints: aws.String("http,https"), - label.TraefikFrontendPassHostHeader: aws.String("true"), - label.TraefikFrontendPassTLSCert: aws.String("true"), - label.TraefikFrontendPriority: aws.String("666"), - label.TraefikFrontendRedirectEntryPoint: aws.String("https"), - label.TraefikFrontendRedirectRegex: aws.String("nope"), - label.TraefikFrontendRedirectReplacement: aws.String("nope"), - label.TraefikFrontendRedirectPermanent: aws.String("true"), - label.TraefikFrontendRule: aws.String("Host:traefik.io"), - label.TraefikFrontendWhiteListSourceRange: aws.String("10.10.10.10"), - label.TraefikFrontendWhiteListUseXForwardedFor: aws.String("true"), + label.TraefikFrontendAuthBasicUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), + label.TraefikFrontendEntryPoints: aws.String("http,https"), + label.TraefikFrontendPassHostHeader: aws.String("true"), + label.TraefikFrontendPassTLSCert: aws.String("true"), + label.TraefikFrontendPriority: aws.String("666"), + label.TraefikFrontendRedirectEntryPoint: aws.String("https"), + label.TraefikFrontendRedirectRegex: aws.String("nope"), + label.TraefikFrontendRedirectReplacement: aws.String("nope"), + label.TraefikFrontendRedirectPermanent: aws.String("true"), + label.TraefikFrontendRule: aws.String("Host:traefik.io"), + label.TraefikFrontendWhiteListSourceRange: aws.String("10.10.10.10"), + label.TraefikFrontendWhiteListUseXForwardedFor: aws.String("true"), - label.TraefikFrontendRequestHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - label.TraefikFrontendResponseHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - label.TraefikFrontendSSLProxyHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - label.TraefikFrontendAllowedHosts: aws.String("foo,bar,bor"), - label.TraefikFrontendHostsProxyHeaders: aws.String("foo,bar,bor"), - label.TraefikFrontendSSLHost: aws.String("foo"), - label.TraefikFrontendCustomFrameOptionsValue: aws.String("foo"), - label.TraefikFrontendContentSecurityPolicy: aws.String("foo"), - label.TraefikFrontendPublicKey: aws.String("foo"), - label.TraefikFrontendReferrerPolicy: aws.String("foo"), - label.TraefikFrontendCustomBrowserXSSValue: aws.String("foo"), - label.TraefikFrontendSTSSeconds: aws.String("666"), - label.TraefikFrontendSSLForceHost: aws.String("true"), - label.TraefikFrontendSSLRedirect: aws.String("true"), - label.TraefikFrontendSSLTemporaryRedirect: aws.String("true"), - label.TraefikFrontendSTSIncludeSubdomains: aws.String("true"), - label.TraefikFrontendSTSPreload: aws.String("true"), - label.TraefikFrontendForceSTSHeader: aws.String("true"), - label.TraefikFrontendFrameDeny: aws.String("true"), - label.TraefikFrontendContentTypeNosniff: aws.String("true"), - label.TraefikFrontendBrowserXSSFilter: aws.String("true"), - label.TraefikFrontendIsDevelopment: aws.String("true"), + label.TraefikFrontendRequestHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + label.TraefikFrontendResponseHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + label.TraefikFrontendSSLProxyHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + label.TraefikFrontendAllowedHosts: aws.String("foo,bar,bor"), + label.TraefikFrontendHostsProxyHeaders: aws.String("foo,bar,bor"), + label.TraefikFrontendSSLHost: aws.String("foo"), + label.TraefikFrontendCustomFrameOptionsValue: aws.String("foo"), + label.TraefikFrontendContentSecurityPolicy: aws.String("foo"), + label.TraefikFrontendPublicKey: aws.String("foo"), + label.TraefikFrontendReferrerPolicy: aws.String("foo"), + label.TraefikFrontendCustomBrowserXSSValue: aws.String("foo"), + label.TraefikFrontendSTSSeconds: aws.String("666"), + label.TraefikFrontendSSLForceHost: aws.String("true"), + label.TraefikFrontendSSLRedirect: aws.String("true"), + label.TraefikFrontendSSLTemporaryRedirect: aws.String("true"), + label.TraefikFrontendSTSIncludeSubdomains: aws.String("true"), + label.TraefikFrontendSTSPreload: aws.String("true"), + label.TraefikFrontendForceSTSHeader: aws.String("true"), + label.TraefikFrontendFrameDeny: aws.String("true"), + label.TraefikFrontendContentTypeNosniff: aws.String("true"), + label.TraefikFrontendBrowserXSSFilter: aws.String("true"), + label.TraefikFrontendIsDevelopment: aws.String("true"), - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: aws.String("404"), - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: aws.String("foobar"), - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: aws.String("foo_query"), - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: aws.String("500,600"), - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: aws.String("foobar"), - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: aws.String("bar_query"), + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: aws.String("404"), + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: aws.String("foobar"), + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: aws.String("foo_query"), + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: aws.String("500,600"), + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: aws.String("foobar"), + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: aws.String("bar_query"), - label.TraefikFrontendRateLimitExtractorFunc: aws.String("client.ip"), - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: aws.String("6"), - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: aws.String("12"), - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: aws.String("18"), - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: aws.String("3"), - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: aws.String("6"), - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: aws.String("9"), - }}, - machine: &machine{ - state: ec2.InstanceStateNameRunning, - privateIP: "10.0.0.1", - ports: []portMapping{{hostPort: 1337}}, - }, - }, - { - Name: "testing-instance-v2", - ID: "6", - containerDefinition: &ecs.ContainerDefinition{ - DockerLabels: map[string]*string{ - label.TraefikPort: aws.String("555"), - label.TraefikProtocol: aws.String("https"), - label.TraefikWeight: aws.String("15"), + label.TraefikFrontendRateLimitExtractorFunc: aws.String("client.ip"), + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: aws.String("6"), + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: aws.String("12"), + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: aws.String("18"), + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: aws.String("3"), + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: aws.String("6"), + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: aws.String("9"), + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("10.0.0.1"), + mPorts( + mPort(0, 1337), + ), + ), + ), + instance( + name("testing-instance-v2"), + ID("6"), + dockerLabels(map[string]*string{ + label.TraefikPort: aws.String("555"), + label.TraefikProtocol: aws.String("https"), + label.TraefikWeight: aws.String("15"), - label.TraefikBackend: aws.String("foobar"), + label.TraefikBackend: aws.String("foobar"), - label.TraefikBackendCircuitBreakerExpression: aws.String("NetworkErrorRatio() > 0.5"), - label.TraefikBackendHealthCheckScheme: aws.String("http"), - label.TraefikBackendHealthCheckPath: aws.String("/health"), - label.TraefikBackendHealthCheckPort: aws.String("880"), - label.TraefikBackendHealthCheckInterval: aws.String("6"), - label.TraefikBackendHealthCheckHostname: aws.String("bar.com"), - label.TraefikBackendHealthCheckHeaders: aws.String("Foo:bar || Bar:foo"), - label.TraefikBackendLoadBalancerMethod: aws.String("drr"), - label.TraefikBackendLoadBalancerSticky: aws.String("true"), - label.TraefikBackendLoadBalancerStickiness: aws.String("true"), - label.TraefikBackendLoadBalancerStickinessCookieName: aws.String("chocolate"), - label.TraefikBackendMaxConnAmount: aws.String("666"), - label.TraefikBackendMaxConnExtractorFunc: aws.String("client.ip"), - label.TraefikBackendBufferingMaxResponseBodyBytes: aws.String("10485760"), - label.TraefikBackendBufferingMemResponseBodyBytes: aws.String("2097152"), - label.TraefikBackendBufferingMaxRequestBodyBytes: aws.String("10485760"), - label.TraefikBackendBufferingMemRequestBodyBytes: aws.String("2097152"), - label.TraefikBackendBufferingRetryExpression: aws.String("IsNetworkError() && Attempts() <= 2"), + label.TraefikBackendCircuitBreakerExpression: aws.String("NetworkErrorRatio() > 0.5"), + label.TraefikBackendHealthCheckScheme: aws.String("http"), + label.TraefikBackendHealthCheckPath: aws.String("/health"), + label.TraefikBackendHealthCheckPort: aws.String("880"), + label.TraefikBackendHealthCheckInterval: aws.String("6"), + label.TraefikBackendHealthCheckHostname: aws.String("bar.com"), + label.TraefikBackendHealthCheckHeaders: aws.String("Foo:bar || Bar:foo"), + label.TraefikBackendLoadBalancerMethod: aws.String("drr"), + label.TraefikBackendLoadBalancerSticky: aws.String("true"), + label.TraefikBackendLoadBalancerStickiness: aws.String("true"), + label.TraefikBackendLoadBalancerStickinessCookieName: aws.String("chocolate"), + label.TraefikBackendMaxConnAmount: aws.String("666"), + label.TraefikBackendMaxConnExtractorFunc: aws.String("client.ip"), + label.TraefikBackendBufferingMaxResponseBodyBytes: aws.String("10485760"), + label.TraefikBackendBufferingMemResponseBodyBytes: aws.String("2097152"), + label.TraefikBackendBufferingMaxRequestBodyBytes: aws.String("10485760"), + label.TraefikBackendBufferingMemRequestBodyBytes: aws.String("2097152"), + label.TraefikBackendBufferingRetryExpression: aws.String("IsNetworkError() && Attempts() <= 2"), - label.TraefikFrontendAuthBasic: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), - label.TraefikFrontendEntryPoints: aws.String("http,https"), - label.TraefikFrontendPassHostHeader: aws.String("true"), - label.TraefikFrontendPassTLSCert: aws.String("true"), - label.TraefikFrontendPriority: aws.String("666"), - label.TraefikFrontendRedirectEntryPoint: aws.String("https"), - label.TraefikFrontendRedirectRegex: aws.String("nope"), - label.TraefikFrontendRedirectReplacement: aws.String("nope"), - label.TraefikFrontendRedirectPermanent: aws.String("true"), - label.TraefikFrontendRule: aws.String("Host:traefik.io"), - label.TraefikFrontendWhiteListSourceRange: aws.String("10.10.10.10"), - label.TraefikFrontendWhiteListUseXForwardedFor: aws.String("true"), + label.TraefikFrontendAuthBasic: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), + label.TraefikFrontendEntryPoints: aws.String("http,https"), + label.TraefikFrontendPassHostHeader: aws.String("true"), + label.TraefikFrontendPassTLSCert: aws.String("true"), + label.TraefikFrontendPriority: aws.String("666"), + label.TraefikFrontendRedirectEntryPoint: aws.String("https"), + label.TraefikFrontendRedirectRegex: aws.String("nope"), + label.TraefikFrontendRedirectReplacement: aws.String("nope"), + label.TraefikFrontendRedirectPermanent: aws.String("true"), + label.TraefikFrontendRule: aws.String("Host:traefik.io"), + label.TraefikFrontendWhiteListSourceRange: aws.String("10.10.10.10"), + label.TraefikFrontendWhiteListUseXForwardedFor: aws.String("true"), - label.TraefikFrontendRequestHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - label.TraefikFrontendResponseHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - label.TraefikFrontendSSLProxyHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), - label.TraefikFrontendAllowedHosts: aws.String("foo,bar,bor"), - label.TraefikFrontendHostsProxyHeaders: aws.String("foo,bar,bor"), - label.TraefikFrontendSSLHost: aws.String("foo"), - label.TraefikFrontendCustomFrameOptionsValue: aws.String("foo"), - label.TraefikFrontendContentSecurityPolicy: aws.String("foo"), - label.TraefikFrontendPublicKey: aws.String("foo"), - label.TraefikFrontendReferrerPolicy: aws.String("foo"), - label.TraefikFrontendCustomBrowserXSSValue: aws.String("foo"), - label.TraefikFrontendSTSSeconds: aws.String("666"), - label.TraefikFrontendSSLForceHost: aws.String("true"), - label.TraefikFrontendSSLRedirect: aws.String("true"), - label.TraefikFrontendSSLTemporaryRedirect: aws.String("true"), - label.TraefikFrontendSTSIncludeSubdomains: aws.String("true"), - label.TraefikFrontendSTSPreload: aws.String("true"), - label.TraefikFrontendForceSTSHeader: aws.String("true"), - label.TraefikFrontendFrameDeny: aws.String("true"), - label.TraefikFrontendContentTypeNosniff: aws.String("true"), - label.TraefikFrontendBrowserXSSFilter: aws.String("true"), - label.TraefikFrontendIsDevelopment: aws.String("true"), + label.TraefikFrontendRequestHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + label.TraefikFrontendResponseHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + label.TraefikFrontendSSLProxyHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), + label.TraefikFrontendAllowedHosts: aws.String("foo,bar,bor"), + label.TraefikFrontendHostsProxyHeaders: aws.String("foo,bar,bor"), + label.TraefikFrontendSSLHost: aws.String("foo"), + label.TraefikFrontendCustomFrameOptionsValue: aws.String("foo"), + label.TraefikFrontendContentSecurityPolicy: aws.String("foo"), + label.TraefikFrontendPublicKey: aws.String("foo"), + label.TraefikFrontendReferrerPolicy: aws.String("foo"), + label.TraefikFrontendCustomBrowserXSSValue: aws.String("foo"), + label.TraefikFrontendSTSSeconds: aws.String("666"), + label.TraefikFrontendSSLForceHost: aws.String("true"), + label.TraefikFrontendSSLRedirect: aws.String("true"), + label.TraefikFrontendSSLTemporaryRedirect: aws.String("true"), + label.TraefikFrontendSTSIncludeSubdomains: aws.String("true"), + label.TraefikFrontendSTSPreload: aws.String("true"), + label.TraefikFrontendForceSTSHeader: aws.String("true"), + label.TraefikFrontendFrameDeny: aws.String("true"), + label.TraefikFrontendContentTypeNosniff: aws.String("true"), + label.TraefikFrontendBrowserXSSFilter: aws.String("true"), + label.TraefikFrontendIsDevelopment: aws.String("true"), - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: aws.String("404"), - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: aws.String("foobar"), - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: aws.String("foo_query"), - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: aws.String("500,600"), - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: aws.String("foobar"), - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: aws.String("bar_query"), + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: aws.String("404"), + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: aws.String("foobar"), + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: aws.String("foo_query"), + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: aws.String("500,600"), + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: aws.String("foobar"), + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: aws.String("bar_query"), - label.TraefikFrontendRateLimitExtractorFunc: aws.String("client.ip"), - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: aws.String("6"), - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: aws.String("12"), - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: aws.String("18"), - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: aws.String("3"), - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: aws.String("6"), - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: aws.String("9"), - }}, - machine: &machine{ - state: ec2.InstanceStateNameRunning, - privateIP: "10.2.2.1", - ports: []portMapping{{hostPort: 1337}}, - }, - }, + label.TraefikFrontendRateLimitExtractorFunc: aws.String("client.ip"), + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: aws.String("6"), + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: aws.String("12"), + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: aws.String("18"), + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: aws.String("3"), + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: aws.String("6"), + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: aws.String("9"), + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("10.2.2.1"), + mPorts( + mPort(0, 1337), + ), + ), + ), }, expected: &types.Configuration{ Backends: map[string]*types.Backend{ diff --git a/provider/ecs/deprecated_config.go b/provider/ecs/deprecated_config.go index 06808b7f1..15debca0b 100644 --- a/provider/ecs/deprecated_config.go +++ b/provider/ecs/deprecated_config.go @@ -45,7 +45,7 @@ func (p *Provider) buildConfigurationV1(instances []ecsInstance) (*types.Configu // Frontend functions "filterFrontends": filterFrontendsV1, - "getFrontendRule": p.getFrontendRule, + "getFrontendRule": p.getFrontendRuleV1, "getPassHostHeader": getFuncBoolValueV1(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeader), "getPassTLSCert": getFuncBoolValueV1(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), "getPriority": getFuncIntValueV1(label.TraefikFrontendPriority, label.DefaultFrontendPriority), @@ -70,7 +70,7 @@ func filterFrontendsV1(instances []ecsInstance) []ecsInstance { byName := make(map[string]struct{}) return fun.Filter(func(i ecsInstance) bool { - backendName := getBackendName(i) + backendName := getBackendNameV1(i) _, found := byName[backendName] if !found { byName[backendName] = struct{}{} @@ -79,6 +79,14 @@ func filterFrontendsV1(instances []ecsInstance) []ecsInstance { }, instances).([]ecsInstance) } +// Deprecated +func (p *Provider) getFrontendRuleV1(i ecsInstance) string { + domain := label.GetStringValue(i.TraefikLabels, label.TraefikDomain, p.Domain) + defaultRule := "Host:" + strings.ToLower(strings.Replace(i.Name, "_", "-", -1)) + "." + domain + + return label.GetStringValue(i.TraefikLabels, label.TraefikFrontendRule, defaultRule) +} + // Deprecated func (p *Provider) filterInstanceV1(i ecsInstance) bool { if i.machine == nil { diff --git a/provider/ecs/ecs.go b/provider/ecs/ecs.go index 89959512d..15a90ddc2 100644 --- a/provider/ecs/ecs.go +++ b/provider/ecs/ecs.go @@ -46,6 +46,8 @@ type ecsInstance struct { containerDefinition *ecs.ContainerDefinition machine *machine TraefikLabels map[string]string + SegmentLabels map[string]string + SegmentName string } type portMapping struct { diff --git a/templates/ecs.tmpl b/templates/ecs.tmpl index 7801a2ff1..256e300fb 100644 --- a/templates/ecs.tmpl +++ b/templates/ecs.tmpl @@ -2,13 +2,13 @@ {{range $serviceName, $instances := .Services }} {{ $firstInstance := index $instances 0 }} - {{ $circuitBreaker := getCircuitBreaker $firstInstance.TraefikLabels }} + {{ $circuitBreaker := getCircuitBreaker $firstInstance.SegmentLabels }} {{if $circuitBreaker }} [backends."backend-{{ $serviceName }}".circuitBreaker] expression = "{{ $circuitBreaker.Expression }}" {{end}} - {{ $loadBalancer := getLoadBalancer $firstInstance.TraefikLabels }} + {{ $loadBalancer := getLoadBalancer $firstInstance.SegmentLabels }} {{if $loadBalancer }} [backends."backend-{{ $serviceName }}".loadBalancer] method = "{{ $loadBalancer.Method }}" @@ -19,14 +19,14 @@ {{end}} {{end}} - {{ $maxConn := getMaxConn $firstInstance.TraefikLabels }} + {{ $maxConn := getMaxConn $firstInstance.SegmentLabels }} {{if $maxConn }} [backends."backend-{{ $serviceName }}".maxConn] extractorFunc = "{{ $maxConn.ExtractorFunc }}" amount = {{ $maxConn.Amount }} {{end}} - {{ $healthCheck := getHealthCheck $firstInstance.TraefikLabels }} + {{ $healthCheck := getHealthCheck $firstInstance.SegmentLabels }} {{if $healthCheck }} [backends."backend-{{ $serviceName }}".healthCheck] scheme = "{{ $healthCheck.Scheme }}" @@ -42,7 +42,7 @@ {{end}} {{end}} - {{ $buffering := getBuffering $firstInstance.TraefikLabels }} + {{ $buffering := getBuffering $firstInstance.SegmentLabels }} {{if $buffering }} [backends."backend-{{ $serviceName }}".buffering] maxRequestBodyBytes = {{ $buffering.MaxRequestBodyBytes }} @@ -64,28 +64,30 @@ {{range $serviceName, $instances := .Services }} {{range $instance := filterFrontends $instances }} - [frontends."frontend-{{ $serviceName }}"] - backend = "backend-{{ $serviceName }}" - priority = {{ getPriority $instance.TraefikLabels }} - passHostHeader = {{ getPassHostHeader $instance.TraefikLabels }} - passTLSCert = {{ getPassTLSCert $instance.TraefikLabels }} + {{ $frontendName := getFrontendName $instance }} - entryPoints = [{{range getEntryPoints $instance.TraefikLabels }} + [frontends."frontend-{{ $frontendName }}"] + backend = "backend-{{ $serviceName }}" + priority = {{ getPriority $instance.SegmentLabels }} + passHostHeader = {{ getPassHostHeader $instance.SegmentLabels }} + passTLSCert = {{ getPassTLSCert $instance.SegmentLabels }} + + entryPoints = [{{range getEntryPoints $instance.SegmentLabels }} "{{.}}", {{end}}] - {{ $auth := getAuth $instance.TraefikLabels }} + {{ $auth := getAuth $instance.SegmentLabels }} {{if $auth }} - [frontends."frontend-{{ $serviceName }}".auth] + [frontends."frontend-{{ $frontendName }}".auth] headerField = "{{ $auth.HeaderField }}" {{if $auth.Forward }} - [frontends."frontend-{{ $serviceName }}".auth.forward] + [frontends."frontend-{{ $frontendName }}".auth.forward] address = "{{ $auth.Forward.Address }}" trustForwardHeader = {{ $auth.Forward.TrustForwardHeader }} {{if $auth.Forward.TLS }} - [frontends."frontend-{{ $serviceName }}".auth.forward.tls] + [frontends."frontend-{{ $frontendName }}".auth.forward.tls] ca = "{{ $auth.Forward.TLS.CA }}" caOptional = {{ $auth.Forward.TLS.CAOptional }} cert = """{{ $auth.Forward.TLS.Cert }}""" @@ -95,7 +97,7 @@ {{end}} {{if $auth.Basic }} - [frontends."frontend-{{ $serviceName }}".auth.basic] + [frontends."frontend-{{ $frontendName }}".auth.basic] removeHeader = {{ $auth.Basic.RemoveHeader }} {{if $auth.Basic.Users }} users = [{{range $auth.Basic.Users }} @@ -106,7 +108,7 @@ {{end}} {{if $auth.Digest }} - [frontends."frontend-{{ $serviceName }}".auth.digest] + [frontends."frontend-{{ $frontendName }}".auth.digest] removeHeader = {{ $auth.Digest.RemoveHeader }} {{if $auth.Digest.Users }} users = [{{range $auth.Digest.Users }} @@ -117,29 +119,29 @@ {{end}} {{end}} - {{ $whitelist := getWhiteList $instance.TraefikLabels }} + {{ $whitelist := getWhiteList $instance.SegmentLabels }} {{if $whitelist }} - [frontends."frontend-{{ $serviceName }}".whiteList] + [frontends."frontend-{{ $frontendName }}".whiteList] sourceRange = [{{range $whitelist.SourceRange }} "{{.}}", {{end}}] useXForwardedFor = {{ $whitelist.UseXForwardedFor }} {{end}} - {{ $redirect := getRedirect $instance.TraefikLabels }} + {{ $redirect := getRedirect $instance.SegmentLabels }} {{if $redirect }} - [frontends."frontend-{{ $serviceName }}".redirect] + [frontends."frontend-{{ $frontendName }}".redirect] entryPoint = "{{ $redirect.EntryPoint }}" regex = "{{ $redirect.Regex }}" replacement = "{{ $redirect.Replacement }}" permanent = {{ $redirect.Permanent }} {{end}} - {{ $errorPages := getErrorPages $instance.TraefikLabels }} + {{ $errorPages := getErrorPages $instance.SegmentLabels }} {{if $errorPages }} - [frontends."frontend-{{ $serviceName }}".errors] + [frontends."frontend-{{ $frontendName }}".errors] {{range $pageName, $page := $errorPages }} - [frontends."frontend-{{ $serviceName }}".errors."{{ $pageName }}"] + [frontends."frontend-{{ $frontendName }}".errors."{{ $pageName }}"] status = [{{range $page.Status }} "{{.}}", {{end}}] @@ -148,22 +150,22 @@ {{end}} {{end}} - {{ $rateLimit := getRateLimit $instance.TraefikLabels }} + {{ $rateLimit := getRateLimit $instance.SegmentLabels }} {{if $rateLimit }} - [frontends."frontend-{{ $serviceName }}".rateLimit] + [frontends."frontend-{{ $frontendName }}".rateLimit] extractorFunc = "{{ $rateLimit.ExtractorFunc }}" - [frontends."frontend-{{ $serviceName }}".rateLimit.rateSet] + [frontends."frontend-{{ $frontendName }}".rateLimit.rateSet] {{ range $limitName, $limit := $rateLimit.RateSet }} - [frontends."frontend-{{ $serviceName }}".rateLimit.rateSet."{{ $limitName }}"] + [frontends."frontend-{{ $frontendName }}".rateLimit.rateSet."{{ $limitName }}"] period = "{{ $limit.Period }}" average = {{ $limit.Average }} burst = {{ $limit.Burst }} {{end}} {{end}} - {{ $headers := getHeaders $instance.TraefikLabels }} + {{ $headers := getHeaders $instance.SegmentLabels }} {{if $headers }} - [frontends."frontend-{{ $serviceName }}".headers] + [frontends."frontend-{{ $frontendName }}".headers] SSLRedirect = {{ $headers.SSLRedirect }} SSLTemporaryRedirect = {{ $headers.SSLTemporaryRedirect }} SSLHost = "{{ $headers.SSLHost }}" @@ -195,28 +197,28 @@ {{end}} {{if $headers.CustomRequestHeaders }} - [frontends."frontend-{{ $serviceName }}".headers.customRequestHeaders] + [frontends."frontend-{{ $frontendName }}".headers.customRequestHeaders] {{range $k, $v := $headers.CustomRequestHeaders }} {{$k}} = "{{$v}}" {{end}} {{end}} {{if $headers.CustomResponseHeaders }} - [frontends."frontend-{{ $serviceName }}".headers.customResponseHeaders] + [frontends."frontend-{{ $frontendName }}".headers.customResponseHeaders] {{range $k, $v := $headers.CustomResponseHeaders }} {{$k}} = "{{$v}}" {{end}} {{end}} {{if $headers.SSLProxyHeaders }} - [frontends."frontend-{{ $serviceName }}".headers.SSLProxyHeaders] + [frontends."frontend-{{ $frontendName }}".headers.SSLProxyHeaders] {{range $k, $v := $headers.SSLProxyHeaders }} {{$k}} = "{{$v}}" {{end}} {{end}} {{end}} - [frontends."frontend-{{ $serviceName }}".routes."route-frontend-{{ $serviceName }}"] + [frontends."frontend-{{ $frontendName }}".routes."route-frontend-{{ $frontendName }}"] rule = "{{ getFrontendRule $instance }}" {{end}} From f586950528479319a5b03feaa4e591b263ea5151 Mon Sep 17 00:00:00 2001 From: Wim Fournier Date: Mon, 27 Aug 2018 17:00:05 +0200 Subject: [PATCH 34/35] multiple frontends for consulcatalog --- docs/configuration/backends/consulcatalog.md | 11 +++ integration/consul_catalog_test.go | 46 ++++++++++++ provider/consulcatalog/config.go | 5 +- provider/consulcatalog/config_test.go | 74 ++++++++++++++++++++ provider/consulcatalog/consul_catalog.go | 61 +++++++++++++++- 5 files changed, 193 insertions(+), 4 deletions(-) diff --git a/docs/configuration/backends/consulcatalog.md b/docs/configuration/backends/consulcatalog.md index be3a6b9ea..4eb124a54 100644 --- a/docs/configuration/backends/consulcatalog.md +++ b/docs/configuration/backends/consulcatalog.md @@ -151,6 +151,17 @@ Additional settings can be defined using Consul Catalog tags. | `.frontend.whiteList.sourceRange=RANGE` | Sets a list of IP-Ranges which are allowed to access.
An unset or empty list allows all Source-IPs to access. If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. | | `.frontend.whiteList.useXForwardedFor=true` | Uses `X-Forwarded-For` header as valid source of IP for the white list. | +### Multiple frontends for a single service + +If you need to support multiple frontends for a service, for example when having multiple `rules` that can't be combined, specify them as follows: + +``` +.frontends.A.rule=Host:A:PathPrefix:/A +.frontends.B.rule=Host:B:PathPrefix:/ +``` + +`A` and `B` here are just arbitrary names, they can be anything. You can use any setting that applies to `.frontend` from the table above. + ### Custom Headers !!! note diff --git a/integration/consul_catalog_test.go b/integration/consul_catalog_test.go index 7607b41d8..548677e04 100644 --- a/integration/consul_catalog_test.go +++ b/integration/consul_catalog_test.go @@ -674,3 +674,49 @@ func (s *ConsulCatalogSuite) TestMaintenanceMode(c *check.C) { err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) c.Assert(err, checker.IsNil) } + +func (s *ConsulCatalogSuite) TestMultipleFrontendRule(c *check.C) { + cmd, display := s.traefikCmd( + withConfigFile("fixtures/consul_catalog/simple.toml"), + "--consulCatalog", + "--consulCatalog.endpoint="+s.consulIP+":8500", + "--consulCatalog.domain=consul.localhost") + defer display(c) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + // Wait for Traefik to turn ready. + err = try.GetRequest("http://127.0.0.1:8000/", 2*time.Second, try.StatusCodeIs(http.StatusNotFound)) + c.Assert(err, checker.IsNil) + + whoami := s.composeProject.Container(c, "whoami1") + + err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, + []string{ + "traefik.frontends.service1.rule=Host:whoami1.consul.localhost", + "traefik.frontends.service2.rule=Host:whoami2.consul.localhost", + }) + 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, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) + c.Assert(err, checker.IsNil) + + req, err = http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) + req.Host = "whoami1.consul.localhost" + + err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) + c.Assert(err, checker.IsNil) + + req, err = http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) + req.Host = "whoami2.consul.localhost" + + err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) + c.Assert(err, checker.IsNil) +} diff --git a/provider/consulcatalog/config.go b/provider/consulcatalog/config.go index 73d8c37e1..94020e40f 100644 --- a/provider/consulcatalog/config.go +++ b/provider/consulcatalog/config.go @@ -55,7 +55,7 @@ func (p *Provider) buildConfigurationV2(catalog []catalogUpdate) *types.Configur var services []*serviceUpdate for _, info := range catalog { if len(info.Nodes) > 0 { - services = append(services, info.Service) + services = append(services, p.generateFrontends(info.Service)...) allNodes = append(allNodes, info.Nodes...) } } @@ -161,6 +161,9 @@ func getCircuitBreaker(labels map[string]string) *types.CircuitBreaker { } func getServiceBackendName(service *serviceUpdate) string { + if service.ParentServiceName != "" { + return strings.ToLower(service.ParentServiceName) + } return strings.ToLower(service.ServiceName) } diff --git a/provider/consulcatalog/config_test.go b/provider/consulcatalog/config_test.go index 332a14dee..11d7f440c 100644 --- a/provider/consulcatalog/config_test.go +++ b/provider/consulcatalog/config_test.go @@ -120,6 +120,80 @@ func TestProviderBuildConfiguration(t *testing.T) { }, }, }, + { + desc: "Should build config which contains three frontends and one backend", + nodes: []catalogUpdate{ + { + Service: &serviceUpdate{ + ServiceName: "test", + Attributes: []string{ + "random.foo=bar", + label.Prefix + "frontend.rule=Host:A", + label.Prefix + "frontends.test1.rule=Host:B", + label.Prefix + "frontends.test2.rule=Host:C", + }, + }, + Nodes: []*api.ServiceEntry{ + { + Service: &api.AgentService{ + Service: "test", + Address: "127.0.0.1", + Port: 80, + Tags: []string{ + "random.foo=bar", + }, + }, + Node: &api.Node{ + Node: "localhost", + Address: "127.0.0.1", + }, + }, + }, + }, + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-test": { + Backend: "backend-test", + PassHostHeader: true, + Routes: map[string]types.Route{ + "route-host-test": { + Rule: "Host:A", + }, + }, + EntryPoints: []string{}, + }, + "frontend-test-test1": { + Backend: "backend-test", + PassHostHeader: true, + Routes: map[string]types.Route{ + "route-host-test-test1": { + Rule: "Host:B", + }, + }, + EntryPoints: []string{}, + }, + "frontend-test-test2": { + Backend: "backend-test", + PassHostHeader: true, + Routes: map[string]types.Route{ + "route-host-test-test2": { + Rule: "Host:C", + }, + }, + EntryPoints: []string{}, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-test": { + Servers: map[string]types.Server{ + "test-0-O0Tnh-SwzY69M6SurTKP3wNKkzI": { + URL: "http://127.0.0.1:80", + Weight: 1, + }, + }, + }, + }, + }, { desc: "Should build config with a basic auth with a backward compatibility", nodes: []catalogUpdate{ diff --git a/provider/consulcatalog/consul_catalog.go b/provider/consulcatalog/consul_catalog.go index 312fd6da5..297096206 100644 --- a/provider/consulcatalog/consul_catalog.go +++ b/provider/consulcatalog/consul_catalog.go @@ -50,9 +50,15 @@ type Service struct { } type serviceUpdate struct { - ServiceName string - Attributes []string - TraefikLabels map[string]string + ServiceName string + ParentServiceName string + Attributes []string + TraefikLabels map[string]string +} + +type frontendSegment struct { + Name string + Labels map[string]string } type catalogUpdate struct { @@ -560,3 +566,52 @@ func (p *Provider) getConstraintTags(tags []string) []string { return values } + +func (p *Provider) generateFrontends(service *serviceUpdate) []*serviceUpdate { + frontends := make([]*serviceUpdate, 0) + // to support .frontend.xxx + frontends = append(frontends, &serviceUpdate{ + ServiceName: service.ServiceName, + ParentServiceName: service.ServiceName, + Attributes: service.Attributes, + TraefikLabels: service.TraefikLabels, + }) + + // loop over children of .frontends.* + for _, frontend := range getSegments(p.Prefix+".frontends", p.Prefix, service.TraefikLabels) { + frontends = append(frontends, &serviceUpdate{ + ServiceName: service.ServiceName + "-" + frontend.Name, + ParentServiceName: service.ServiceName, + Attributes: service.Attributes, + TraefikLabels: frontend.Labels, + }) + } + + return frontends +} +func getSegments(path string, prefix string, tree map[string]string) []*frontendSegment { + segments := make([]*frontendSegment, 0) + // find segment names + segmentNames := make(map[string]bool) + for key := range tree { + if strings.HasPrefix(key, path+".") { + segmentNames[strings.SplitN(strings.TrimPrefix(key, path+"."), ".", 2)[0]] = true + } + } + + // get labels for each segment found + for segment := range segmentNames { + labels := make(map[string]string) + for key, value := range tree { + if strings.HasPrefix(key, path+"."+segment) { + labels[prefix+".frontend"+strings.TrimPrefix(key, path+"."+segment)] = value + } + } + segments = append(segments, &frontendSegment{ + Name: segment, + Labels: labels, + }) + } + + return segments +} From 56488d435f6c513b7f904407d448db47da5b0b0e Mon Sep 17 00:00:00 2001 From: SALLEYRON Julien Date: Mon, 27 Aug 2018 18:10:03 +0200 Subject: [PATCH 35/35] Handle Te header when http2 --- Gopkg.lock | 4 +- vendor/github.com/gorilla/websocket/client.go | 103 +++++++++++++---- vendor/github.com/gorilla/websocket/conn.go | 104 ++++++++++-------- .../github.com/gorilla/websocket/conn_read.go | 18 --- .../gorilla/websocket/conn_read_legacy.go | 21 ---- .../github.com/gorilla/websocket/prepared.go | 1 - vendor/github.com/gorilla/websocket/proxy.go | 2 +- vendor/github.com/gorilla/websocket/server.go | 97 +++++++++++++--- vendor/github.com/gorilla/websocket/trace.go | 19 ++++ .../github.com/gorilla/websocket/trace_17.go | 12 ++ vendor/github.com/gorilla/websocket/util.go | 2 +- vendor/github.com/vulcand/oxy/forward/fwd.go | 4 +- .../vulcand/oxy/forward/post_config.go | 5 + .../vulcand/oxy/forward/post_config_18.go | 42 +++++++ .../github.com/vulcand/oxy/forward/rewrite.go | 6 - 15 files changed, 306 insertions(+), 134 deletions(-) delete mode 100644 vendor/github.com/gorilla/websocket/conn_read.go delete mode 100644 vendor/github.com/gorilla/websocket/conn_read_legacy.go create mode 100644 vendor/github.com/gorilla/websocket/trace.go create mode 100644 vendor/github.com/gorilla/websocket/trace_17.go create mode 100644 vendor/github.com/vulcand/oxy/forward/post_config.go create mode 100644 vendor/github.com/vulcand/oxy/forward/post_config_18.go diff --git a/Gopkg.lock b/Gopkg.lock index 8c7af184a..358d7c263 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -706,7 +706,7 @@ branch = "master" name = "github.com/gorilla/websocket" packages = ["."] - revision = "eb925808374e5ca90c83401a40d711dc08c0c0f6" + revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d" [[projects]] name = "github.com/gravitational/trace" @@ -1272,7 +1272,7 @@ "roundrobin", "utils" ] - revision = "885e42fe04d8e0efa6c18facad4e0fc5757cde9b" + revision = "f6bbeac6d5c4c06f88ba07ed42983ff36a5b407e" [[projects]] name = "github.com/vulcand/predicate" diff --git a/vendor/github.com/gorilla/websocket/client.go b/vendor/github.com/gorilla/websocket/client.go index 8e90de27f..2e32fd506 100644 --- a/vendor/github.com/gorilla/websocket/client.go +++ b/vendor/github.com/gorilla/websocket/client.go @@ -6,12 +6,14 @@ package websocket import ( "bytes" + "context" "crypto/tls" "errors" "io" "io/ioutil" "net" "net/http" + "net/http/httptrace" "net/url" "strings" "time" @@ -51,6 +53,10 @@ type Dialer struct { // NetDial is nil, net.Dial is used. NetDial func(network, addr string) (net.Conn, error) + // NetDialContext specifies the dial function for creating TCP connections. If + // NetDialContext is nil, net.DialContext is used. + NetDialContext func(ctx context.Context, network, addr string) (net.Conn, error) + // Proxy specifies a function to return a proxy for a given // Request. If the function returns a non-nil error, the // request is aborted with the provided error. @@ -69,6 +75,17 @@ type Dialer struct { // do not limit the size of the messages that can be sent or received. ReadBufferSize, WriteBufferSize int + // WriteBufferPool is a pool of buffers for write operations. If the value + // is not set, then write buffers are allocated to the connection for the + // lifetime of the connection. + // + // A pool is most useful when the application has a modest volume of writes + // across a large number of connections. + // + // Applications should use a single pool for each unique value of + // WriteBufferSize. + WriteBufferPool BufferPool + // Subprotocols specifies the client's requested subprotocols. Subprotocols []string @@ -84,6 +101,11 @@ type Dialer struct { Jar http.CookieJar } +// Dial creates a new client connection by calling DialContext with a background context. +func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { + return d.DialContext(context.Background(), urlStr, requestHeader) +} + var errMalformedURL = errors.New("malformed ws or wss URL") func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) { @@ -111,19 +133,20 @@ var DefaultDialer = &Dialer{ } // nilDialer is dialer to use when receiver is nil. -var nilDialer Dialer = *DefaultDialer +var nilDialer = *DefaultDialer -// Dial creates a new client connection. Use requestHeader to specify the +// DialContext creates a new client connection. Use requestHeader to specify the // origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie). // Use the response.Header to get the selected subprotocol // (Sec-WebSocket-Protocol) and cookies (Set-Cookie). // +// The context will be used in the request and in the Dialer +// // If the WebSocket handshake fails, ErrBadHandshake is returned along with a // non-nil *http.Response so that callers can handle redirects, authentication, // etcetera. The response body may not contain the entire response and does not // need to be closed by the application. -func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { - +func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { if d == nil { d = &nilDialer } @@ -161,6 +184,7 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re Header: make(http.Header), Host: u.Host, } + req = req.WithContext(ctx) // Set the cookies present in the cookie jar of the dialer if d.Jar != nil { @@ -201,23 +225,33 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re } if d.EnableCompression { - req.Header.Set("Sec-Websocket-Extensions", "permessage-deflate; server_no_context_takeover; client_no_context_takeover") + req.Header["Sec-WebSocket-Extensions"] = []string{"permessage-deflate; server_no_context_takeover; client_no_context_takeover"} } - var deadline time.Time if d.HandshakeTimeout != 0 { - deadline = time.Now().Add(d.HandshakeTimeout) + var cancel func() + ctx, cancel = context.WithTimeout(ctx, d.HandshakeTimeout) + defer cancel() } // Get network dial function. - netDial := d.NetDial - if netDial == nil { - netDialer := &net.Dialer{Deadline: deadline} - netDial = netDialer.Dial + var netDial func(network, add string) (net.Conn, error) + + if d.NetDialContext != nil { + netDial = func(network, addr string) (net.Conn, error) { + return d.NetDialContext(ctx, network, addr) + } + } else if d.NetDial != nil { + netDial = d.NetDial + } else { + netDialer := &net.Dialer{} + netDial = func(network, addr string) (net.Conn, error) { + return netDialer.DialContext(ctx, network, addr) + } } // If needed, wrap the dial function to set the connection deadline. - if !deadline.Equal(time.Time{}) { + if deadline, ok := ctx.Deadline(); ok { forwardDial := netDial netDial = func(network, addr string) (net.Conn, error) { c, err := forwardDial(network, addr) @@ -249,7 +283,17 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re } hostPort, hostNoPort := hostPortNoPort(u) + trace := httptrace.ContextClientTrace(ctx) + if trace != nil && trace.GetConn != nil { + trace.GetConn(hostPort) + } + netConn, err := netDial("tcp", hostPort) + if trace != nil && trace.GotConn != nil { + trace.GotConn(httptrace.GotConnInfo{ + Conn: netConn, + }) + } if err != nil { return nil, nil, err } @@ -267,22 +311,31 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re } tlsConn := tls.Client(netConn, cfg) netConn = tlsConn - if err := tlsConn.Handshake(); err != nil { - return nil, nil, err + + var err error + if trace != nil { + err = doHandshakeWithTrace(trace, tlsConn, cfg) + } else { + err = doHandshake(tlsConn, cfg) } - if !cfg.InsecureSkipVerify { - if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil { - return nil, nil, err - } + + if err != nil { + return nil, nil, err } } - conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize) + conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize, d.WriteBufferPool, nil, nil) if err := req.Write(netConn); err != nil { return nil, nil, err } + if trace != nil && trace.GotFirstResponseByte != nil { + if peek, err := conn.br.Peek(1); err == nil && len(peek) == 1 { + trace.GotFirstResponseByte() + } + } + resp, err := http.ReadResponse(conn.br, req) if err != nil { return nil, nil, err @@ -328,3 +381,15 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re netConn = nil // to avoid close in defer. return conn, resp, nil } + +func doHandshake(tlsConn *tls.Conn, cfg *tls.Config) error { + if err := tlsConn.Handshake(); err != nil { + return err + } + if !cfg.InsecureSkipVerify { + if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil { + return err + } + } + return nil +} diff --git a/vendor/github.com/gorilla/websocket/conn.go b/vendor/github.com/gorilla/websocket/conn.go index 5f46bf4a5..d2a21c148 100644 --- a/vendor/github.com/gorilla/websocket/conn.go +++ b/vendor/github.com/gorilla/websocket/conn.go @@ -223,6 +223,20 @@ func isValidReceivedCloseCode(code int) bool { return validReceivedCloseCodes[code] || (code >= 3000 && code <= 4999) } +// BufferPool represents a pool of buffers. The *sync.Pool type satisfies this +// interface. The type of the value stored in a pool is not specified. +type BufferPool interface { + // Get gets a value from the pool or returns nil if the pool is empty. + Get() interface{} + // Put adds a value to the pool. + Put(interface{}) +} + +// writePoolData is the type added to the write buffer pool. This wrapper is +// used to prevent applications from peeking at and depending on the values +// added to the pool. +type writePoolData struct{ buf []byte } + // The Conn type represents a WebSocket connection. type Conn struct { conn net.Conn @@ -232,6 +246,8 @@ type Conn struct { // Write fields mu chan bool // used as mutex to protect write to conn writeBuf []byte // frame is constructed in this buffer. + writePool BufferPool + writeBufSize int writeDeadline time.Time writer io.WriteCloser // the current writer returned to the application isWriting bool // for best-effort concurrent write detection @@ -263,64 +279,29 @@ type Conn struct { newDecompressionReader func(io.Reader) io.ReadCloser } -func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int) *Conn { - return newConnBRW(conn, isServer, readBufferSize, writeBufferSize, nil) -} +func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, writeBufferPool BufferPool, br *bufio.Reader, writeBuf []byte) *Conn { -type writeHook struct { - p []byte -} - -func (wh *writeHook) Write(p []byte) (int, error) { - wh.p = p - return len(p), nil -} - -func newConnBRW(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, brw *bufio.ReadWriter) *Conn { - mu := make(chan bool, 1) - mu <- true - - var br *bufio.Reader - if readBufferSize == 0 && brw != nil && brw.Reader != nil { - // Reuse the supplied bufio.Reader if the buffer has a useful size. - // This code assumes that peek on a reader returns - // bufio.Reader.buf[:0]. - brw.Reader.Reset(conn) - if p, err := brw.Reader.Peek(0); err == nil && cap(p) >= 256 { - br = brw.Reader - } - } if br == nil { if readBufferSize == 0 { readBufferSize = defaultReadBufferSize - } - if readBufferSize < maxControlFramePayloadSize { + } else if readBufferSize < maxControlFramePayloadSize { + // must be large enough for control frame readBufferSize = maxControlFramePayloadSize } br = bufio.NewReaderSize(conn, readBufferSize) } - var writeBuf []byte - if writeBufferSize == 0 && brw != nil && brw.Writer != nil { - // Use the bufio.Writer's buffer if the buffer has a useful size. This - // code assumes that bufio.Writer.buf[:1] is passed to the - // bufio.Writer's underlying writer. - var wh writeHook - brw.Writer.Reset(&wh) - brw.Writer.WriteByte(0) - brw.Flush() - if cap(wh.p) >= maxFrameHeaderSize+256 { - writeBuf = wh.p[:cap(wh.p)] - } - } - - if writeBuf == nil { - if writeBufferSize == 0 { - writeBufferSize = defaultWriteBufferSize - } - writeBuf = make([]byte, writeBufferSize+maxFrameHeaderSize) + if writeBufferSize <= 0 { + writeBufferSize = defaultWriteBufferSize + } + writeBufferSize += maxFrameHeaderSize + + if writeBuf == nil && writeBufferPool == nil { + writeBuf = make([]byte, writeBufferSize) } + mu := make(chan bool, 1) + mu <- true c := &Conn{ isServer: isServer, br: br, @@ -328,6 +309,8 @@ func newConnBRW(conn net.Conn, isServer bool, readBufferSize, writeBufferSize in mu: mu, readFinal: true, writeBuf: writeBuf, + writePool: writeBufferPool, + writeBufSize: writeBufferSize, enableWriteCompression: true, compressionLevel: defaultCompressionLevel, } @@ -370,6 +353,15 @@ func (c *Conn) writeFatal(err error) error { return err } +func (c *Conn) read(n int) ([]byte, error) { + p, err := c.br.Peek(n) + if err == io.EOF { + err = errUnexpectedEOF + } + c.br.Discard(len(p)) + return p, err +} + func (c *Conn) write(frameType int, deadline time.Time, buf0, buf1 []byte) error { <-c.mu defer func() { c.mu <- true }() @@ -475,7 +467,19 @@ func (c *Conn) prepWrite(messageType int) error { c.writeErrMu.Lock() err := c.writeErr c.writeErrMu.Unlock() - return err + if err != nil { + return err + } + + if c.writeBuf == nil { + wpd, ok := c.writePool.Get().(writePoolData) + if ok { + c.writeBuf = wpd.buf + } else { + c.writeBuf = make([]byte, c.writeBufSize) + } + } + return nil } // NextWriter returns a writer for the next message to send. The writer's Close @@ -601,6 +605,10 @@ func (w *messageWriter) flushFrame(final bool, extra []byte) error { if final { c.writer = nil + if c.writePool != nil { + c.writePool.Put(writePoolData{buf: c.writeBuf}) + c.writeBuf = nil + } return nil } diff --git a/vendor/github.com/gorilla/websocket/conn_read.go b/vendor/github.com/gorilla/websocket/conn_read.go deleted file mode 100644 index 1ea15059e..000000000 --- a/vendor/github.com/gorilla/websocket/conn_read.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build go1.5 - -package websocket - -import "io" - -func (c *Conn) read(n int) ([]byte, error) { - p, err := c.br.Peek(n) - if err == io.EOF { - err = errUnexpectedEOF - } - c.br.Discard(len(p)) - return p, err -} diff --git a/vendor/github.com/gorilla/websocket/conn_read_legacy.go b/vendor/github.com/gorilla/websocket/conn_read_legacy.go deleted file mode 100644 index 018541cf6..000000000 --- a/vendor/github.com/gorilla/websocket/conn_read_legacy.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build !go1.5 - -package websocket - -import "io" - -func (c *Conn) read(n int) ([]byte, error) { - p, err := c.br.Peek(n) - if err == io.EOF { - err = errUnexpectedEOF - } - if len(p) > 0 { - // advance over the bytes just read - io.ReadFull(c.br, p) - } - return p, err -} diff --git a/vendor/github.com/gorilla/websocket/prepared.go b/vendor/github.com/gorilla/websocket/prepared.go index 1efffbd1e..74ec565d2 100644 --- a/vendor/github.com/gorilla/websocket/prepared.go +++ b/vendor/github.com/gorilla/websocket/prepared.go @@ -19,7 +19,6 @@ import ( type PreparedMessage struct { messageType int data []byte - err error mu sync.Mutex frames map[prepareKey]*preparedFrame } diff --git a/vendor/github.com/gorilla/websocket/proxy.go b/vendor/github.com/gorilla/websocket/proxy.go index 102538bd3..bf2478e43 100644 --- a/vendor/github.com/gorilla/websocket/proxy.go +++ b/vendor/github.com/gorilla/websocket/proxy.go @@ -14,7 +14,7 @@ import ( "strings" ) -type netDialerFunc func(netowrk, addr string) (net.Conn, error) +type netDialerFunc func(network, addr string) (net.Conn, error) func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) { return fn(network, addr) diff --git a/vendor/github.com/gorilla/websocket/server.go b/vendor/github.com/gorilla/websocket/server.go index 3e96a00c8..a761824b3 100644 --- a/vendor/github.com/gorilla/websocket/server.go +++ b/vendor/github.com/gorilla/websocket/server.go @@ -7,7 +7,7 @@ package websocket import ( "bufio" "errors" - "net" + "io" "net/http" "net/url" "strings" @@ -33,10 +33,23 @@ type Upgrader struct { // or received. ReadBufferSize, WriteBufferSize int + // WriteBufferPool is a pool of buffers for write operations. If the value + // is not set, then write buffers are allocated to the connection for the + // lifetime of the connection. + // + // A pool is most useful when the application has a modest volume of writes + // across a large number of connections. + // + // Applications should use a single pool for each unique value of + // WriteBufferSize. + WriteBufferPool BufferPool + // Subprotocols specifies the server's supported protocols in order of - // preference. If this field is set, then the Upgrade method negotiates a + // preference. If this field is not nil, then the Upgrade method negotiates a // subprotocol by selecting the first match in this list with a protocol - // requested by the client. + // requested by the client. If there's no match, then no protocol is + // negotiated (the Sec-Websocket-Protocol header is not included in the + // handshake response). Subprotocols []string // Error specifies the function for generating HTTP error responses. If Error @@ -103,7 +116,7 @@ func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header // // The responseHeader is included in the response to the client's upgrade // request. Use the responseHeader to specify cookies (Set-Cookie) and the -// application negotiated subprotocol (Sec-Websocket-Protocol). +// application negotiated subprotocol (Sec-WebSocket-Protocol). // // If the upgrade fails, then Upgrade replies to the client with an HTTP error // response. @@ -127,7 +140,7 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade } if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok { - return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-Websocket-Extensions' headers are unsupported") + return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-WebSocket-Extensions' headers are unsupported") } checkOrigin := u.CheckOrigin @@ -140,7 +153,7 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade challengeKey := r.Header.Get("Sec-Websocket-Key") if challengeKey == "" { - return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: `Sec-Websocket-Key' header is missing or blank") + return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: `Sec-WebSocket-Key' header is missing or blank") } subprotocol := u.selectSubprotocol(r, responseHeader) @@ -157,17 +170,12 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade } } - var ( - netConn net.Conn - err error - ) - h, ok := w.(http.Hijacker) if !ok { return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker") } var brw *bufio.ReadWriter - netConn, brw, err = h.Hijack() + netConn, brw, err := h.Hijack() if err != nil { return u.returnError(w, r, http.StatusInternalServerError, err.Error()) } @@ -177,7 +185,21 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade return nil, errors.New("websocket: client sent data before handshake is complete") } - c := newConnBRW(netConn, true, u.ReadBufferSize, u.WriteBufferSize, brw) + var br *bufio.Reader + if u.ReadBufferSize == 0 && bufioReaderSize(netConn, brw.Reader) > 256 { + // Reuse hijacked buffered reader as connection reader. + br = brw.Reader + } + + buf := bufioWriterBuffer(netConn, brw.Writer) + + var writeBuf []byte + if u.WriteBufferPool == nil && u.WriteBufferSize == 0 && len(buf) >= maxFrameHeaderSize+256 { + // Reuse hijacked write buffer as connection buffer. + writeBuf = buf + } + + c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize, u.WriteBufferPool, br, writeBuf) c.subprotocol = subprotocol if compress { @@ -185,17 +207,23 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade c.newDecompressionReader = decompressNoContextTakeover } - p := c.writeBuf[:0] + // Use larger of hijacked buffer and connection write buffer for header. + p := buf + if len(c.writeBuf) > len(p) { + p = c.writeBuf + } + p = p[:0] + p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...) p = append(p, computeAcceptKey(challengeKey)...) p = append(p, "\r\n"...) if c.subprotocol != "" { - p = append(p, "Sec-Websocket-Protocol: "...) + p = append(p, "Sec-WebSocket-Protocol: "...) p = append(p, c.subprotocol...) p = append(p, "\r\n"...) } if compress { - p = append(p, "Sec-Websocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...) + p = append(p, "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...) } for k, vs := range responseHeader { if k == "Sec-Websocket-Protocol" { @@ -296,3 +324,40 @@ func IsWebSocketUpgrade(r *http.Request) bool { return tokenListContainsValue(r.Header, "Connection", "upgrade") && tokenListContainsValue(r.Header, "Upgrade", "websocket") } + +// bufioReaderSize size returns the size of a bufio.Reader. +func bufioReaderSize(originalReader io.Reader, br *bufio.Reader) int { + // This code assumes that peek on a reset reader returns + // bufio.Reader.buf[:0]. + // TODO: Use bufio.Reader.Size() after Go 1.10 + br.Reset(originalReader) + if p, err := br.Peek(0); err == nil { + return cap(p) + } + return 0 +} + +// writeHook is an io.Writer that records the last slice passed to it vio +// io.Writer.Write. +type writeHook struct { + p []byte +} + +func (wh *writeHook) Write(p []byte) (int, error) { + wh.p = p + return len(p), nil +} + +// bufioWriterBuffer grabs the buffer from a bufio.Writer. +func bufioWriterBuffer(originalWriter io.Writer, bw *bufio.Writer) []byte { + // This code assumes that bufio.Writer.buf[:1] is passed to the + // bufio.Writer's underlying writer. + var wh writeHook + bw.Reset(&wh) + bw.WriteByte(0) + bw.Flush() + + bw.Reset(originalWriter) + + return wh.p[:cap(wh.p)] +} diff --git a/vendor/github.com/gorilla/websocket/trace.go b/vendor/github.com/gorilla/websocket/trace.go new file mode 100644 index 000000000..834f122a0 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/trace.go @@ -0,0 +1,19 @@ +// +build go1.8 + +package websocket + +import ( + "crypto/tls" + "net/http/httptrace" +) + +func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error { + if trace.TLSHandshakeStart != nil { + trace.TLSHandshakeStart() + } + err := doHandshake(tlsConn, cfg) + if trace.TLSHandshakeDone != nil { + trace.TLSHandshakeDone(tlsConn.ConnectionState(), err) + } + return err +} diff --git a/vendor/github.com/gorilla/websocket/trace_17.go b/vendor/github.com/gorilla/websocket/trace_17.go new file mode 100644 index 000000000..77d05a0b5 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/trace_17.go @@ -0,0 +1,12 @@ +// +build !go1.8 + +package websocket + +import ( + "crypto/tls" + "net/http/httptrace" +) + +func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error { + return doHandshake(tlsConn, cfg) +} diff --git a/vendor/github.com/gorilla/websocket/util.go b/vendor/github.com/gorilla/websocket/util.go index 385fa01be..354001e1e 100644 --- a/vendor/github.com/gorilla/websocket/util.go +++ b/vendor/github.com/gorilla/websocket/util.go @@ -178,7 +178,7 @@ headers: return false } -// parseExtensiosn parses WebSocket extensions from a header. +// parseExtensions parses WebSocket extensions from a header. func parseExtensions(header http.Header) []map[string]string { // From RFC 6455: // diff --git a/vendor/github.com/vulcand/oxy/forward/fwd.go b/vendor/github.com/vulcand/oxy/forward/fwd.go index 3a715e479..5a8e81c77 100644 --- a/vendor/github.com/vulcand/oxy/forward/fwd.go +++ b/vendor/github.com/vulcand/oxy/forward/fwd.go @@ -259,6 +259,8 @@ func New(setters ...optSetter) (*Forwarder, error) { errorHandler: f.errHandler, } + f.postConfig() + return f, nil } @@ -342,7 +344,7 @@ func (f *httpForwarder) serveWebSocket(w http.ResponseWriter, req *http.Request, // WebSocket is only in http/1.1 dialer.TLSClientConfig.NextProtos = []string{"http/1.1"} } - targetConn, resp, err := dialer.Dial(outReq.URL.String(), outReq.Header) + targetConn, resp, err := dialer.DialContext(outReq.Context(), outReq.URL.String(), outReq.Header) if err != nil { if resp == nil { ctx.errHandler.ServeHTTP(w, req, err) diff --git a/vendor/github.com/vulcand/oxy/forward/post_config.go b/vendor/github.com/vulcand/oxy/forward/post_config.go new file mode 100644 index 000000000..1c4b12316 --- /dev/null +++ b/vendor/github.com/vulcand/oxy/forward/post_config.go @@ -0,0 +1,5 @@ +// +build go1.11 + +package forward + +func (f *Forwarder) postConfig() {} diff --git a/vendor/github.com/vulcand/oxy/forward/post_config_18.go b/vendor/github.com/vulcand/oxy/forward/post_config_18.go new file mode 100644 index 000000000..7fee6843c --- /dev/null +++ b/vendor/github.com/vulcand/oxy/forward/post_config_18.go @@ -0,0 +1,42 @@ +// +build !go1.11 + +package forward + +import ( + "context" + "net/http" +) + +type key string + +const ( + teHeader key = "TeHeader" +) + +type TeTrailerRoundTripper struct { + http.RoundTripper +} + +func (t *TeTrailerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + teHeader := req.Context().Value(teHeader) + if teHeader != nil { + req.Header.Set("Te", teHeader.(string)) + } + return t.RoundTripper.RoundTrip(req) +} + +type TeTrailerRewriter struct { + ReqRewriter +} + +func (t *TeTrailerRewriter) Rewrite(req *http.Request) { + if req.Header.Get("Te") == "trailers" { + *req = *req.WithContext(context.WithValue(req.Context(), teHeader, req.Header.Get("Te"))) + } + t.ReqRewriter.Rewrite(req) +} + +func (f *Forwarder) postConfig() { + f.roundTripper = &TeTrailerRoundTripper{RoundTripper: f.roundTripper} + f.rewriter = &TeTrailerRewriter{ReqRewriter: f.rewriter} +} diff --git a/vendor/github.com/vulcand/oxy/forward/rewrite.go b/vendor/github.com/vulcand/oxy/forward/rewrite.go index 60c1a1947..b5f8da154 100644 --- a/vendor/github.com/vulcand/oxy/forward/rewrite.go +++ b/vendor/github.com/vulcand/oxy/forward/rewrite.go @@ -69,12 +69,6 @@ func (rw *HeaderRewriter) Rewrite(req *http.Request) { if rw.Hostname != "" { req.Header.Set(XForwardedServer, rw.Hostname) } - - if !IsWebsocketRequest(req) { - // Remove hop-by-hop headers to the backend. Especially important is "Connection" because we want a persistent - // connection, regardless of what the client sent to us. - utils.RemoveHeaders(req.Header, HopHeaders...) - } } func forwardedPort(req *http.Request) string {