diff --git a/.golangci.yml b/.golangci.yml index 43065313c..662de5057 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -134,7 +134,6 @@ issues: exclude: - 'Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked' - "should have a package comment, unless it's in another file for this package" - - 'SA1019: http.CloseNotifier has been deprecated' # FIXME must be fixed - 'SA1019: cfg.SSLRedirect is deprecated' - 'SA1019: cfg.SSLTemporaryRedirect is deprecated' - 'SA1019: cfg.SSLHost is deprecated' diff --git a/docs/content/middlewares/http/compress.md b/docs/content/middlewares/http/compress.md index 53c39ca62..7e4b79613 100644 --- a/docs/content/middlewares/http/compress.md +++ b/docs/content/middlewares/http/compress.md @@ -5,23 +5,24 @@ description: "Traefik Proxy's HTTP middleware lets you compress responses before # Compress -Compress Responses before Sending them to the Client +Compress Allows Compressing Responses before Sending them to the Client {: .subtitle } ![Compress](../../assets/img/middleware/compress.png) -The Compress middleware uses gzip compression. +The Compress middleware supports gzip and Brotli compression. +The activation of compression, and the compression method choice rely (among other things) on the request's `Accept-Encoding` header. ## Configuration Examples ```yaml tab="Docker" -# Enable gzip compression +# Enable compression labels: - "traefik.http.middlewares.test-compress.compress=true" ``` ```yaml tab="Kubernetes" -# Enable gzip compression +# Enable compression apiVersion: traefik.containo.us/v1alpha1 kind: Middleware metadata: @@ -31,7 +32,7 @@ spec: ``` ```yaml tab="Consul Catalog" -# Enable gzip compression +# Enable compression - "traefik.http.middlewares.test-compress.compress=true" ``` @@ -42,13 +43,13 @@ spec: ``` ```yaml tab="Rancher" -# Enable gzip compression +# Enable compression labels: - "traefik.http.middlewares.test-compress.compress=true" ``` ```yaml tab="File (YAML)" -# Enable gzip compression +# Enable compression http: middlewares: test-compress: @@ -56,7 +57,7 @@ http: ``` ```toml tab="File (TOML)" -# Enable gzip compression +# Enable compression [http.middlewares] [http.middlewares.test-compress.compress] ``` @@ -65,23 +66,34 @@ http: Responses are compressed when the following criteria are all met: - * The response body is larger than the configured minimum amount of bytes (default is `1024`). - * The `Accept-Encoding` request header contains `gzip`. + * The `Accept-Encoding` request header contains `gzip`, `*`, and/or `br` with or without [quality values](https://developer.mozilla.org/en-US/docs/Glossary/Quality_values). + If the `Accept-Encoding` request header is absent, it is meant as br compression is requested. + If it is present, but its value is the empty string, then compression is disabled. * The response is not already compressed, i.e. the `Content-Encoding` response header is not already set. - - If the `Content-Type` header is not defined, or empty, the compress middleware will automatically [detect](https://mimesniff.spec.whatwg.org/) a content type. - It will also set the `Content-Type` header according to the detected MIME type. + * The response`Content-Type` header is not one among the [excludedContentTypes options](#excludedcontenttypes). + * The response body is larger than the [configured minimum amount of bytes](#minresponsebodybytes) (default is `1024`). ## Configuration Options ### `excludedContentTypes` +_Optional, Default=""_ + `excludedContentTypes` specifies a list of content types to compare the `Content-Type` header of the incoming requests and responses before compressing. The responses with content types defined in `excludedContentTypes` are not compressed. Content types are compared in a case-insensitive, whitespace-ignored manner. +!!! info "In the case of gzip" + + If the `Content-Type` header is not defined, or empty, the compress middleware will automatically [detect](https://mimesniff.spec.whatwg.org/) a content type. + It will also set the `Content-Type` header according to the detected MIME type. + +!!! info "gRPC" + + Note that `application/grpc` is never compressed. + ```yaml tab="Docker" labels: - "traefik.http.middlewares.test-compress.compress.excludedcontenttypes=text/event-stream" @@ -130,9 +142,9 @@ http: ### `minResponseBodyBytes` -`minResponseBodyBytes` specifies the minimum amount of bytes a response body must have to be compressed. +_Optional, Default=1024_ -The default value is `1024`, which should be a reasonable value for most cases. +`minResponseBodyBytes` specifies the minimum amount of bytes a response body must have to be compressed. Responses smaller than the specified values will not be compressed. diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index 649431ffc..fc571b026 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -57,15 +57,15 @@ path = "foobar" method = "foobar" port = 42 - interval = "foobar" - timeout = "foobar" + interval = "42s" + timeout = "42s" hostname = "foobar" followRedirects = true [http.services.Service01.loadBalancer.healthCheck.headers] name0 = "foobar" name1 = "foobar" [http.services.Service01.loadBalancer.responseForwarding] - flushInterval = "foobar" + flushInterval = "42s" [http.services.Service02] [http.services.Service02.mirroring] service = "foobar" diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index 7b153bb28..808528ef9 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -62,8 +62,8 @@ http: path: foobar method: foobar port: 42 - interval: foobar - timeout: foobar + interval: 42s + timeout: 42s hostname: foobar followRedirects: true headers: @@ -71,7 +71,7 @@ http: name1: foobar passHostHeader: true responseForwarding: - flushInterval: foobar + flushInterval: 42s serversTransport: foobar Service02: mirroring: diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml index d7ca0f5da..18411243e 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -749,7 +749,8 @@ spec: excludedContentTypes: description: ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests - and responses before compressing. + and responses before compressing. `application/grpc` is always + excluded. items: type: string type: array diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index 5457dfafa..9fa147791 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -214,15 +214,15 @@ | `traefik/http/services/Service01/loadBalancer/healthCheck/headers/name0` | `foobar` | | `traefik/http/services/Service01/loadBalancer/healthCheck/headers/name1` | `foobar` | | `traefik/http/services/Service01/loadBalancer/healthCheck/hostname` | `foobar` | -| `traefik/http/services/Service01/loadBalancer/healthCheck/interval` | `foobar` | +| `traefik/http/services/Service01/loadBalancer/healthCheck/interval` | `42s` | | `traefik/http/services/Service01/loadBalancer/healthCheck/method` | `foobar` | | `traefik/http/services/Service01/loadBalancer/healthCheck/mode` | `foobar` | | `traefik/http/services/Service01/loadBalancer/healthCheck/path` | `foobar` | | `traefik/http/services/Service01/loadBalancer/healthCheck/port` | `42` | | `traefik/http/services/Service01/loadBalancer/healthCheck/scheme` | `foobar` | -| `traefik/http/services/Service01/loadBalancer/healthCheck/timeout` | `foobar` | +| `traefik/http/services/Service01/loadBalancer/healthCheck/timeout` | `42s` | | `traefik/http/services/Service01/loadBalancer/passHostHeader` | `true` | -| `traefik/http/services/Service01/loadBalancer/responseForwarding/flushInterval` | `foobar` | +| `traefik/http/services/Service01/loadBalancer/responseForwarding/flushInterval` | `42s` | | `traefik/http/services/Service01/loadBalancer/servers/0/url` | `foobar` | | `traefik/http/services/Service01/loadBalancer/servers/1/url` | `foobar` | | `traefik/http/services/Service01/loadBalancer/serversTransport` | `foobar` | diff --git a/docs/content/reference/dynamic-configuration/marathon-labels.json b/docs/content/reference/dynamic-configuration/marathon-labels.json index fbadce690..5bc8e9e7c 100644 --- a/docs/content/reference/dynamic-configuration/marathon-labels.json +++ b/docs/content/reference/dynamic-configuration/marathon-labels.json @@ -150,15 +150,15 @@ "traefik.http.services.service01.loadbalancer.healthcheck.headers.name0": "foobar", "traefik.http.services.service01.loadbalancer.healthcheck.headers.name1": "foobar", "traefik.http.services.service01.loadbalancer.healthcheck.hostname": "foobar", -"traefik.http.services.service01.loadbalancer.healthcheck.interval": "foobar", +"traefik.http.services.service01.loadbalancer.healthcheck.interval": "42s", "traefik.http.services.service01.loadbalancer.healthcheck.path": "foobar", "traefik.http.services.service01.loadbalancer.healthcheck.method": "foobar", "traefik.http.services.service01.loadbalancer.healthcheck.port": "42", "traefik.http.services.service01.loadbalancer.healthcheck.scheme": "foobar", "traefik.http.services.service01.loadbalancer.healthcheck.mode": "foobar", -"traefik.http.services.service01.loadbalancer.healthcheck.timeout": "foobar", +"traefik.http.services.service01.loadbalancer.healthcheck.timeout": "42s", "traefik.http.services.service01.loadbalancer.passhostheader": "true", -"traefik.http.services.service01.loadbalancer.responseforwarding.flushinterval": "foobar", +"traefik.http.services.service01.loadbalancer.responseforwarding.flushinterval": "42s", "traefik.http.services.service01.loadbalancer.serverstransport": "foobar", "traefik.http.services.service01.loadbalancer.sticky.cookie": "true", "traefik.http.services.service01.loadbalancer.sticky.cookie.httponly": "true", diff --git a/docs/content/reference/dynamic-configuration/traefik.containo.us_middlewares.yaml b/docs/content/reference/dynamic-configuration/traefik.containo.us_middlewares.yaml index 1fe2e7f63..0715292d5 100644 --- a/docs/content/reference/dynamic-configuration/traefik.containo.us_middlewares.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.containo.us_middlewares.yaml @@ -172,7 +172,8 @@ spec: excludedContentTypes: description: ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests - and responses before compressing. + and responses before compressing. `application/grpc` is always + excluded. items: type: string type: array diff --git a/docs/content/routing/routers/index.md b/docs/content/routing/routers/index.md index 00bd82375..4c25f4e0d 100644 --- a/docs/content/routing/routers/index.md +++ b/docs/content/routing/routers/index.md @@ -233,18 +233,18 @@ If the rule is verified, the router becomes active, calls middlewares, and then The table below lists all the available matchers: -| Rule | Description | -|--------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------| -| ```Headers(`key`, `value`)``` | Check if there is a key `key`defined in the headers, with the value `value` | -| ```HeadersRegexp(`key`, `regexp`)``` | Check if there is a key `key`defined in the headers, with a value that matches the regular expression `regexp` | -| ```Host(`example.com`, ...)``` | Check if the request domain (host header value) targets one of the given `domains`. | -| ```HostHeader(`example.com`, ...)``` | Same as `Host`, only exists for historical reasons. | -| ```HostRegexp(`example.com`, `{subdomain:[a-z]+}.example.com`, ...)``` | Match the request domain. See "Regexp Syntax" below. | -| ```Method(`GET`, ...)``` | Check if the request method is one of the given `methods` (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`) | -| ```Path(`/path`, `/articles/{cat:[a-z]+}/{id:[0-9]+}`, ...)``` | Match exact request path. See "Regexp Syntax" below. | -| ```PathPrefix(`/products/`, `/articles/{cat:[a-z]+}/{id:[0-9]+}`)``` | Match request prefix path. See "Regexp Syntax" below. | -| ```Query(`foo=bar`, `bar=baz`)``` | Match Query String parameters. It accepts a sequence of key=value pairs. | -| ```ClientIP(`10.0.0.0/16`, `::1`)``` | Match if the request client IP is one of the given IP/CIDR. It accepts IPv4, IPv6 and CIDR formats. | +| Rule | Description | +|------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------| +| ```Headers(`key`, `value`)``` | Check if there is a key `key`defined in the headers, with the value `value` | +| ```HeadersRegexp(`key`, `regexp`)``` | Check if there is a key `key`defined in the headers, with a value that matches the regular expression `regexp` | +| ```Host(`example.com`, ...)``` | Check if the request domain (host header value) targets one of the given `domains`. | +| ```HostHeader(`example.com`, ...)``` | Same as `Host`, only exists for historical reasons. | +| ```HostRegexp(`example.com`, `{subdomain:[a-z]+}.example.com`, ...)``` | Match the request domain. See "Regexp Syntax" below. | +| ```Method(`GET`, ...)``` | Check if the request method is one of the given `methods` (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`) | +| ```Path(`/path`, `/articles/{cat:[a-z]+}/{id:[0-9]+}`, ...)``` | Match exact request path. See "Regexp Syntax" below. | +| ```PathPrefix(`/products/`, `/articles/{cat:[a-z]+}/{id:[0-9]+}`)``` | Match request prefix path. See "Regexp Syntax" below. | +| ```Query(`foo=bar`, `bar=baz`)``` | Match Query String parameters. It accepts a sequence of key=value pairs. | +| ```ClientIP(`10.0.0.0/16`, `::1`)``` | Match if the request client IP is one of the given IP/CIDR. It accepts IPv4, IPv6 and CIDR formats. | !!! important "Non-ASCII Domain Names" @@ -1041,6 +1041,30 @@ By default, a router with a TLS section will terminate the TLS connections, mean [tcp.routers.Router-1.tls] ``` +??? info "Postgres STARTTLS" + + Traefik supports the Postgres STARTTLS protocol, + which allows TLS routing for Postgres connections. + + To do so, Traefik reads the first bytes sent by a Postgres client, + identifies if they correspond to the message of a STARTTLS negotiation, + and, if so, acknowledges and signals the client that it can start the TLS handshake. + + Please note/remember that there are subtleties inherent to STARTTLS in whether + the connection ends up being a TLS one or not. These subtleties depend on the + `sslmode` value in the client configuration (and on the server authentication + rules). Therefore, it is recommended to use the `require` value for the + `sslmode`. + + Afterwards, the TLS handshake, and routing based on TLS, can proceed as expected. + + !!! warning "Postgres STARTTLS with TCP TLS PassThrough routers" + + As mentioned above, the `sslmode` configuration parameter does have an impact on + whether a STARTTLS session will succeed. In particular in the context of TCP TLS + PassThrough, some of the values (such as `allow`) do not even make sense. Which + is why, once more it is recommended to use the `require` value. + #### `passthrough` As seen above, a TLS router will terminate the TLS connection by default. diff --git a/go.mod b/go.mod index a28670c0c..9fe0bfc7a 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/ExpediaDotCom/haystack-client-go v0.0.0-20190315171017-e7edbdf53a61 github.com/Masterminds/sprig/v3 v3.2.2 github.com/abbot/go-http-auth v0.0.0-00010101000000-000000000000 + github.com/andybalholm/brotli v1.0.4 github.com/aws/aws-sdk-go v1.44.47 github.com/cenkalti/backoff/v4 v4.1.3 github.com/compose-spec/compose-go v1.0.3 diff --git a/go.sum b/go.sum index 38e01b047..7801b7c1b 100644 --- a/go.sum +++ b/go.sum @@ -212,6 +212,8 @@ github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755/go.mod h1:RcDobYh8k5VP6TNybz9m github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index d7ca0f5da..18411243e 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -749,7 +749,8 @@ spec: excludedContentTypes: description: ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests - and responses before compressing. + and responses before compressing. `application/grpc` is always + excluded. items: type: string type: array diff --git a/integration/testdata/rawdata-consul.json b/integration/testdata/rawdata-consul.json index 0bf110eac..60423f234 100644 --- a/integration/testdata/rawdata-consul.json +++ b/integration/testdata/rawdata-consul.json @@ -179,7 +179,10 @@ "url": "http://10.0.1.1:8889" } ], - "passHostHeader": true + "passHostHeader": true, + "responseForwarding": { + "flushInterval": "100ms" + } }, "status": "enabled", "usedBy": [ @@ -201,7 +204,10 @@ "url": "http://10.0.1.2:8889" } ], - "passHostHeader": true + "passHostHeader": true, + "responseForwarding": { + "flushInterval": "100ms" + } }, "status": "enabled" }, @@ -215,7 +221,10 @@ "url": "http://10.0.1.3:8889" } ], - "passHostHeader": true + "passHostHeader": true, + "responseForwarding": { + "flushInterval": "100ms" + } }, "status": "enabled" } diff --git a/integration/testdata/rawdata-crd-label-selector.json b/integration/testdata/rawdata-crd-label-selector.json index d75df29a0..63ace05eb 100644 --- a/integration/testdata/rawdata-crd-label-selector.json +++ b/integration/testdata/rawdata-crd-label-selector.json @@ -47,7 +47,10 @@ "url": "http://10.42.0.4:80" } ], - "passHostHeader": true + "passHostHeader": true, + "responseForwarding": { + "flushInterval": "100ms" + } }, "status": "enabled", "usedBy": [ diff --git a/integration/testdata/rawdata-crd.json b/integration/testdata/rawdata-crd.json index 2b1139b0d..a49ebddb2 100644 --- a/integration/testdata/rawdata-crd.json +++ b/integration/testdata/rawdata-crd.json @@ -137,7 +137,10 @@ "url": "http://10.42.0.7:80" } ], - "passHostHeader": true + "passHostHeader": true, + "responseForwarding": { + "flushInterval": "100ms" + } }, "status": "enabled", "usedBy": [ @@ -158,7 +161,10 @@ "url": "http://10.42.0.7:80" } ], - "passHostHeader": true + "passHostHeader": true, + "responseForwarding": { + "flushInterval": "100ms" + } }, "status": "enabled", "usedBy": [ @@ -180,6 +186,9 @@ } ], "passHostHeader": true, + "responseForwarding": { + "flushInterval": "100ms" + }, "serversTransport": "default-mytransport@kubernetescrd" }, "status": "enabled", @@ -201,7 +210,10 @@ "url": "http://10.42.0.7:80" } ], - "passHostHeader": true + "passHostHeader": true, + "responseForwarding": { + "flushInterval": "100ms" + } }, "status": "enabled", "serverStatus": { diff --git a/integration/testdata/rawdata-etcd.json b/integration/testdata/rawdata-etcd.json index 92b4aa2b5..03abf1607 100644 --- a/integration/testdata/rawdata-etcd.json +++ b/integration/testdata/rawdata-etcd.json @@ -179,7 +179,10 @@ "url": "http://10.0.1.1:8889" } ], - "passHostHeader": true + "passHostHeader": true, + "responseForwarding": { + "flushInterval": "100ms" + } }, "status": "enabled", "usedBy": [ @@ -201,7 +204,10 @@ "url": "http://10.0.1.2:8889" } ], - "passHostHeader": true + "passHostHeader": true, + "responseForwarding": { + "flushInterval": "100ms" + } }, "status": "enabled" }, @@ -215,7 +221,10 @@ "url": "http://10.0.1.3:8889" } ], - "passHostHeader": true + "passHostHeader": true, + "responseForwarding": { + "flushInterval": "100ms" + } }, "status": "enabled" } diff --git a/integration/testdata/rawdata-gateway.json b/integration/testdata/rawdata-gateway.json index a18a09ef8..46c50efd6 100644 --- a/integration/testdata/rawdata-gateway.json +++ b/integration/testdata/rawdata-gateway.json @@ -128,7 +128,10 @@ "url": "http://10.42.0.7:80" } ], - "passHostHeader": true + "passHostHeader": true, + "responseForwarding": { + "flushInterval": "100ms" + } }, "status": "enabled", "serverStatus": { diff --git a/integration/testdata/rawdata-ingress-label-selector.json b/integration/testdata/rawdata-ingress-label-selector.json index 497835714..54fbae930 100644 --- a/integration/testdata/rawdata-ingress-label-selector.json +++ b/integration/testdata/rawdata-ingress-label-selector.json @@ -88,7 +88,10 @@ "url": "http://10.42.0.7:80" } ], - "passHostHeader": true + "passHostHeader": true, + "responseForwarding": { + "flushInterval": "100ms" + } }, "status": "enabled", "usedBy": [ diff --git a/integration/testdata/rawdata-ingress.json b/integration/testdata/rawdata-ingress.json index 2943e4cbd..a912d1ea0 100644 --- a/integration/testdata/rawdata-ingress.json +++ b/integration/testdata/rawdata-ingress.json @@ -121,7 +121,10 @@ "url": "XXXX" } ], - "passHostHeader": true + "passHostHeader": true, + "responseForwarding": { + "flushInterval": "100ms" + } }, "status": "enabled", "usedBy": [ @@ -143,7 +146,10 @@ "url": "http://10.42.0.8:80" } ], - "passHostHeader": true + "passHostHeader": true, + "responseForwarding": { + "flushInterval": "100ms" + } }, "status": "enabled", "usedBy": [ diff --git a/integration/testdata/rawdata-ingressclass.json b/integration/testdata/rawdata-ingressclass.json index 0944a7cff..e592135bc 100644 --- a/integration/testdata/rawdata-ingressclass.json +++ b/integration/testdata/rawdata-ingressclass.json @@ -88,7 +88,10 @@ "url": "http://10.42.0.5:80" } ], - "passHostHeader": true + "passHostHeader": true, + "responseForwarding": { + "flushInterval": "100ms" + } }, "status": "enabled", "usedBy": [ diff --git a/integration/testdata/rawdata-redis.json b/integration/testdata/rawdata-redis.json index 1dda1cebe..a3fb20f66 100644 --- a/integration/testdata/rawdata-redis.json +++ b/integration/testdata/rawdata-redis.json @@ -179,7 +179,10 @@ "url": "http://10.0.1.1:8889" } ], - "passHostHeader": true + "passHostHeader": true, + "responseForwarding": { + "flushInterval": "100ms" + } }, "status": "enabled", "usedBy": [ @@ -201,7 +204,10 @@ "url": "http://10.0.1.2:8889" } ], - "passHostHeader": true + "passHostHeader": true, + "responseForwarding": { + "flushInterval": "100ms" + } }, "status": "enabled" }, @@ -215,7 +221,10 @@ "url": "http://10.0.1.3:8889" } ], - "passHostHeader": true + "passHostHeader": true, + "responseForwarding": { + "flushInterval": "100ms" + } }, "status": "enabled" } diff --git a/integration/testdata/rawdata-zk.json b/integration/testdata/rawdata-zk.json index 67d5c47f9..c4e54b85e 100644 --- a/integration/testdata/rawdata-zk.json +++ b/integration/testdata/rawdata-zk.json @@ -179,7 +179,10 @@ "url": "http://10.0.1.1:8889" } ], - "passHostHeader": true + "passHostHeader": true, + "responseForwarding": { + "flushInterval": "100ms" + } }, "status": "enabled", "usedBy": [ @@ -201,7 +204,10 @@ "url": "http://10.0.1.2:8889" } ], - "passHostHeader": true + "passHostHeader": true, + "responseForwarding": { + "flushInterval": "100ms" + } }, "status": "enabled" }, @@ -215,7 +221,10 @@ "url": "http://10.0.1.3:8889" } ], - "passHostHeader": true + "passHostHeader": true, + "responseForwarding": { + "flushInterval": "100ms" + } }, "status": "enabled" } diff --git a/pkg/config/dynamic/fixtures/sample.toml b/pkg/config/dynamic/fixtures/sample.toml index 4e52c4d03..b280b5484 100644 --- a/pkg/config/dynamic/fixtures/sample.toml +++ b/pkg/config/dynamic/fixtures/sample.toml @@ -425,14 +425,14 @@ mode = "foobar" path = "foobar" port = 42 - interval = "foobar" - timeout = "foobar" + interval = "10s" + timeout = "10s" hostname = "foobar" [http.services.Service0.loadBalancer.healthCheck.headers] name0 = "foobar" name1 = "foobar" [http.services.Service0.loadBalancer.responseForwarding] - flushInterval = "foobar" + flushInterval = "10s" [tcp] [tcp.routers] diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index f93c1a6a3..89cbf7026 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -9,6 +9,19 @@ import ( "github.com/traefik/traefik/v2/pkg/types" ) +const ( + // DefaultHealthCheckInterval is the default value for the ServerHealthCheck interval. + DefaultHealthCheckInterval = ptypes.Duration(30 * time.Second) + // DefaultHealthCheckTimeout is the default value for the ServerHealthCheck timeout. + DefaultHealthCheckTimeout = ptypes.Duration(5 * time.Second) + + // DefaultPassHostHeader is the default value for the ServersLoadBalancer passHostHeader. + DefaultPassHostHeader = true + + // DefaultFlushInterval is the default value for the ResponseForwarding flush interval. + DefaultFlushInterval = ptypes.Duration(100 * time.Millisecond) +) + // +k8s:deepcopy-gen=true // HTTPConfiguration contains all the HTTP configuration parameters. @@ -178,8 +191,11 @@ func (l *ServersLoadBalancer) Mergeable(loadBalancer *ServersLoadBalancer) bool // SetDefaults Default values for a ServersLoadBalancer. func (l *ServersLoadBalancer) SetDefaults() { - defaultPassHostHeader := true + defaultPassHostHeader := DefaultPassHostHeader l.PassHostHeader = &defaultPassHostHeader + + l.ResponseForwarding = &ResponseForwarding{} + l.ResponseForwarding.SetDefaults() } // +k8s:deepcopy-gen=true @@ -191,7 +207,12 @@ type ResponseForwarding struct { // This configuration is ignored when ReverseProxy recognizes a response as a streaming response; // for such responses, writes are flushed to the client immediately. // Default: 100ms - FlushInterval string `json:"flushInterval,omitempty" toml:"flushInterval,omitempty" yaml:"flushInterval,omitempty" export:"true"` + FlushInterval ptypes.Duration `json:"flushInterval,omitempty" toml:"flushInterval,omitempty" yaml:"flushInterval,omitempty" export:"true"` +} + +// SetDefaults Default values for a ResponseForwarding. +func (r *ResponseForwarding) SetDefaults() { + r.FlushInterval = DefaultFlushInterval } // +k8s:deepcopy-gen=true @@ -212,15 +233,13 @@ func (s *Server) SetDefaults() { // ServerHealthCheck holds the HealthCheck configuration. type ServerHealthCheck struct { - Scheme string `json:"scheme,omitempty" toml:"scheme,omitempty" yaml:"scheme,omitempty" export:"true"` - Mode string `json:"mode,omitempty" toml:"mode,omitempty" yaml:"mode,omitempty" export:"true"` - Path string `json:"path,omitempty" toml:"path,omitempty" yaml:"path,omitempty" export:"true"` - Method string `json:"method,omitempty" toml:"method,omitempty" yaml:"method,omitempty" export:"true"` - Port int `json:"port,omitempty" toml:"port,omitempty,omitzero" yaml:"port,omitempty" export:"true"` - // TODO change string to ptypes.Duration - Interval string `json:"interval,omitempty" toml:"interval,omitempty" yaml:"interval,omitempty" export:"true"` - // TODO change string to ptypes.Duration - Timeout string `json:"timeout,omitempty" toml:"timeout,omitempty" yaml:"timeout,omitempty" export:"true"` + Scheme string `json:"scheme,omitempty" toml:"scheme,omitempty" yaml:"scheme,omitempty" export:"true"` + Mode string `json:"mode,omitempty" toml:"mode,omitempty" yaml:"mode,omitempty" export:"true"` + Path string `json:"path,omitempty" toml:"path,omitempty" yaml:"path,omitempty" export:"true"` + Method string `json:"method,omitempty" toml:"method,omitempty" yaml:"method,omitempty" export:"true"` + Port int `json:"port,omitempty" toml:"port,omitempty,omitzero" yaml:"port,omitempty" export:"true"` + Interval ptypes.Duration `json:"interval,omitempty" toml:"interval,omitempty" yaml:"interval,omitempty" export:"true"` + Timeout ptypes.Duration `json:"timeout,omitempty" toml:"timeout,omitempty" yaml:"timeout,omitempty" export:"true"` Hostname string `json:"hostname,omitempty" toml:"hostname,omitempty" yaml:"hostname,omitempty"` FollowRedirects *bool `json:"followRedirects" toml:"followRedirects" yaml:"followRedirects" export:"true"` Headers map[string]string `json:"headers,omitempty" toml:"headers,omitempty" yaml:"headers,omitempty" export:"true"` @@ -231,6 +250,8 @@ func (h *ServerHealthCheck) SetDefaults() { fr := true h.FollowRedirects = &fr h.Mode = "http" + h.Interval = DefaultHealthCheckInterval + h.Timeout = DefaultHealthCheckTimeout } // +k8s:deepcopy-gen=true diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index ea482a9eb..a8a2dfe3b 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -161,6 +161,7 @@ func (c *CircuitBreaker) SetDefaults() { // More info: https://doc.traefik.io/traefik/v2.9/middlewares/http/compress/ type Compress struct { // ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing. + // `application/grpc` is always excluded. ExcludedContentTypes []string `json:"excludedContentTypes,omitempty" toml:"excludedContentTypes,omitempty" yaml:"excludedContentTypes,omitempty" export:"true"` // MinResponseBodyBytes defines the minimum amount of bytes a response body must have to be compressed. // Default: 1024. diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go index e3bb4e609..702e9bffe 100644 --- a/pkg/config/label/label_test.go +++ b/pkg/config/label/label_test.go @@ -148,16 +148,16 @@ func TestDecodeConfiguration(t *testing.T) { "traefik.http.services.Service0.loadbalancer.healthcheck.headers.name0": "foobar", "traefik.http.services.Service0.loadbalancer.healthcheck.headers.name1": "foobar", "traefik.http.services.Service0.loadbalancer.healthcheck.hostname": "foobar", - "traefik.http.services.Service0.loadbalancer.healthcheck.interval": "foobar", + "traefik.http.services.Service0.loadbalancer.healthcheck.interval": "1s", "traefik.http.services.Service0.loadbalancer.healthcheck.path": "foobar", "traefik.http.services.Service0.loadbalancer.healthcheck.method": "foobar", "traefik.http.services.Service0.loadbalancer.healthcheck.port": "42", "traefik.http.services.Service0.loadbalancer.healthcheck.scheme": "foobar", "traefik.http.services.Service0.loadbalancer.healthcheck.mode": "foobar", - "traefik.http.services.Service0.loadbalancer.healthcheck.timeout": "foobar", + "traefik.http.services.Service0.loadbalancer.healthcheck.timeout": "1s", "traefik.http.services.Service0.loadbalancer.healthcheck.followredirects": "true", "traefik.http.services.Service0.loadbalancer.passhostheader": "true", - "traefik.http.services.Service0.loadbalancer.responseforwarding.flushinterval": "foobar", + "traefik.http.services.Service0.loadbalancer.responseforwarding.flushinterval": "1s", "traefik.http.services.Service0.loadbalancer.server.scheme": "foobar", "traefik.http.services.Service0.loadbalancer.server.port": "8080", "traefik.http.services.Service0.loadbalancer.sticky.cookie.name": "foobar", @@ -165,16 +165,16 @@ func TestDecodeConfiguration(t *testing.T) { "traefik.http.services.Service1.loadbalancer.healthcheck.headers.name0": "foobar", "traefik.http.services.Service1.loadbalancer.healthcheck.headers.name1": "foobar", "traefik.http.services.Service1.loadbalancer.healthcheck.hostname": "foobar", - "traefik.http.services.Service1.loadbalancer.healthcheck.interval": "foobar", + "traefik.http.services.Service1.loadbalancer.healthcheck.interval": "1s", "traefik.http.services.Service1.loadbalancer.healthcheck.path": "foobar", "traefik.http.services.Service1.loadbalancer.healthcheck.method": "foobar", "traefik.http.services.Service1.loadbalancer.healthcheck.port": "42", "traefik.http.services.Service1.loadbalancer.healthcheck.scheme": "foobar", "traefik.http.services.Service1.loadbalancer.healthcheck.mode": "foobar", - "traefik.http.services.Service1.loadbalancer.healthcheck.timeout": "foobar", + "traefik.http.services.Service1.loadbalancer.healthcheck.timeout": "1s", "traefik.http.services.Service1.loadbalancer.healthcheck.followredirects": "true", "traefik.http.services.Service1.loadbalancer.passhostheader": "true", - "traefik.http.services.Service1.loadbalancer.responseforwarding.flushinterval": "foobar", + "traefik.http.services.Service1.loadbalancer.responseforwarding.flushinterval": "1s", "traefik.http.services.Service1.loadbalancer.server.scheme": "foobar", "traefik.http.services.Service1.loadbalancer.server.port": "8080", "traefik.http.services.Service1.loadbalancer.sticky": "false", @@ -656,8 +656,8 @@ func TestDecodeConfiguration(t *testing.T) { Path: "foobar", Method: "foobar", Port: 42, - Interval: "foobar", - Timeout: "foobar", + Interval: ptypes.Duration(time.Second), + Timeout: ptypes.Duration(time.Second), Hostname: "foobar", Headers: map[string]string{ "name0": "foobar", @@ -667,7 +667,7 @@ func TestDecodeConfiguration(t *testing.T) { }, PassHostHeader: func(v bool) *bool { return &v }(true), ResponseForwarding: &dynamic.ResponseForwarding{ - FlushInterval: "foobar", + FlushInterval: ptypes.Duration(time.Second), }, }, }, @@ -685,8 +685,8 @@ func TestDecodeConfiguration(t *testing.T) { Path: "foobar", Method: "foobar", Port: 42, - Interval: "foobar", - Timeout: "foobar", + Interval: ptypes.Duration(time.Second), + Timeout: ptypes.Duration(time.Second), Hostname: "foobar", Headers: map[string]string{ "name0": "foobar", @@ -696,7 +696,7 @@ func TestDecodeConfiguration(t *testing.T) { }, PassHostHeader: func(v bool) *bool { return &v }(true), ResponseForwarding: &dynamic.ResponseForwarding{ - FlushInterval: "foobar", + FlushInterval: ptypes.Duration(time.Second), }, }, }, @@ -1148,8 +1148,8 @@ func TestEncodeConfiguration(t *testing.T) { Path: "foobar", Method: "foobar", Port: 42, - Interval: "foobar", - Timeout: "foobar", + Interval: ptypes.Duration(time.Second), + Timeout: ptypes.Duration(time.Second), Hostname: "foobar", Headers: map[string]string{ "name0": "foobar", @@ -1158,7 +1158,7 @@ func TestEncodeConfiguration(t *testing.T) { }, PassHostHeader: func(v bool) *bool { return &v }(true), ResponseForwarding: &dynamic.ResponseForwarding{ - FlushInterval: "foobar", + FlushInterval: ptypes.Duration(time.Second), }, }, }, @@ -1175,8 +1175,8 @@ func TestEncodeConfiguration(t *testing.T) { Path: "foobar", Method: "foobar", Port: 42, - Interval: "foobar", - Timeout: "foobar", + Interval: ptypes.Duration(time.Second), + Timeout: ptypes.Duration(time.Second), Hostname: "foobar", Headers: map[string]string{ "name0": "foobar", @@ -1185,7 +1185,7 @@ func TestEncodeConfiguration(t *testing.T) { }, PassHostHeader: func(v bool) *bool { return &v }(true), ResponseForwarding: &dynamic.ResponseForwarding{ - FlushInterval: "foobar", + FlushInterval: ptypes.Duration(time.Second), }, }, }, @@ -1332,14 +1332,14 @@ func TestEncodeConfiguration(t *testing.T) { "traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Headers.name1": "foobar", "traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Hostname": "foobar", - "traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Interval": "foobar", + "traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Interval": "1000000000", "traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Path": "foobar", "traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Method": "foobar", "traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Port": "42", "traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Scheme": "foobar", - "traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Timeout": "foobar", + "traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Timeout": "1000000000", "traefik.HTTP.Services.Service0.LoadBalancer.PassHostHeader": "true", - "traefik.HTTP.Services.Service0.LoadBalancer.ResponseForwarding.FlushInterval": "foobar", + "traefik.HTTP.Services.Service0.LoadBalancer.ResponseForwarding.FlushInterval": "1000000000", "traefik.HTTP.Services.Service0.LoadBalancer.server.Port": "8080", "traefik.HTTP.Services.Service0.LoadBalancer.server.Scheme": "foobar", "traefik.HTTP.Services.Service0.LoadBalancer.Sticky.Cookie.Name": "foobar", @@ -1348,14 +1348,14 @@ func TestEncodeConfiguration(t *testing.T) { "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Headers.name0": "foobar", "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Headers.name1": "foobar", "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Hostname": "foobar", - "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Interval": "foobar", + "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Interval": "1000000000", "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Path": "foobar", "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Method": "foobar", "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Port": "42", "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Scheme": "foobar", - "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Timeout": "foobar", + "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Timeout": "1000000000", "traefik.HTTP.Services.Service1.LoadBalancer.PassHostHeader": "true", - "traefik.HTTP.Services.Service1.LoadBalancer.ResponseForwarding.FlushInterval": "foobar", + "traefik.HTTP.Services.Service1.LoadBalancer.ResponseForwarding.FlushInterval": "1000000000", "traefik.HTTP.Services.Service1.LoadBalancer.server.Port": "8080", "traefik.HTTP.Services.Service1.LoadBalancer.server.Scheme": "foobar", "traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Headers.name0": "foobar", diff --git a/pkg/config/runtime/runtime.go b/pkg/config/runtime/runtime.go index 9f64c7147..713541538 100644 --- a/pkg/config/runtime/runtime.go +++ b/pkg/config/runtime/runtime.go @@ -15,6 +15,12 @@ const ( StatusWarning = "warning" ) +// Status of the servers. +const ( + StatusUp = "UP" + StatusDown = "DOWN" +) + // Configuration holds the information about the currently running traefik instance. type Configuration struct { Routers map[string]*RouterInfo `json:"routers,omitempty"` diff --git a/pkg/config/runtime/runtime_test.go b/pkg/config/runtime/runtime_test.go index 3a7588be3..e952cb20c 100644 --- a/pkg/config/runtime/runtime_test.go +++ b/pkg/config/runtime/runtime_test.go @@ -2,9 +2,11 @@ package runtime_test import ( "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + ptypes "github.com/traefik/paerser/types" "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/config/runtime" ) @@ -49,7 +51,7 @@ func TestPopulateUsedBy(t *testing.T) { {URL: "http://127.0.0.1:8086"}, }, HealthCheck: &dynamic.ServerHealthCheck{ - Interval: "500ms", + Interval: ptypes.Duration(500 * time.Millisecond), Path: "/health", }, }, @@ -159,7 +161,7 @@ func TestPopulateUsedBy(t *testing.T) { }, }, HealthCheck: &dynamic.ServerHealthCheck{ - Interval: "500ms", + Interval: ptypes.Duration(500 * time.Millisecond), Path: "/health", }, }, @@ -177,7 +179,7 @@ func TestPopulateUsedBy(t *testing.T) { }, }, HealthCheck: &dynamic.ServerHealthCheck{ - Interval: "500ms", + Interval: ptypes.Duration(500 * time.Millisecond), Path: "/health", }, }, diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go index d3dc851bd..ca9a91069 100644 --- a/pkg/healthcheck/healthcheck.go +++ b/pkg/healthcheck/healthcheck.go @@ -8,17 +8,12 @@ import ( "net/http" "net/url" "strconv" - "strings" - "sync" "time" gokitmetrics "github.com/go-kit/kit/metrics" "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/config/runtime" "github.com/traefik/traefik/v2/pkg/log" - "github.com/traefik/traefik/v2/pkg/metrics" - "github.com/traefik/traefik/v2/pkg/safe" - "github.com/vulcand/oxy/roundrobin" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" @@ -26,267 +21,153 @@ import ( "google.golang.org/grpc/status" ) -const ( - serverUp = "UP" - serverDown = "DOWN" -) +const modeGRPC = "grpc" -const ( - HTTPMode = "http" - GRPCMode = "grpc" -) - -var ( - singleton *HealthCheck - once sync.Once -) - -// Balancer is the set of operations required to manage the list of servers in a load-balancer. -type Balancer interface { - Servers() []*url.URL - RemoveServer(u *url.URL) error - UpsertServer(u *url.URL, options ...roundrobin.ServerOption) error +// StatusSetter should be implemented by a service that, when the status of a +// registered target change, needs to be notified of that change. +type StatusSetter interface { + SetStatus(ctx context.Context, childName string, up bool) } -// BalancerHandler includes functionality for load-balancing management. -type BalancerHandler interface { - ServeHTTP(w http.ResponseWriter, req *http.Request) - Balancer +// StatusUpdater should be implemented by a service that, when its status +// changes (e.g. all if its children are down), needs to propagate upwards (to +// their parent(s)) that change. +type StatusUpdater interface { + RegisterStatusUpdater(fn func(up bool)) error } -// BalancerStatusHandler is an http Handler that does load-balancing, -// and updates its parents of its status. -type BalancerStatusHandler interface { - BalancerHandler - StatusUpdater +type metricsHealthCheck interface { + ServiceServerUpGauge() gokitmetrics.Gauge } -type metricsHealthcheck struct { - serverUpGauge gokitmetrics.Gauge +type ServiceHealthChecker struct { + balancer StatusSetter + info *runtime.ServiceInfo + + config *dynamic.ServerHealthCheck + interval time.Duration + timeout time.Duration + + metrics metricsHealthCheck + + client *http.Client + targets map[string]*url.URL } -// Options are the public health check options. -type Options struct { - Headers map[string]string - Hostname string - Scheme string - Mode string - Path string - Method string - Port int - FollowRedirects bool - Transport http.RoundTripper - Interval time.Duration - Timeout time.Duration - LB Balancer -} - -func (opt Options) String() string { - return fmt.Sprintf("[Hostname: %s Headers: %v Path: %s Method: %s Port: %d Interval: %s Timeout: %s FollowRedirects: %v]", opt.Hostname, opt.Headers, opt.Path, opt.Method, opt.Port, opt.Interval, opt.Timeout, opt.FollowRedirects) -} - -type backendURL struct { - url *url.URL - weight int -} - -// BackendConfig HealthCheck configuration for a backend. -type BackendConfig struct { - Options - name string - disabledURLs []backendURL -} - -func (b *BackendConfig) newRequest(serverURL *url.URL) (*http.Request, error) { - u, err := serverURL.Parse(b.Path) - if err != nil { - return nil, err - } - - if len(b.Scheme) > 0 { - u.Scheme = b.Scheme - } - - if b.Port != 0 { - u.Host = net.JoinHostPort(u.Hostname(), strconv.Itoa(b.Port)) - } - - return http.NewRequest(http.MethodGet, u.String(), http.NoBody) -} - -// setRequestOptions sets all request options present on the BackendConfig. -func (b *BackendConfig) setRequestOptions(req *http.Request) *http.Request { - if b.Options.Hostname != "" { - req.Host = b.Options.Hostname - } - - for k, v := range b.Options.Headers { - req.Header.Set(k, v) - } - - if b.Options.Method != "" { - req.Method = strings.ToUpper(b.Options.Method) - } - - return req -} - -// HealthCheck struct. -type HealthCheck struct { - Backends map[string]*BackendConfig - metrics metricsHealthcheck - cancel context.CancelFunc -} - -// SetBackendsConfiguration set backends configuration. -func (hc *HealthCheck) SetBackendsConfiguration(parentCtx context.Context, backends map[string]*BackendConfig) { - hc.Backends = backends - if hc.cancel != nil { - hc.cancel() - } - ctx, cancel := context.WithCancel(parentCtx) - hc.cancel = cancel - - for _, backend := range backends { - currentBackend := backend - safe.Go(func() { - hc.execute(ctx, currentBackend) - }) - } -} - -func (hc *HealthCheck) execute(ctx context.Context, backend *BackendConfig) { +func NewServiceHealthChecker(ctx context.Context, metrics metricsHealthCheck, config *dynamic.ServerHealthCheck, service StatusSetter, info *runtime.ServiceInfo, transport http.RoundTripper, targets map[string]*url.URL) *ServiceHealthChecker { logger := log.FromContext(ctx) - logger.Debugf("Initial health check for backend: %q", backend.name) - hc.checkServersLB(ctx, backend) - - ticker := time.NewTicker(backend.Interval) - defer ticker.Stop() - for { - select { - case <-ctx.Done(): - logger.Debugf("Stopping current health check goroutines of backend: %s", backend.name) - return - case <-ticker.C: - logger.Debugf("Routine health check refresh for backend: %s", backend.name) - hc.checkServersLB(ctx, backend) - } - } -} - -func (hc *HealthCheck) checkServersLB(ctx context.Context, backend *BackendConfig) { - logger := log.FromContext(ctx) - - enabledURLs := backend.LB.Servers() - - var newDisabledURLs []backendURL - for _, disabledURL := range backend.disabledURLs { - serverUpMetricValue := float64(0) - - if err := checkHealth(disabledURL.url, backend); err == nil { - logger.Warnf("Health check up: returning to server list. Backend: %q URL: %q Weight: %d", - backend.name, disabledURL.url.String(), disabledURL.weight) - if err = backend.LB.UpsertServer(disabledURL.url, roundrobin.Weight(disabledURL.weight)); err != nil { - logger.Error(err) - } - serverUpMetricValue = 1 - } else { - logger.Warnf("Health check still failing. Backend: %q URL: %q Reason: %s", backend.name, disabledURL.url.String(), err) - newDisabledURLs = append(newDisabledURLs, disabledURL) - } - - labelValues := []string{"service", backend.name, "url", disabledURL.url.String()} - hc.metrics.serverUpGauge.With(labelValues...).Set(serverUpMetricValue) + interval := time.Duration(config.Interval) + if interval <= 0 { + logger.Error("Health check interval smaller than zero") + interval = time.Duration(dynamic.DefaultHealthCheckInterval) } - backend.disabledURLs = newDisabledURLs - - for _, enabledURL := range enabledURLs { - serverUpMetricValue := float64(1) - - if err := checkHealth(enabledURL, backend); err != nil { - weight := 1 - rr, ok := backend.LB.(*roundrobin.RoundRobin) - if ok { - var gotWeight bool - weight, gotWeight = rr.ServerWeight(enabledURL) - if !gotWeight { - weight = 1 - } - } - - logger.Warnf("Health check failed, removing from server list. Backend: %q URL: %q Weight: %d Reason: %s", - backend.name, enabledURL.String(), weight, err) - if err := backend.LB.RemoveServer(enabledURL); err != nil { - logger.Error(err) - } - - backend.disabledURLs = append(backend.disabledURLs, backendURL{enabledURL, weight}) - serverUpMetricValue = 0 - } - - labelValues := []string{"service", backend.name, "url", enabledURL.String()} - hc.metrics.serverUpGauge.With(labelValues...).Set(serverUpMetricValue) - } -} - -// GetHealthCheck returns the health check which is guaranteed to be a singleton. -func GetHealthCheck(registry metrics.Registry) *HealthCheck { - once.Do(func() { - singleton = newHealthCheck(registry) - }) - return singleton -} - -func newHealthCheck(registry metrics.Registry) *HealthCheck { - return &HealthCheck{ - Backends: make(map[string]*BackendConfig), - metrics: metricsHealthcheck{ - serverUpGauge: registry.ServiceServerUpGauge(), - }, - } -} - -// NewBackendConfig Instantiate a new BackendConfig. -func NewBackendConfig(options Options, backendName string) *BackendConfig { - return &BackendConfig{ - Options: options, - name: backendName, - } -} - -// checkHealth calls the proper health check function depending on the -// backend config mode, defaults to HTTP. -func checkHealth(serverURL *url.URL, backend *BackendConfig) error { - if backend.Options.Mode == GRPCMode { - return checkHealthGRPC(serverURL, backend) - } - return checkHealthHTTP(serverURL, backend) -} - -// checkHealthHTTP returns an error with a meaningful description if the health check failed. -// Dedicated to HTTP servers. -func checkHealthHTTP(serverURL *url.URL, backend *BackendConfig) error { - req, err := backend.newRequest(serverURL) - if err != nil { - return fmt.Errorf("failed to create HTTP request: %w", err) + timeout := time.Duration(config.Timeout) + if timeout <= 0 { + logger.Error("Health check timeout smaller than zero") + timeout = time.Duration(dynamic.DefaultHealthCheckTimeout) } - req = backend.setRequestOptions(req) - - client := http.Client{ - Timeout: backend.Options.Timeout, - Transport: backend.Options.Transport, + if timeout >= interval { + logger.Warnf("Health check timeout should be lower than the health check interval. Interval set to timeout + 1 second (%s).", interval) + interval = timeout + time.Second } - if !backend.FollowRedirects { + client := &http.Client{ + Transport: transport, + } + + if config.FollowRedirects != nil && !*config.FollowRedirects { client.CheckRedirect = func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse } } - resp, err := client.Do(req) + return &ServiceHealthChecker{ + balancer: service, + info: info, + config: config, + interval: interval, + timeout: timeout, + targets: targets, + client: client, + metrics: metrics, + } +} + +func (shc *ServiceHealthChecker) Launch(ctx context.Context) { + ticker := time.NewTicker(shc.interval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + + case <-ticker.C: + for proxyName, target := range shc.targets { + select { + case <-ctx.Done(): + return + default: + } + + up := true + serverUpMetricValue := float64(1) + + if err := shc.executeHealthCheck(ctx, shc.config, target); err != nil { + // The context is canceled when the dynamic configuration is refreshed. + if errors.Is(err, context.Canceled) { + return + } + + log.FromContext(ctx). + WithField("targetURL", target.String()). + WithError(err). + Warn("Health check failed.") + + up = false + serverUpMetricValue = float64(0) + } + + shc.balancer.SetStatus(ctx, proxyName, up) + + statusStr := runtime.StatusDown + if up { + statusStr = runtime.StatusUp + } + + shc.info.UpdateServerStatus(target.String(), statusStr) + + shc.metrics.ServiceServerUpGauge(). + With("service", proxyName). + With("url", target.String()). + Set(serverUpMetricValue) + } + } + } +} + +func (shc *ServiceHealthChecker) executeHealthCheck(ctx context.Context, config *dynamic.ServerHealthCheck, target *url.URL) error { + ctx, cancel := context.WithDeadline(ctx, time.Now().Add(shc.timeout)) + defer cancel() + + if config.Mode == modeGRPC { + return shc.checkHealthGRPC(ctx, target) + } + return shc.checkHealthHTTP(ctx, target) +} + +// checkHealthHTTP returns an error with a meaningful description if the health check failed. +// Dedicated to HTTP servers. +func (shc *ServiceHealthChecker) checkHealthHTTP(ctx context.Context, target *url.URL) error { + req, err := shc.newRequest(ctx, target) + if err != nil { + return fmt.Errorf("create HTTP request: %w", err) + } + + resp, err := shc.client.Do(req) if err != nil { return fmt.Errorf("HTTP request failed: %w", err) } @@ -300,34 +181,61 @@ func checkHealthHTTP(serverURL *url.URL, backend *BackendConfig) error { return nil } +func (shc *ServiceHealthChecker) newRequest(ctx context.Context, target *url.URL) (*http.Request, error) { + u, err := target.Parse(shc.config.Path) + if err != nil { + return nil, err + } + + if len(shc.config.Scheme) > 0 { + u.Scheme = shc.config.Scheme + } + + if shc.config.Port != 0 { + u.Host = net.JoinHostPort(u.Hostname(), strconv.Itoa(shc.config.Port)) + } + + req, err := http.NewRequestWithContext(ctx, shc.config.Method, u.String(), http.NoBody) + if err != nil { + return nil, fmt.Errorf("failed to create HTTP request: %w", err) + } + + if shc.config.Hostname != "" { + req.Host = shc.config.Hostname + } + + for k, v := range shc.config.Headers { + req.Header.Set(k, v) + } + + return req, nil +} + // checkHealthGRPC returns an error with a meaningful description if the health check failed. // Dedicated to gRPC servers implementing gRPC Health Checking Protocol v1. -func checkHealthGRPC(serverURL *url.URL, backend *BackendConfig) error { - u, err := serverURL.Parse(backend.Path) +func (shc *ServiceHealthChecker) checkHealthGRPC(ctx context.Context, serverURL *url.URL) error { + u, err := serverURL.Parse(shc.config.Path) if err != nil { return fmt.Errorf("failed to parse server URL: %w", err) } port := u.Port() - if backend.Options.Port != 0 { - port = strconv.Itoa(backend.Options.Port) + if shc.config.Port != 0 { + port = strconv.Itoa(shc.config.Port) } serverAddr := net.JoinHostPort(u.Hostname(), port) var opts []grpc.DialOption - switch backend.Options.Scheme { + switch shc.config.Scheme { case "http", "h2c", "": opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) } - ctx, cancel := context.WithTimeout(context.Background(), backend.Options.Timeout) - defer cancel() - conn, err := grpc.DialContext(ctx, serverAddr, opts...) if err != nil { if errors.Is(err, context.DeadlineExceeded) { - return fmt.Errorf("fail to connect to %s within %s: %w", serverAddr, backend.Options.Timeout, err) + return fmt.Errorf("fail to connect to %s within %s: %w", serverAddr, shc.config.Timeout, err) } return fmt.Errorf("fail to connect to %s: %w", serverAddr, err) } @@ -341,6 +249,8 @@ func checkHealthGRPC(serverURL *url.URL, backend *BackendConfig) error { return fmt.Errorf("gRPC server does not implement the health protocol: %w", err) case codes.DeadlineExceeded: return fmt.Errorf("gRPC health check timeout: %w", err) + case codes.Canceled: + return context.Canceled } } @@ -353,155 +263,3 @@ func checkHealthGRPC(serverURL *url.URL, backend *BackendConfig) error { return nil } - -// StatusUpdater should be implemented by a service that, when its status -// changes (e.g. all if its children are down), needs to propagate upwards (to -// their parent(s)) that change. -type StatusUpdater interface { - RegisterStatusUpdater(fn func(up bool)) error -} - -// NewLBStatusUpdater returns a new LbStatusUpdater. -func NewLBStatusUpdater(bh BalancerHandler, info *runtime.ServiceInfo, hc *dynamic.ServerHealthCheck) *LbStatusUpdater { - return &LbStatusUpdater{ - BalancerHandler: bh, - serviceInfo: info, - wantsHealthCheck: hc != nil, - } -} - -// LbStatusUpdater wraps a BalancerHandler and a ServiceInfo, -// so it can keep track of the status of a server in the ServiceInfo. -type LbStatusUpdater struct { - BalancerHandler - serviceInfo *runtime.ServiceInfo // can be nil - updaters []func(up bool) - wantsHealthCheck bool -} - -// RegisterStatusUpdater adds fn to the list of hooks that are run when the -// status of the Balancer changes. -// Not thread safe. -func (lb *LbStatusUpdater) RegisterStatusUpdater(fn func(up bool)) error { - if !lb.wantsHealthCheck { - return errors.New("healthCheck not enabled in config for this loadbalancer service") - } - - lb.updaters = append(lb.updaters, fn) - return nil -} - -// RemoveServer removes the given server from the BalancerHandler, -// and updates the status of the server to "DOWN". -func (lb *LbStatusUpdater) RemoveServer(u *url.URL) error { - // TODO(mpl): when we have the freedom to change the signature of RemoveServer - // (kinda stuck because of oxy for now), let's pass around a context to improve - // logging. - ctx := context.TODO() - upBefore := len(lb.BalancerHandler.Servers()) > 0 - err := lb.BalancerHandler.RemoveServer(u) - if err != nil { - return err - } - if lb.serviceInfo != nil { - lb.serviceInfo.UpdateServerStatus(u.String(), serverDown) - } - log.FromContext(ctx).Debugf("child %s now %s", u.String(), serverDown) - - if !upBefore { - // we were already down, and we still are, no need to propagate. - log.FromContext(ctx).Debugf("Still %s, no need to propagate", serverDown) - return nil - } - if len(lb.BalancerHandler.Servers()) > 0 { - // we were up, and we still are, no need to propagate - log.FromContext(ctx).Debugf("Still %s, no need to propagate", serverUp) - return nil - } - - log.FromContext(ctx).Debugf("Propagating new %s status", serverDown) - for _, fn := range lb.updaters { - fn(false) - } - return nil -} - -// UpsertServer adds the given server to the BalancerHandler, -// and updates the status of the server to "UP". -func (lb *LbStatusUpdater) UpsertServer(u *url.URL, options ...roundrobin.ServerOption) error { - ctx := context.TODO() - upBefore := len(lb.BalancerHandler.Servers()) > 0 - err := lb.BalancerHandler.UpsertServer(u, options...) - if err != nil { - return err - } - if lb.serviceInfo != nil { - lb.serviceInfo.UpdateServerStatus(u.String(), serverUp) - } - log.FromContext(ctx).Debugf("child %s now %s", u.String(), serverUp) - - if upBefore { - // we were up, and we still are, no need to propagate - log.FromContext(ctx).Debugf("Still %s, no need to propagate", serverUp) - return nil - } - - log.FromContext(ctx).Debugf("Propagating new %s status", serverUp) - for _, fn := range lb.updaters { - fn(true) - } - return nil -} - -// Balancers is a list of Balancers(s) that implements the Balancer interface. -type Balancers []Balancer - -// Servers returns the deduplicated server URLs from all the Balancer. -// Note that the deduplication is only possible because all the underlying -// balancers are of the same kind (the oxy implementation). -// The comparison property is the same as the one found at: -// https://github.com/vulcand/oxy/blob/fb2728c857b7973a27f8de2f2190729c0f22cf49/roundrobin/rr.go#L347. -func (b Balancers) Servers() []*url.URL { - seen := make(map[string]struct{}) - - var servers []*url.URL - for _, lb := range b { - for _, server := range lb.Servers() { - key := serverKey(server) - if _, ok := seen[key]; ok { - continue - } - - servers = append(servers, server) - seen[key] = struct{}{} - } - } - - return servers -} - -// RemoveServer removes the given server from all the Balancer, -// and updates the status of the server to "DOWN". -func (b Balancers) RemoveServer(u *url.URL) error { - for _, lb := range b { - if err := lb.RemoveServer(u); err != nil { - return err - } - } - return nil -} - -// UpsertServer adds the given server to all the Balancer, -// and updates the status of the server to "UP". -func (b Balancers) UpsertServer(u *url.URL, options ...roundrobin.ServerOption) error { - for _, lb := range b { - if err := lb.UpsertServer(u, options...); err != nil { - return err - } - } - return nil -} - -func serverKey(u *url.URL) string { - return u.Path + u.Host + u.Scheme -} diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go index 43941033a..b1e6d4f11 100644 --- a/pkg/healthcheck/healthcheck_test.go +++ b/pkg/healthcheck/healthcheck_test.go @@ -11,127 +11,324 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + ptypes "github.com/traefik/paerser/types" + "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/config/runtime" "github.com/traefik/traefik/v2/pkg/testhelpers" - "github.com/vulcand/oxy/roundrobin" healthpb "google.golang.org/grpc/health/grpc_health_v1" ) -const ( - healthCheckInterval = 200 * time.Millisecond - healthCheckTimeout = 100 * time.Millisecond -) - -func TestSetBackendsConfiguration(t *testing.T) { +func TestServiceHealthChecker_newRequest(t *testing.T) { testCases := []struct { - desc string - startHealthy bool - mode string - server StartTestServer - expectedNumRemovedServers int - expectedNumUpsertedServers int - expectedGaugeValue float64 + desc string + targetURL string + config dynamic.ServerHealthCheck + expTarget string + expError bool + expHostname string + expHeader string + expMethod string }{ { - desc: "healthy server staying healthy", - startHealthy: true, - server: newHTTPServer(http.StatusOK), - expectedNumRemovedServers: 0, - expectedNumUpsertedServers: 0, - expectedGaugeValue: 1, + desc: "no port override", + targetURL: "http://backend1:80", + config: dynamic.ServerHealthCheck{ + Path: "/test", + Port: 0, + }, + expError: false, + expTarget: "http://backend1:80/test", + expHostname: "backend1:80", + expMethod: http.MethodGet, }, { - desc: "healthy server staying healthy (StatusNoContent)", - startHealthy: true, - server: newHTTPServer(http.StatusNoContent), - expectedNumRemovedServers: 0, - expectedNumUpsertedServers: 0, - expectedGaugeValue: 1, + desc: "port override", + targetURL: "http://backend2:80", + config: dynamic.ServerHealthCheck{ + Path: "/test", + Port: 8080, + }, + expError: false, + expTarget: "http://backend2:8080/test", + expHostname: "backend2:8080", + expMethod: http.MethodGet, }, { - desc: "healthy server staying healthy (StatusPermanentRedirect)", - startHealthy: true, - server: newHTTPServer(http.StatusPermanentRedirect), - expectedNumRemovedServers: 0, - expectedNumUpsertedServers: 0, - expectedGaugeValue: 1, + desc: "no port override with no port in server URL", + targetURL: "http://backend1", + config: dynamic.ServerHealthCheck{ + Path: "/health", + Port: 0, + }, + expError: false, + expTarget: "http://backend1/health", + expHostname: "backend1", + expMethod: http.MethodGet, }, { - desc: "healthy server becoming sick", - startHealthy: true, - server: newHTTPServer(http.StatusServiceUnavailable), - expectedNumRemovedServers: 1, - expectedNumUpsertedServers: 0, - expectedGaugeValue: 0, + desc: "port override with no port in server URL", + targetURL: "http://backend2", + config: dynamic.ServerHealthCheck{ + Path: "/health", + Port: 8080, + }, + expError: false, + expTarget: "http://backend2:8080/health", + expHostname: "backend2:8080", + expMethod: http.MethodGet, }, { - desc: "sick server becoming healthy", - startHealthy: false, - server: newHTTPServer(http.StatusOK), - expectedNumRemovedServers: 0, - expectedNumUpsertedServers: 1, - expectedGaugeValue: 1, + desc: "scheme override", + targetURL: "https://backend1:80", + config: dynamic.ServerHealthCheck{ + Scheme: "http", + Path: "/test", + Port: 0, + }, + expError: false, + expTarget: "http://backend1:80/test", + expHostname: "backend1:80", + expMethod: http.MethodGet, }, { - desc: "sick server staying sick", - startHealthy: false, - server: newHTTPServer(http.StatusServiceUnavailable), - expectedNumRemovedServers: 0, - expectedNumUpsertedServers: 0, - expectedGaugeValue: 0, + desc: "path with param", + targetURL: "http://backend1:80", + config: dynamic.ServerHealthCheck{ + Path: "/health?powpow=do", + Port: 0, + }, + expError: false, + expTarget: "http://backend1:80/health?powpow=do", + expHostname: "backend1:80", + expMethod: http.MethodGet, }, { - desc: "healthy server toggling to sick and back to healthy", - startHealthy: true, - server: newHTTPServer(http.StatusServiceUnavailable, http.StatusOK), - expectedNumRemovedServers: 1, - expectedNumUpsertedServers: 1, - expectedGaugeValue: 1, + desc: "path with params", + targetURL: "http://backend1:80", + config: dynamic.ServerHealthCheck{ + Path: "/health?powpow=do&do=powpow", + Port: 0, + }, + expError: false, + expTarget: "http://backend1:80/health?powpow=do&do=powpow", + expHostname: "backend1:80", + expMethod: http.MethodGet, }, { - desc: "healthy grpc server staying healthy", - mode: "grpc", - startHealthy: true, - server: newGRPCServer(healthpb.HealthCheckResponse_SERVING), - expectedNumRemovedServers: 0, - expectedNumUpsertedServers: 0, - expectedGaugeValue: 1, + desc: "path with invalid path", + targetURL: "http://backend1:80", + config: dynamic.ServerHealthCheck{ + Path: ":", + Port: 0, + }, + expError: true, + expTarget: "", + expHostname: "backend1:80", + expMethod: http.MethodGet, }, { - desc: "healthy grpc server becoming sick", - mode: "grpc", - startHealthy: true, - server: newGRPCServer(healthpb.HealthCheckResponse_NOT_SERVING), - expectedNumRemovedServers: 1, - expectedNumUpsertedServers: 0, - expectedGaugeValue: 0, + desc: "override hostname", + targetURL: "http://backend1:80", + config: dynamic.ServerHealthCheck{ + Hostname: "myhost", + Path: "/", + }, + expTarget: "http://backend1:80/", + expHostname: "myhost", + expHeader: "", + expMethod: http.MethodGet, }, { - desc: "sick grpc server becoming healthy", - mode: "grpc", - startHealthy: false, - server: newGRPCServer(healthpb.HealthCheckResponse_SERVING), - expectedNumRemovedServers: 0, - expectedNumUpsertedServers: 1, - expectedGaugeValue: 1, + desc: "not override hostname", + targetURL: "http://backend1:80", + config: dynamic.ServerHealthCheck{ + Hostname: "", + Path: "/", + }, + expTarget: "http://backend1:80/", + expHostname: "backend1:80", + expHeader: "", + expMethod: http.MethodGet, }, { - desc: "sick grpc server staying sick", - mode: "grpc", - startHealthy: false, - server: newGRPCServer(healthpb.HealthCheckResponse_NOT_SERVING), - expectedNumRemovedServers: 0, - expectedNumUpsertedServers: 0, - expectedGaugeValue: 0, + desc: "custom header", + targetURL: "http://backend1:80", + config: dynamic.ServerHealthCheck{ + Headers: map[string]string{"Custom-Header": "foo"}, + Hostname: "", + Path: "/", + }, + expTarget: "http://backend1:80/", + expHostname: "backend1:80", + expHeader: "foo", + expMethod: http.MethodGet, }, { - desc: "healthy grpc server toggling to sick and back to healthy", - mode: "grpc", - startHealthy: true, - server: newGRPCServer(healthpb.HealthCheckResponse_NOT_SERVING, healthpb.HealthCheckResponse_SERVING), - expectedNumRemovedServers: 1, - expectedNumUpsertedServers: 1, - expectedGaugeValue: 1, + desc: "custom header with hostname override", + targetURL: "http://backend1:80", + config: dynamic.ServerHealthCheck{ + Headers: map[string]string{"Custom-Header": "foo"}, + Hostname: "myhost", + Path: "/", + }, + expTarget: "http://backend1:80/", + expHostname: "myhost", + expHeader: "foo", + expMethod: http.MethodGet, + }, + { + desc: "custom method", + targetURL: "http://backend1:80", + config: dynamic.ServerHealthCheck{ + Path: "/", + Method: http.MethodHead, + }, + expTarget: "http://backend1:80/", + expHostname: "backend1:80", + expMethod: http.MethodHead, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + shc := ServiceHealthChecker{config: &test.config} + + u := testhelpers.MustParseURL(test.targetURL) + req, err := shc.newRequest(context.Background(), u) + + if test.expError { + require.Error(t, err) + assert.Nil(t, req) + } else { + require.NoError(t, err, "failed to create new request") + require.NotNil(t, req) + + assert.Equal(t, test.expTarget, req.URL.String()) + assert.Equal(t, test.expHeader, req.Header.Get("Custom-Header")) + assert.Equal(t, test.expHostname, req.Host) + assert.Equal(t, test.expMethod, req.Method) + } + }) + } +} + +func TestServiceHealthChecker_checkHealthHTTP_NotFollowingRedirects(t *testing.T) { + redirectServerCalled := false + redirectTestServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + redirectServerCalled = true + })) + defer redirectTestServer.Close() + + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(dynamic.DefaultHealthCheckTimeout)) + defer cancel() + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Header().Add("location", redirectTestServer.URL) + rw.WriteHeader(http.StatusSeeOther) + })) + defer server.Close() + + config := &dynamic.ServerHealthCheck{ + Path: "/path", + FollowRedirects: Bool(false), + Interval: dynamic.DefaultHealthCheckInterval, + Timeout: dynamic.DefaultHealthCheckTimeout, + } + healthChecker := NewServiceHealthChecker(ctx, nil, config, nil, nil, http.DefaultTransport, nil) + + err := healthChecker.checkHealthHTTP(ctx, testhelpers.MustParseURL(server.URL)) + require.NoError(t, err) + + assert.False(t, redirectServerCalled, "HTTP redirect must not be followed") +} + +func TestServiceHealthChecker_Launch(t *testing.T) { + testCases := []struct { + desc string + mode string + server StartTestServer + expNumRemovedServers int + expNumUpsertedServers int + expGaugeValue float64 + targetStatus string + }{ + { + desc: "healthy server staying healthy", + server: newHTTPServer(http.StatusOK), + expNumRemovedServers: 0, + expNumUpsertedServers: 1, + expGaugeValue: 1, + targetStatus: runtime.StatusUp, + }, + { + desc: "healthy server staying healthy (StatusNoContent)", + server: newHTTPServer(http.StatusNoContent), + expNumRemovedServers: 0, + expNumUpsertedServers: 1, + expGaugeValue: 1, + targetStatus: runtime.StatusUp, + }, + { + desc: "healthy server staying healthy (StatusPermanentRedirect)", + server: newHTTPServer(http.StatusPermanentRedirect), + expNumRemovedServers: 0, + expNumUpsertedServers: 1, + expGaugeValue: 1, + targetStatus: runtime.StatusUp, + }, + { + desc: "healthy server becoming sick", + server: newHTTPServer(http.StatusServiceUnavailable), + expNumRemovedServers: 1, + expNumUpsertedServers: 0, + expGaugeValue: 0, + targetStatus: runtime.StatusDown, + }, + { + desc: "healthy server toggling to sick and back to healthy", + server: newHTTPServer(http.StatusServiceUnavailable, http.StatusOK), + expNumRemovedServers: 1, + expNumUpsertedServers: 1, + expGaugeValue: 1, + targetStatus: runtime.StatusUp, + }, + { + desc: "healthy server toggling to healthy and go to sick", + server: newHTTPServer(http.StatusOK, http.StatusServiceUnavailable), + expNumRemovedServers: 1, + expNumUpsertedServers: 1, + expGaugeValue: 0, + targetStatus: runtime.StatusDown, + }, + { + desc: "healthy grpc server staying healthy", + mode: "grpc", + server: newGRPCServer(healthpb.HealthCheckResponse_SERVING), + expNumRemovedServers: 0, + expNumUpsertedServers: 1, + expGaugeValue: 1, + targetStatus: runtime.StatusUp, + }, + { + desc: "healthy grpc server becoming sick", + mode: "grpc", + server: newGRPCServer(healthpb.HealthCheckResponse_NOT_SERVING), + expNumRemovedServers: 1, + expNumUpsertedServers: 0, + expGaugeValue: 0, + targetStatus: runtime.StatusDown, + }, + { + desc: "healthy grpc server toggling to sick and back to healthy", + mode: "grpc", + server: newGRPCServer(healthpb.HealthCheckResponse_NOT_SERVING, healthpb.HealthCheckResponse_SERVING), + expNumRemovedServers: 1, + expNumUpsertedServers: 1, + expGaugeValue: 1, + targetStatus: runtime.StatusUp, }, } @@ -145,37 +342,26 @@ func TestSetBackendsConfiguration(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - serverURL, timeout := test.server.Start(t, cancel) + targetURL, timeout := test.server.Start(t, cancel) lb := &testLoadBalancer{RWMutex: &sync.RWMutex{}} - options := Options{ + config := &dynamic.ServerHealthCheck{ Mode: test.mode, Path: "/path", - Interval: healthCheckInterval, - Timeout: healthCheckTimeout, - LB: lb, - } - backend := NewBackendConfig(options, "backendName") - - if test.startHealthy { - lb.servers = append(lb.servers, serverURL) - } else { - backend.disabledURLs = append(backend.disabledURLs, backendURL{url: serverURL, weight: 1}) + Interval: ptypes.Duration(500 * time.Millisecond), + Timeout: ptypes.Duration(499 * time.Millisecond), } - collectingMetrics := &testhelpers.CollectingGauge{} - - check := HealthCheck{ - Backends: make(map[string]*BackendConfig), - metrics: metricsHealthcheck{serverUpGauge: collectingMetrics}, - } + gauge := &testhelpers.CollectingGauge{} + serviceInfo := &runtime.ServiceInfo{} + hc := NewServiceHealthChecker(ctx, &MetricsMock{gauge}, config, lb, serviceInfo, http.DefaultTransport, map[string]*url.URL{"test": targetURL}) wg := sync.WaitGroup{} wg.Add(1) go func() { - check.execute(ctx, backend) + hc.Launch(ctx) wg.Done() }() @@ -189,392 +375,14 @@ func TestSetBackendsConfiguration(t *testing.T) { lb.Lock() defer lb.Unlock() - assert.Equal(t, test.expectedNumRemovedServers, lb.numRemovedServers, "removed servers") - assert.Equal(t, test.expectedNumUpsertedServers, lb.numUpsertedServers, "upserted servers") - assert.Equal(t, test.expectedGaugeValue, collectingMetrics.GaugeValue, "ServerUp Gauge") + assert.Equal(t, test.expNumRemovedServers, lb.numRemovedServers, "removed servers") + assert.Equal(t, test.expNumUpsertedServers, lb.numUpsertedServers, "upserted servers") + assert.Equal(t, test.expGaugeValue, gauge.GaugeValue, "ServerUp Gauge") + assert.Equal(t, serviceInfo.GetAllStatus(), map[string]string{targetURL.String(): test.targetStatus}) }) } } -func TestNewRequest(t *testing.T) { - type expected struct { - err bool - value string - } - - testCases := []struct { - desc string - serverURL string - options Options - expected expected - }{ - { - desc: "no port override", - serverURL: "http://backend1:80", - options: Options{ - Path: "/test", - Port: 0, - }, - expected: expected{ - err: false, - value: "http://backend1:80/test", - }, - }, - { - desc: "port override", - serverURL: "http://backend2:80", - options: Options{ - Path: "/test", - Port: 8080, - }, - expected: expected{ - err: false, - value: "http://backend2:8080/test", - }, - }, - { - desc: "no port override with no port in server URL", - serverURL: "http://backend1", - options: Options{ - Path: "/health", - Port: 0, - }, - expected: expected{ - err: false, - value: "http://backend1/health", - }, - }, - { - desc: "port override with no port in server URL", - serverURL: "http://backend2", - options: Options{ - Path: "/health", - Port: 8080, - }, - expected: expected{ - err: false, - value: "http://backend2:8080/health", - }, - }, - { - desc: "scheme override", - serverURL: "https://backend1:80", - options: Options{ - Scheme: "http", - Path: "/test", - Port: 0, - }, - expected: expected{ - err: false, - value: "http://backend1:80/test", - }, - }, - { - desc: "path with param", - serverURL: "http://backend1:80", - options: Options{ - Path: "/health?powpow=do", - Port: 0, - }, - expected: expected{ - err: false, - value: "http://backend1:80/health?powpow=do", - }, - }, - { - desc: "path with params", - serverURL: "http://backend1:80", - options: Options{ - Path: "/health?powpow=do&do=powpow", - Port: 0, - }, - expected: expected{ - err: false, - value: "http://backend1:80/health?powpow=do&do=powpow", - }, - }, - { - desc: "path with invalid path", - serverURL: "http://backend1:80", - options: Options{ - Path: ":", - Port: 0, - }, - expected: expected{ - err: true, - value: "", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - backend := NewBackendConfig(test.options, "backendName") - - u := testhelpers.MustParseURL(test.serverURL) - - req, err := backend.newRequest(u) - - if test.expected.err { - require.Error(t, err) - assert.Nil(t, nil) - } else { - require.NoError(t, err, "failed to create new backend request") - require.NotNil(t, req) - assert.Equal(t, test.expected.value, req.URL.String()) - } - }) - } -} - -func TestRequestOptions(t *testing.T) { - testCases := []struct { - desc string - serverURL string - options Options - expectedHostname string - expectedHeader string - expectedMethod string - }{ - { - desc: "override hostname", - serverURL: "http://backend1:80", - options: Options{ - Hostname: "myhost", - Path: "/", - }, - expectedHostname: "myhost", - expectedHeader: "", - expectedMethod: http.MethodGet, - }, - { - desc: "not override hostname", - serverURL: "http://backend1:80", - options: Options{ - Hostname: "", - Path: "/", - }, - expectedHostname: "backend1:80", - expectedHeader: "", - expectedMethod: http.MethodGet, - }, - { - desc: "custom header", - serverURL: "http://backend1:80", - options: Options{ - Headers: map[string]string{"Custom-Header": "foo"}, - Hostname: "", - Path: "/", - }, - expectedHostname: "backend1:80", - expectedHeader: "foo", - expectedMethod: http.MethodGet, - }, - { - desc: "custom header with hostname override", - serverURL: "http://backend1:80", - options: Options{ - Headers: map[string]string{"Custom-Header": "foo"}, - Hostname: "myhost", - Path: "/", - }, - expectedHostname: "myhost", - expectedHeader: "foo", - expectedMethod: http.MethodGet, - }, - { - desc: "custom method", - serverURL: "http://backend1:80", - options: Options{ - Path: "/", - Method: http.MethodHead, - }, - expectedHostname: "backend1:80", - expectedMethod: http.MethodHead, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - backend := NewBackendConfig(test.options, "backendName") - - u, err := url.Parse(test.serverURL) - require.NoError(t, err) - - req, err := backend.newRequest(u) - require.NoError(t, err, "failed to create new backend request") - - req = backend.setRequestOptions(req) - - assert.Equal(t, "http://backend1:80/", req.URL.String()) - assert.Equal(t, test.expectedHostname, req.Host) - assert.Equal(t, test.expectedHeader, req.Header.Get("Custom-Header")) - assert.Equal(t, test.expectedMethod, req.Method) - }) - } -} - -func TestBalancers_Servers(t *testing.T) { - server1, err := url.Parse("http://foo.com") - require.NoError(t, err) - - balancer1, err := roundrobin.New(nil) - require.NoError(t, err) - - err = balancer1.UpsertServer(server1) - require.NoError(t, err) - - server2, err := url.Parse("http://foo.com") - require.NoError(t, err) - - balancer2, err := roundrobin.New(nil) - require.NoError(t, err) - - err = balancer2.UpsertServer(server2) - require.NoError(t, err) - - balancers := Balancers([]Balancer{balancer1, balancer2}) - - want, err := url.Parse("http://foo.com") - require.NoError(t, err) - - assert.Equal(t, 1, len(balancers.Servers())) - assert.Equal(t, want, balancers.Servers()[0]) -} - -func TestBalancers_UpsertServer(t *testing.T) { - balancer1, err := roundrobin.New(nil) - require.NoError(t, err) - - balancer2, err := roundrobin.New(nil) - require.NoError(t, err) - - want, err := url.Parse("http://foo.com") - require.NoError(t, err) - - balancers := Balancers([]Balancer{balancer1, balancer2}) - - err = balancers.UpsertServer(want) - require.NoError(t, err) - - assert.Equal(t, 1, len(balancer1.Servers())) - assert.Equal(t, want, balancer1.Servers()[0]) - - assert.Equal(t, 1, len(balancer2.Servers())) - assert.Equal(t, want, balancer2.Servers()[0]) -} - -func TestBalancers_RemoveServer(t *testing.T) { - server, err := url.Parse("http://foo.com") - require.NoError(t, err) - - balancer1, err := roundrobin.New(nil) - require.NoError(t, err) - - err = balancer1.UpsertServer(server) - require.NoError(t, err) - - balancer2, err := roundrobin.New(nil) - require.NoError(t, err) - - err = balancer2.UpsertServer(server) - require.NoError(t, err) - - balancers := Balancers([]Balancer{balancer1, balancer2}) - - err = balancers.RemoveServer(server) - require.NoError(t, err) - - assert.Equal(t, 0, len(balancer1.Servers())) - assert.Equal(t, 0, len(balancer2.Servers())) -} - -func TestLBStatusUpdater(t *testing.T) { - lb := &testLoadBalancer{RWMutex: &sync.RWMutex{}} - svInfo := &runtime.ServiceInfo{} - lbsu := NewLBStatusUpdater(lb, svInfo, nil) - newServer, err := url.Parse("http://foo.com") - assert.NoError(t, err) - err = lbsu.UpsertServer(newServer, roundrobin.Weight(1)) - assert.NoError(t, err) - assert.Equal(t, len(lbsu.Servers()), 1) - assert.Equal(t, len(lbsu.BalancerHandler.(*testLoadBalancer).Options()), 1) - statuses := svInfo.GetAllStatus() - assert.Equal(t, len(statuses), 1) - for k, v := range statuses { - assert.Equal(t, k, newServer.String()) - assert.Equal(t, v, serverUp) - break - } - err = lbsu.RemoveServer(newServer) - assert.NoError(t, err) - assert.Equal(t, len(lbsu.Servers()), 0) - statuses = svInfo.GetAllStatus() - assert.Equal(t, len(statuses), 1) - for k, v := range statuses { - assert.Equal(t, k, newServer.String()) - assert.Equal(t, v, serverDown) - break - } -} - -func TestNotFollowingRedirects(t *testing.T) { - redirectServerCalled := false - redirectTestServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - redirectServerCalled = true - })) - defer redirectTestServer.Close() - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.Header().Add("location", redirectTestServer.URL) - rw.WriteHeader(http.StatusSeeOther) - cancel() - })) - defer server.Close() - - lb := &testLoadBalancer{ - RWMutex: &sync.RWMutex{}, - servers: []*url.URL{testhelpers.MustParseURL(server.URL)}, - } - - backend := NewBackendConfig(Options{ - Path: "/path", - Interval: healthCheckInterval, - Timeout: healthCheckTimeout, - LB: lb, - FollowRedirects: false, - }, "backendName") - - collectingMetrics := &testhelpers.CollectingGauge{} - check := HealthCheck{ - Backends: make(map[string]*BackendConfig), - metrics: metricsHealthcheck{serverUpGauge: collectingMetrics}, - } - - wg := sync.WaitGroup{} - wg.Add(1) - - go func() { - check.execute(ctx, backend) - wg.Done() - }() - - timeout := time.Duration(int(healthCheckInterval) + 500) - select { - case <-time.After(timeout): - t.Fatal("test did not complete in time") - case <-ctx.Done(): - wg.Wait() - } - - assert.False(t, redirectServerCalled, "HTTP redirect must not be followed") +func Bool(b bool) *bool { + return &b } diff --git a/pkg/healthcheck/mock_test.go b/pkg/healthcheck/mock_test.go index 19e60b15c..5192fe53e 100644 --- a/pkg/healthcheck/mock_test.go +++ b/pkg/healthcheck/mock_test.go @@ -10,9 +10,10 @@ import ( "testing" "time" + gokitmetrics "github.com/go-kit/kit/metrics" "github.com/stretchr/testify/assert" + "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/testhelpers" - "github.com/vulcand/oxy/roundrobin" "google.golang.org/grpc" healthpb "google.golang.org/grpc/health/grpc_health_v1" ) @@ -64,10 +65,13 @@ func newGRPCServer(healthSequence ...healthpb.HealthCheckResponse_ServingStatus) } func (s *GRPCServer) Check(_ context.Context, _ *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) { - stat := s.status.Pop() if s.status.IsEmpty() { s.done() + return &healthpb.HealthCheckResponse{ + Status: healthpb.HealthCheckResponse_SERVICE_UNKNOWN, + }, nil } + stat := s.status.Pop() return &healthpb.HealthCheckResponse{ Status: stat, @@ -75,10 +79,13 @@ func (s *GRPCServer) Check(_ context.Context, _ *healthpb.HealthCheckRequest) (* } func (s *GRPCServer) Watch(_ *healthpb.HealthCheckRequest, server healthpb.Health_WatchServer) error { - stat := s.status.Pop() if s.status.IsEmpty() { s.done() + return server.Send(&healthpb.HealthCheckResponse{ + Status: healthpb.HealthCheckResponse_SERVICE_UNKNOWN, + }) } + stat := s.status.Pop() return server.Send(&healthpb.HealthCheckResponse{ Status: stat, @@ -105,7 +112,7 @@ func (s *GRPCServer) Start(t *testing.T, done func()) (*url.URL, time.Duration) }() // Make test timeout dependent on number of expected requests, health check interval, and a safety margin. - return testhelpers.MustParseURL("http://" + listener.Addr().String()), time.Duration(len(s.status.sequence)*int(healthCheckInterval) + 500) + return testhelpers.MustParseURL("http://" + listener.Addr().String()), time.Duration(len(s.status.sequence)*int(dynamic.DefaultHealthCheckInterval) + 500) } type HTTPServer struct { @@ -126,13 +133,14 @@ func newHTTPServer(healthSequence ...int) *HTTPServer { // ServeHTTP returns HTTP response codes following a status sequences. // It calls the given 'done' function once all request health indicators have been depleted. func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, _ *http.Request) { + if s.status.IsEmpty() { + s.done() + return + } + stat := s.status.Pop() w.WriteHeader(stat) - - if s.status.IsEmpty() { - s.done() - } } func (s *HTTPServer) Start(t *testing.T, done func()) (*url.URL, time.Duration) { @@ -144,7 +152,7 @@ func (s *HTTPServer) Start(t *testing.T, done func()) (*url.URL, time.Duration) t.Cleanup(ts.Close) // Make test timeout dependent on number of expected requests, health check interval, and a safety margin. - return testhelpers.MustParseURL(ts.URL), time.Duration(len(s.status.sequence)*int(healthCheckInterval) + 500) + return testhelpers.MustParseURL(ts.URL), time.Duration(len(s.status.sequence)*int(dynamic.DefaultHealthCheckInterval) + 500) } type testLoadBalancer struct { @@ -153,53 +161,20 @@ type testLoadBalancer struct { *sync.RWMutex numRemovedServers int numUpsertedServers int - servers []*url.URL - // options is just to make sure that LBStatusUpdater forwards options on Upsert to its BalancerHandler - options []roundrobin.ServerOption } -func (lb *testLoadBalancer) ServeHTTP(w http.ResponseWriter, req *http.Request) { - // noop -} - -func (lb *testLoadBalancer) RemoveServer(u *url.URL) error { - lb.Lock() - defer lb.Unlock() - lb.numRemovedServers++ - lb.removeServer(u) - return nil -} - -func (lb *testLoadBalancer) UpsertServer(u *url.URL, options ...roundrobin.ServerOption) error { - lb.Lock() - defer lb.Unlock() - lb.numUpsertedServers++ - lb.servers = append(lb.servers, u) - lb.options = append(lb.options, options...) - return nil -} - -func (lb *testLoadBalancer) Servers() []*url.URL { - return lb.servers -} - -func (lb *testLoadBalancer) Options() []roundrobin.ServerOption { - return lb.options -} - -func (lb *testLoadBalancer) removeServer(u *url.URL) { - var i int - var serverURL *url.URL - found := false - for i, serverURL = range lb.servers { - if *serverURL == *u { - found = true - break - } +func (lb *testLoadBalancer) SetStatus(ctx context.Context, childName string, up bool) { + if up { + lb.numUpsertedServers++ + } else { + lb.numRemovedServers++ } - if !found { - return - } - - lb.servers = append(lb.servers[:i], lb.servers[i+1:]...) +} + +type MetricsMock struct { + Gauge gokitmetrics.Gauge +} + +func (m *MetricsMock) ServiceServerUpGauge() gokitmetrics.Gauge { + return m.Gauge } diff --git a/pkg/middlewares/accesslog/field_middleware.go b/pkg/middlewares/accesslog/field_middleware.go index c4182ab78..b37e0dda9 100644 --- a/pkg/middlewares/accesslog/field_middleware.go +++ b/pkg/middlewares/accesslog/field_middleware.go @@ -41,14 +41,6 @@ func (f *FieldHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { } } -// AddServiceFields add service fields. -func AddServiceFields(rw http.ResponseWriter, req *http.Request, next http.Handler, data *LogData) { - data.Core[ServiceURL] = req.URL // note that this is *not* the original incoming URL - data.Core[ServiceAddr] = req.URL.Host - - next.ServeHTTP(rw, req) -} - // AddOriginFields add origin fields. func AddOriginFields(rw http.ResponseWriter, req *http.Request, next http.Handler, data *LogData) { start := time.Now().UTC() diff --git a/pkg/middlewares/capture/capture.go b/pkg/middlewares/capture/capture.go index 845c2e983..1a67d0ae4 100644 --- a/pkg/middlewares/capture/capture.go +++ b/pkg/middlewares/capture/capture.go @@ -62,13 +62,13 @@ func FromContext(ctx context.Context) (Capture, error) { // Capture is the object populated by the capture middleware, // holding probes that allow to gather information about the request and response. type Capture struct { - rr *readCounter - rw responseWriter + rr *readCounter + crw *captureResponseWriter } // NeedsReset returns whether the given http.ResponseWriter is the capture's probe. func (c *Capture) NeedsReset(rw http.ResponseWriter) bool { - return c.rw != rw + return c.crw.rw != rw } // Reset returns a new handler that renews the Capture's probes, and inserts @@ -83,18 +83,18 @@ func (c *Capture) Reset(next http.Handler) http.Handler { c.rr = readCounter newReq.Body = readCounter } - c.rw = newResponseWriter(rw) + c.crw = &captureResponseWriter{rw: rw} - next.ServeHTTP(c.rw, newReq) + next.ServeHTTP(c.crw, newReq) }) } func (c *Capture) ResponseSize() int64 { - return c.rw.Size() + return c.crw.Size() } func (c *Capture) StatusCode() int { - return c.rw.Status() + return c.crw.Status() } // RequestSize returns the size of the request's body if it applies, @@ -123,22 +123,7 @@ func (r *readCounter) Close() error { return r.source.Close() } -var _ middlewares.Stateful = &responseWriterWithCloseNotify{} - -type responseWriter interface { - http.ResponseWriter - Size() int64 - Status() int -} - -func newResponseWriter(rw http.ResponseWriter) responseWriter { - capt := &captureResponseWriter{rw: rw} - if _, ok := rw.(http.CloseNotifier); !ok { - return capt - } - - return &responseWriterWithCloseNotify{capt} -} +var _ middlewares.Stateful = &captureResponseWriter{} // captureResponseWriter is a wrapper of type http.ResponseWriter // that tracks response status and size. @@ -189,13 +174,3 @@ func (crw *captureResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) return nil, nil, fmt.Errorf("not a hijacker: %T", crw.rw) } - -type responseWriterWithCloseNotify struct { - *captureResponseWriter -} - -// CloseNotify returns a channel that receives at most a -// single value (true) when the client connection has gone away. -func (r *responseWriterWithCloseNotify) CloseNotify() <-chan bool { - return r.rw.(http.CloseNotifier).CloseNotify() -} diff --git a/pkg/middlewares/capture/capture_test.go b/pkg/middlewares/capture/capture_test.go index 9d8f91fcf..7705aa6b6 100644 --- a/pkg/middlewares/capture/capture_test.go +++ b/pkg/middlewares/capture/capture_test.go @@ -189,44 +189,3 @@ func TestRequestReader(t *testing.T) { require.NoError(t, err) assert.Equal(t, int64(3), rr.size) } - -type rwWithCloseNotify struct { - *httptest.ResponseRecorder -} - -func (r *rwWithCloseNotify) CloseNotify() <-chan bool { - panic("implement me") -} - -func TestCloseNotifier(t *testing.T) { - testCases := []struct { - rw http.ResponseWriter - desc string - implementsCloseNotifier bool - }{ - { - rw: httptest.NewRecorder(), - desc: "does not implement CloseNotifier", - implementsCloseNotifier: false, - }, - { - rw: &rwWithCloseNotify{httptest.NewRecorder()}, - desc: "implements CloseNotifier", - implementsCloseNotifier: true, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - _, ok := test.rw.(http.CloseNotifier) - assert.Equal(t, test.implementsCloseNotifier, ok) - - rw := newResponseWriter(test.rw) - _, impl := rw.(http.CloseNotifier) - assert.Equal(t, test.implementsCloseNotifier, impl) - }) - } -} diff --git a/pkg/middlewares/compress/brotli/brotli.go b/pkg/middlewares/compress/brotli/brotli.go new file mode 100644 index 000000000..ebc93b5b3 --- /dev/null +++ b/pkg/middlewares/compress/brotli/brotli.go @@ -0,0 +1,338 @@ +package brotli + +import ( + "bufio" + "fmt" + "io" + "mime" + "net" + "net/http" + + "github.com/andybalholm/brotli" +) + +const ( + vary = "Vary" + acceptEncoding = "Accept-Encoding" + contentEncoding = "Content-Encoding" + contentLength = "Content-Length" + contentType = "Content-Type" +) + +// Config is the Brotli handler configuration. +type Config struct { + // ExcludedContentTypes is the list of content types for which we should not compress. + ExcludedContentTypes []string + // MinSize is the minimum size (in bytes) required to enable compression. + MinSize int +} + +// NewWrapper returns a new Brotli compressing wrapper. +func NewWrapper(cfg Config) (func(http.Handler) http.HandlerFunc, error) { + if cfg.MinSize < 0 { + return nil, fmt.Errorf("minimum size must be greater than or equal to zero") + } + + var contentTypes []parsedContentType + for _, v := range cfg.ExcludedContentTypes { + mediaType, params, err := mime.ParseMediaType(v) + if err != nil { + return nil, fmt.Errorf("parsing media type: %w", err) + } + + contentTypes = append(contentTypes, parsedContentType{mediaType, params}) + } + + return func(h http.Handler) http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + rw.Header().Add(vary, acceptEncoding) + + brw := &responseWriter{ + rw: rw, + bw: brotli.NewWriter(rw), + minSize: cfg.MinSize, + statusCode: http.StatusOK, + excludedContentTypes: contentTypes, + } + defer brw.close() + + h.ServeHTTP(brw, r) + } + }, nil +} + +// TODO: check whether we want to implement content-type sniffing (as gzip does) +// TODO: check whether we should support Accept-Ranges (as gzip does, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Ranges) +type responseWriter struct { + rw http.ResponseWriter + bw *brotli.Writer + + minSize int + excludedContentTypes []parsedContentType + + buf []byte + hijacked bool + compressionStarted bool + compressionDisabled bool + headersSent bool + + // Mostly needed to avoid calling bw.Flush/bw.Close when no data was + // written in bw. + seenData bool + + statusCodeSet bool + statusCode int +} + +func (r *responseWriter) Header() http.Header { + return r.rw.Header() +} + +func (r *responseWriter) WriteHeader(statusCode int) { + if r.statusCodeSet { + return + } + + r.statusCode = statusCode + r.statusCodeSet = true +} + +func (r *responseWriter) Write(p []byte) (int, error) { + // i.e. has write ever been called at least once with non nil data. + if !r.seenData && len(p) > 0 { + r.seenData = true + } + + // We do not compress, either for contentEncoding or contentType reasons. + if r.compressionDisabled { + return r.rw.Write(p) + } + + // We have already buffered more than minSize, + // We are now in compression cruise mode until the end of times. + if r.compressionStarted { + // If compressionStarted we assume we have sent headers already + return r.bw.Write(p) + } + + // If we detect a contentEncoding, we know we are never going to compress. + if r.rw.Header().Get(contentEncoding) != "" { + r.compressionDisabled = true + return r.rw.Write(p) + } + + // Disable compression according to user wishes in excludedContentTypes. + if ct := r.rw.Header().Get(contentType); ct != "" { + mediaType, params, err := mime.ParseMediaType(ct) + if err != nil { + return 0, fmt.Errorf("parsing media type: %w", err) + } + + for _, excludedContentType := range r.excludedContentTypes { + if excludedContentType.equals(mediaType, params) { + r.compressionDisabled = true + return r.rw.Write(p) + } + } + } + + // We buffer until we know whether to compress (i.e. when we reach minSize received). + if len(r.buf)+len(p) < r.minSize { + r.buf = append(r.buf, p...) + return len(p), nil + } + + // If we ever make it here, we have received at least minSize, which means we want to compress, + // and we are going to send headers right away. + r.compressionStarted = true + + // Since we know we are going to compress we will never be able to know the actual length. + r.rw.Header().Del(contentLength) + + r.rw.Header().Set(contentEncoding, "br") + r.rw.WriteHeader(r.statusCode) + r.headersSent = true + + // Start with sending what we have previously buffered, before actually writing + // the bytes in argument. + n, err := r.bw.Write(r.buf) + if err != nil { + r.buf = r.buf[n:] + // Return zero because we haven't taken care of the bytes in argument yet. + return 0, err + } + + // If we wrote less than what we wanted, we need to reclaim the leftovers + the bytes in argument, + // and keep them for a subsequent Write. + if n < len(r.buf) { + r.buf = r.buf[n:] + r.buf = append(r.buf, p...) + return len(p), nil + } + + // Otherwise just reset the buffer. + r.buf = r.buf[:0] + + // Now that we emptied the buffer, we can actually write the given bytes. + return r.bw.Write(p) +} + +// Flush flushes data to the appropriate underlying writer(s), although it does +// not guarantee that all buffered data will be sent. +// If not enough bytes have been written to determine whether to enable compression, +// no flushing will take place. +func (r *responseWriter) Flush() { + if !r.seenData { + // we should not flush if there never was any data, because flushing the bw + // (just like closing) would send some extra end of compressionStarted stream bytes. + return + } + + // It was already established by Write that compression is disabled, we only + // have to flush the uncompressed writer. + if r.compressionDisabled { + if rw, ok := r.rw.(http.Flusher); ok { + rw.Flush() + } + + return + } + + // Here, nothing was ever written either to rw or to bw (since we're still + // waiting to decide whether to compress), so we do not need to flush anything. + // Note that we diverge with klauspost's gzip behavior, where they instead + // force compression and flush whatever was in the buffer in this case. + if !r.compressionStarted { + return + } + + // Conversely, we here know that something was already written to bw (or is + // going to be written right after anyway), so bw will have to be flushed. + // Also, since we know that bw writes to rw, but (apparently) never flushes it, + // we have to do it ourselves. + defer func() { + // because we also ignore the error returned by Write anyway + _ = r.bw.Flush() + + if rw, ok := r.rw.(http.Flusher); ok { + rw.Flush() + } + }() + + // We empty whatever is left of the buffer that Write never took care of. + n, err := r.bw.Write(r.buf) + if err != nil { + return + } + + // And just like in Write we also handle "short writes". + if n < len(r.buf) { + r.buf = r.buf[n:] + return + } + + r.buf = r.buf[:0] +} + +func (r *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + if hijacker, ok := r.rw.(http.Hijacker); ok { + // We only make use of r.hijacked in close (and not in Write/WriteHeader) + // because we want to let the stdlib catch the error on writes, as + // they already do a good job of logging it. + r.hijacked = true + return hijacker.Hijack() + } + + return nil, nil, fmt.Errorf("%T is not a http.Hijacker", r.rw) +} + +// close closes the underlying writers if/when appropriate. +// Note that the compressed writer should not be closed if we never used it, +// as it would otherwise send some extra "end of compression" bytes. +// Close also makes sure to flush whatever was left to write from the buffer. +func (r *responseWriter) close() error { + if r.hijacked { + return nil + } + + // We have to take care of statusCode ourselves (in case there was never any + // call to Write or WriteHeader before us) as it's the only header we buffer. + if !r.headersSent { + r.rw.WriteHeader(r.statusCode) + r.headersSent = true + } + + // Nothing was ever written anywhere, nothing to flush. + if !r.seenData { + return nil + } + + // If compression was disabled, there never was anything in the buffer to flush, + // and nothing was ever written to bw. + if r.compressionDisabled { + return nil + } + + if len(r.buf) == 0 { + // If we got here we know compression has started, so we can safely flush on bw. + return r.bw.Close() + } + + // There is still data in the buffer, because we never reached minSize (to + // determine whether to compress). We therefore flush it uncompressed. + if !r.compressionStarted { + n, err := r.rw.Write(r.buf) + if err != nil { + return err + } + if n < len(r.buf) { + return io.ErrShortWrite + } + return nil + } + + // There is still data in the buffer, simply because Write did not take care of it all. + // We flush it to the compressed writer. + n, err := r.bw.Write(r.buf) + if err != nil { + r.bw.Close() + return err + } + if n < len(r.buf) { + r.bw.Close() + return io.ErrShortWrite + } + return r.bw.Close() +} + +// parsedContentType is the parsed representation of one of the inputs to ContentTypes. +// From https://github.com/klauspost/compress/blob/master/gzhttp/compress.go#L401. +type parsedContentType struct { + mediaType string + params map[string]string +} + +// equals returns whether this content type matches another content type. +func (p parsedContentType) equals(mediaType string, params map[string]string) bool { + if p.mediaType != mediaType { + return false + } + + // if p has no params, don't care about other's params + if len(p.params) == 0 { + return true + } + + // if p has any params, they must be identical to other's. + if len(p.params) != len(params) { + return false + } + + for k, v := range p.params { + if w, ok := params[k]; !ok || v != w { + return false + } + } + + return true +} diff --git a/pkg/middlewares/compress/brotli/brotli_test.go b/pkg/middlewares/compress/brotli/brotli_test.go new file mode 100644 index 000000000..373fe71ca --- /dev/null +++ b/pkg/middlewares/compress/brotli/brotli_test.go @@ -0,0 +1,618 @@ +package brotli + +import ( + "bytes" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/andybalholm/brotli" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + smallTestBody = []byte("aaabbc" + strings.Repeat("aaabbbccc", 9) + "aaabbbc") + bigTestBody = []byte(strings.Repeat(strings.Repeat("aaabbbccc", 66)+" ", 6) + strings.Repeat("aaabbbccc", 66)) +) + +func Test_Vary(t *testing.T) { + h := newTestHandler(t, smallTestBody) + + req, _ := http.NewRequest(http.MethodGet, "/whatever", nil) + req.Header.Set(acceptEncoding, "br") + + rw := httptest.NewRecorder() + h.ServeHTTP(rw, req) + + assert.Equal(t, http.StatusOK, rw.Code) + assert.Equal(t, acceptEncoding, rw.Header().Get(vary)) +} + +func Test_SmallBodyNoCompression(t *testing.T) { + h := newTestHandler(t, smallTestBody) + + req, _ := http.NewRequest(http.MethodGet, "/whatever", nil) + req.Header.Set(acceptEncoding, "br") + + rw := httptest.NewRecorder() + h.ServeHTTP(rw, req) + + // With less than 1024 bytes the response should not be compressed. + assert.Equal(t, http.StatusOK, rw.Code) + assert.Empty(t, rw.Header().Get(contentEncoding)) + assert.Equal(t, smallTestBody, rw.Body.Bytes()) +} + +func Test_AlreadyCompressed(t *testing.T) { + h := newTestHandler(t, bigTestBody) + + req, _ := http.NewRequest(http.MethodGet, "/compressed", nil) + req.Header.Set(acceptEncoding, "br") + + rw := httptest.NewRecorder() + h.ServeHTTP(rw, req) + + assert.Equal(t, bigTestBody, rw.Body.Bytes()) +} + +func Test_NoBody(t *testing.T) { + testCases := []struct { + desc string + statusCode int + body []byte + }{ + { + desc: "status no content", + statusCode: http.StatusNoContent, + body: nil, + }, + { + desc: "status not modified", + statusCode: http.StatusNotModified, + body: nil, + }, + { + desc: "status OK with empty body", + statusCode: http.StatusOK, + body: []byte{}, + }, + { + desc: "status OK with nil body", + statusCode: http.StatusOK, + body: nil, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + h := mustNewWrapper(t, Config{MinSize: 1024})(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(test.statusCode) + + _, err := rw.Write(test.body) + require.NoError(t, err) + })) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set(acceptEncoding, "br") + + rw := httptest.NewRecorder() + h.ServeHTTP(rw, req) + + body, err := io.ReadAll(rw.Body) + require.NoError(t, err) + + assert.Empty(t, rw.Header().Get(contentEncoding)) + assert.Empty(t, body) + }) + } +} + +func Test_MinSize(t *testing.T) { + cfg := Config{ + MinSize: 128, + } + + var bodySize int + h := mustNewWrapper(t, cfg)(http.HandlerFunc( + func(rw http.ResponseWriter, req *http.Request) { + for i := 0; i < bodySize; i++ { + // We make sure to Write at least once less than minSize so that both + // cases below go through the same algo: i.e. they start buffering + // because they haven't reached minSize. + _, err := rw.Write([]byte{'x'}) + require.NoError(t, err) + } + }, + )) + + req, _ := http.NewRequest(http.MethodGet, "/whatever", &bytes.Buffer{}) + req.Header.Add(acceptEncoding, "br") + + // Short response is not compressed + bodySize = cfg.MinSize - 1 + rw := httptest.NewRecorder() + h.ServeHTTP(rw, req) + + assert.Empty(t, rw.Result().Header.Get(contentEncoding)) + + // Long response is compressed + bodySize = cfg.MinSize + rw = httptest.NewRecorder() + h.ServeHTTP(rw, req) + + assert.Equal(t, "br", rw.Result().Header.Get(contentEncoding)) +} + +func Test_MultipleWriteHeader(t *testing.T) { + h := mustNewWrapper(t, Config{MinSize: 1024})(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + // We ensure that the subsequent call to WriteHeader is a noop. + rw.WriteHeader(http.StatusInternalServerError) + rw.WriteHeader(http.StatusNotFound) + })) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set(acceptEncoding, "br") + + rw := httptest.NewRecorder() + h.ServeHTTP(rw, req) + + assert.Equal(t, http.StatusInternalServerError, rw.Code) +} + +func Test_FlushBeforeWrite(t *testing.T) { + srv := httptest.NewServer(mustNewWrapper(t, Config{MinSize: 1024})(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + rw.(http.Flusher).Flush() + + _, err := rw.Write(bigTestBody) + require.NoError(t, err) + }))) + defer srv.Close() + + req, err := http.NewRequest(http.MethodGet, srv.URL, http.NoBody) + require.NoError(t, err) + + req.Header.Set(acceptEncoding, "br") + + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + defer res.Body.Close() + + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Equal(t, "br", res.Header.Get(contentEncoding)) + + got, err := io.ReadAll(brotli.NewReader(res.Body)) + require.NoError(t, err) + assert.Equal(t, bigTestBody, got) +} + +func Test_FlushAfterWrite(t *testing.T) { + srv := httptest.NewServer(mustNewWrapper(t, Config{MinSize: 1024})(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + + _, err := rw.Write(bigTestBody[0:1]) + require.NoError(t, err) + + rw.(http.Flusher).Flush() + for _, b := range bigTestBody[1:] { + _, err := rw.Write([]byte{b}) + require.NoError(t, err) + } + }))) + defer srv.Close() + + req, err := http.NewRequest(http.MethodGet, srv.URL, http.NoBody) + require.NoError(t, err) + + req.Header.Set(acceptEncoding, "br") + + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + defer res.Body.Close() + + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Equal(t, "br", res.Header.Get(contentEncoding)) + + got, err := io.ReadAll(brotli.NewReader(res.Body)) + require.NoError(t, err) + assert.Equal(t, bigTestBody, got) +} + +func Test_FlushAfterWriteNil(t *testing.T) { + srv := httptest.NewServer(mustNewWrapper(t, Config{MinSize: 1024})(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + + _, err := rw.Write(nil) + require.NoError(t, err) + + rw.(http.Flusher).Flush() + }))) + defer srv.Close() + + req, err := http.NewRequest(http.MethodGet, srv.URL, http.NoBody) + require.NoError(t, err) + + req.Header.Set(acceptEncoding, "br") + + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + defer res.Body.Close() + + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Empty(t, res.Header.Get(contentEncoding)) + + got, err := io.ReadAll(brotli.NewReader(res.Body)) + require.NoError(t, err) + assert.Empty(t, got) +} + +func Test_FlushAfterAllWrites(t *testing.T) { + srv := httptest.NewServer(mustNewWrapper(t, Config{MinSize: 1024})(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + for i := range bigTestBody { + _, err := rw.Write(bigTestBody[i : i+1]) + require.NoError(t, err) + } + rw.(http.Flusher).Flush() + }))) + defer srv.Close() + + req, err := http.NewRequest(http.MethodGet, srv.URL, http.NoBody) + require.NoError(t, err) + + req.Header.Set(acceptEncoding, "br") + + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + defer res.Body.Close() + + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Equal(t, "br", res.Header.Get(contentEncoding)) + + got, err := io.ReadAll(brotli.NewReader(res.Body)) + require.NoError(t, err) + assert.Equal(t, bigTestBody, got) +} + +func Test_ExcludedContentTypes(t *testing.T) { + testCases := []struct { + desc string + contentType string + excludedContentTypes []string + expCompression bool + }{ + { + desc: "Always compress when content types are empty", + contentType: "", + excludedContentTypes: []string{}, + expCompression: true, + }, + { + desc: "MIME match", + contentType: "application/json", + excludedContentTypes: []string{"application/json"}, + expCompression: false, + }, + { + desc: "MIME no match", + contentType: "text/xml", + excludedContentTypes: []string{"application/json"}, + expCompression: true, + }, + { + desc: "MIME match with no other directive ignores non-MIME directives", + contentType: "application/json; charset=utf-8", + excludedContentTypes: []string{"application/json"}, + expCompression: false, + }, + { + desc: "MIME match with other directives requires all directives be equal, different charset", + contentType: "application/json; charset=ascii", + excludedContentTypes: []string{"application/json; charset=utf-8"}, + expCompression: true, + }, + { + desc: "MIME match with other directives requires all directives be equal, same charset", + contentType: "application/json; charset=utf-8", + excludedContentTypes: []string{"application/json; charset=utf-8"}, + expCompression: false, + }, + { + desc: "MIME match with other directives requires all directives be equal, missing charset", + contentType: "application/json", + excludedContentTypes: []string{"application/json; charset=ascii"}, + expCompression: true, + }, + { + desc: "MIME match case insensitive", + contentType: "Application/Json", + excludedContentTypes: []string{"application/json"}, + expCompression: false, + }, + { + desc: "MIME match ignore whitespace", + contentType: "application/json;charset=utf-8", + excludedContentTypes: []string{"application/json; charset=utf-8"}, + expCompression: false, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + cfg := Config{ + MinSize: 1024, + ExcludedContentTypes: test.excludedContentTypes, + } + h := mustNewWrapper(t, cfg)(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Header().Set(contentType, test.contentType) + + rw.WriteHeader(http.StatusOK) + + _, err := rw.Write(bigTestBody) + require.NoError(t, err) + })) + + req, _ := http.NewRequest(http.MethodGet, "/whatever", nil) + req.Header.Set(acceptEncoding, "br") + + rw := httptest.NewRecorder() + h.ServeHTTP(rw, req) + + assert.Equal(t, http.StatusOK, rw.Code) + + if test.expCompression { + assert.Equal(t, "br", rw.Header().Get(contentEncoding)) + + got, err := io.ReadAll(brotli.NewReader(rw.Body)) + assert.Nil(t, err) + assert.Equal(t, bigTestBody, got) + } else { + assert.NotEqual(t, "br", rw.Header().Get("Content-Encoding")) + + got, err := io.ReadAll(rw.Body) + assert.Nil(t, err) + assert.Equal(t, bigTestBody, got) + } + }) + } +} + +func Test_FlushExcludedContentTypes(t *testing.T) { + testCases := []struct { + desc string + contentType string + excludedContentTypes []string + expCompression bool + }{ + { + desc: "Always compress when content types are empty", + contentType: "", + excludedContentTypes: []string{}, + expCompression: true, + }, + { + desc: "MIME match", + contentType: "application/json", + excludedContentTypes: []string{"application/json"}, + expCompression: false, + }, + { + desc: "MIME no match", + contentType: "text/xml", + excludedContentTypes: []string{"application/json"}, + expCompression: true, + }, + { + desc: "MIME match with no other directive ignores non-MIME directives", + contentType: "application/json; charset=utf-8", + excludedContentTypes: []string{"application/json"}, + expCompression: false, + }, + { + desc: "MIME match with other directives requires all directives be equal, different charset", + contentType: "application/json; charset=ascii", + excludedContentTypes: []string{"application/json; charset=utf-8"}, + expCompression: true, + }, + { + desc: "MIME match with other directives requires all directives be equal, same charset", + contentType: "application/json; charset=utf-8", + excludedContentTypes: []string{"application/json; charset=utf-8"}, + expCompression: false, + }, + { + desc: "MIME match with other directives requires all directives be equal, missing charset", + contentType: "application/json", + excludedContentTypes: []string{"application/json; charset=ascii"}, + expCompression: true, + }, + { + desc: "MIME match case insensitive", + contentType: "Application/Json", + excludedContentTypes: []string{"application/json"}, + expCompression: false, + }, + { + desc: "MIME match ignore whitespace", + contentType: "application/json;charset=utf-8", + excludedContentTypes: []string{"application/json; charset=utf-8"}, + expCompression: false, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + cfg := Config{ + MinSize: 1024, + ExcludedContentTypes: test.excludedContentTypes, + } + h := mustNewWrapper(t, cfg)(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Header().Set(contentType, test.contentType) + rw.WriteHeader(http.StatusOK) + + tb := bigTestBody + for len(tb) > 0 { + // Write 100 bytes per run + // Detection should not be affected (we send 100 bytes) + toWrite := 100 + if toWrite > len(tb) { + toWrite = len(tb) + } + + _, err := rw.Write(tb[:toWrite]) + require.NoError(t, err) + + // Flush between each write + rw.(http.Flusher).Flush() + tb = tb[toWrite:] + } + })) + + req, _ := http.NewRequest(http.MethodGet, "/whatever", nil) + req.Header.Set(acceptEncoding, "br") + + // This doesn't allow checking flushes, but we validate if content is correct. + rw := httptest.NewRecorder() + h.ServeHTTP(rw, req) + + assert.Equal(t, http.StatusOK, rw.Code) + + if test.expCompression { + assert.Equal(t, "br", rw.Header().Get(contentEncoding)) + + got, err := io.ReadAll(brotli.NewReader(rw.Body)) + assert.Nil(t, err) + assert.Equal(t, bigTestBody, got) + } else { + assert.NotEqual(t, "br", rw.Header().Get(contentEncoding)) + + got, err := io.ReadAll(rw.Body) + assert.Nil(t, err) + assert.Equal(t, bigTestBody, got) + } + }) + } +} + +func mustNewWrapper(t *testing.T, cfg Config) func(http.Handler) http.HandlerFunc { + t.Helper() + + w, err := NewWrapper(cfg) + require.NoError(t, err) + + return w +} + +func newTestHandler(t *testing.T, body []byte) http.Handler { + t.Helper() + + return mustNewWrapper(t, Config{MinSize: 1024})( + http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if req.URL.Path == "/compressed" { + rw.Header().Set("Content-Encoding", "br") + } + + _, err := rw.Write(body) + require.NoError(t, err) + }), + ) +} + +func TestParseContentType_equals(t *testing.T) { + testCases := []struct { + desc string + pct parsedContentType + mediaType string + params map[string]string + expect assert.BoolAssertionFunc + }{ + { + desc: "empty parsed content type", + expect: assert.True, + }, + { + desc: "simple content type", + pct: parsedContentType{ + mediaType: "plain/text", + }, + mediaType: "plain/text", + expect: assert.True, + }, + { + desc: "content type with params", + pct: parsedContentType{ + mediaType: "plain/text", + params: map[string]string{ + "charset": "utf8", + }, + }, + mediaType: "plain/text", + params: map[string]string{ + "charset": "utf8", + }, + expect: assert.True, + }, + { + desc: "different content type", + pct: parsedContentType{ + mediaType: "plain/text", + }, + mediaType: "application/json", + expect: assert.False, + }, + { + desc: "content type with params", + pct: parsedContentType{ + mediaType: "plain/text", + params: map[string]string{ + "charset": "utf8", + }, + }, + mediaType: "plain/text", + params: map[string]string{ + "charset": "latin-1", + }, + expect: assert.False, + }, + { + desc: "different number of parameters", + pct: parsedContentType{ + mediaType: "plain/text", + params: map[string]string{ + "charset": "utf8", + }, + }, + mediaType: "plain/text", + params: map[string]string{ + "charset": "utf8", + "q": "0.8", + }, + expect: assert.False, + }, + } + + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + test.expect(t, test.pct.equals(test.mediaType, test.params)) + }) + } +} diff --git a/pkg/middlewares/compress/compress.go b/pkg/middlewares/compress/compress.go index a83cc6682..55caa51b0 100644 --- a/pkg/middlewares/compress/compress.go +++ b/pkg/middlewares/compress/compress.go @@ -1,22 +1,26 @@ package compress import ( - "compress/gzip" "context" + "fmt" "mime" "net/http" + "strings" "github.com/klauspost/compress/gzhttp" "github.com/opentracing/opentracing-go/ext" "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/log" "github.com/traefik/traefik/v2/pkg/middlewares" + "github.com/traefik/traefik/v2/pkg/middlewares/compress/brotli" "github.com/traefik/traefik/v2/pkg/tracing" ) -const ( - typeName = "Compress" -) +const typeName = "Compress" + +// DefaultMinSize is the default minimum size (in bytes) required to enable compression. +// See https://github.com/klauspost/compress/blob/9559b037e79ad673c71f6ef7c732c00949014cd2/gzhttp/compress.go#L47. +const DefaultMinSize = 1024 // Compress is a middleware that allows to compress the response. type compress struct { @@ -24,6 +28,9 @@ type compress struct { name string excludes []string minSize int + + brotliHandler http.Handler + gzipHandler http.Handler } // New creates a new compress middleware. @@ -40,42 +47,117 @@ func New(ctx context.Context, next http.Handler, conf dynamic.Compress, name str excludes = append(excludes, mediaType) } - minSize := gzhttp.DefaultMinSize + minSize := DefaultMinSize if conf.MinResponseBodyBytes > 0 { minSize = conf.MinResponseBodyBytes } - return &compress{next: next, name: name, excludes: excludes, minSize: minSize}, nil + c := &compress{ + next: next, + name: name, + excludes: excludes, + minSize: minSize, + } + + var err error + c.brotliHandler, err = c.newBrotliHandler() + if err != nil { + return nil, err + } + + c.gzipHandler, err = c.newGzipHandler() + if err != nil { + return nil, err + } + + return c, nil } func (c *compress) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - mediaType, _, err := mime.ParseMediaType(req.Header.Get("Content-Type")) - if err != nil { - log.FromContext(middlewares.GetLoggerCtx(context.Background(), c.name, typeName)).Debug(err) + logger := log.FromContext(middlewares.GetLoggerCtx(req.Context(), c.name, typeName)) + + if req.Method == http.MethodHead { + c.next.ServeHTTP(rw, req) + return } + mediaType, _, err := mime.ParseMediaType(req.Header.Get("Content-Type")) + if err != nil { + logger.WithError(err).Debug("Unable to parse MIME type") + } + + // Notably for text/event-stream requests the response should not be compressed. + // See https://github.com/traefik/traefik/issues/2576 if contains(c.excludes, mediaType) { c.next.ServeHTTP(rw, req) - } else { - ctx := middlewares.GetLoggerCtx(req.Context(), c.name, typeName) - c.gzipHandler(ctx).ServeHTTP(rw, req) + return } + + // Client allows us to do whatever we want, so we br compress. + // See https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.3 + acceptEncoding, ok := req.Header["Accept-Encoding"] + if !ok { + c.brotliHandler.ServeHTTP(rw, req) + return + } + + if encodingAccepts(acceptEncoding, "br") { + c.brotliHandler.ServeHTTP(rw, req) + return + } + + if encodingAccepts(acceptEncoding, "gzip") { + c.gzipHandler.ServeHTTP(rw, req) + return + } + + c.next.ServeHTTP(rw, req) } func (c *compress) GetTracingInformation() (string, ext.SpanKindEnum) { return c.name, tracing.SpanKindNoneEnum } -func (c *compress) gzipHandler(ctx context.Context) http.Handler { +func (c *compress) newGzipHandler() (http.Handler, error) { wrapper, err := gzhttp.NewWrapper( gzhttp.ExceptContentTypes(c.excludes), - gzhttp.CompressionLevel(gzip.DefaultCompression), - gzhttp.MinSize(c.minSize)) + gzhttp.MinSize(c.minSize), + ) if err != nil { - log.FromContext(ctx).Error(err) + return nil, fmt.Errorf("new gzip wrapper: %w", err) } - return wrapper(c.next) + return wrapper(c.next), nil +} + +func (c *compress) newBrotliHandler() (http.Handler, error) { + cfg := brotli.Config{ + ExcludedContentTypes: c.excludes, + MinSize: c.minSize, + } + + wrapper, err := brotli.NewWrapper(cfg) + if err != nil { + return nil, fmt.Errorf("new brotli wrapper: %w", err) + } + + return wrapper(c.next), nil +} + +func encodingAccepts(acceptEncoding []string, typ string) bool { + for _, ae := range acceptEncoding { + for _, e := range strings.Split(ae, ",") { + parsed := strings.Split(strings.TrimSpace(e), ";") + if len(parsed) == 0 { + continue + } + if parsed[0] == typ || parsed[0] == "*" { + return true + } + } + } + + return false } func contains(values []string, val string) bool { @@ -84,5 +166,6 @@ func contains(values []string, val string) bool { return true } } + return false } diff --git a/pkg/middlewares/compress/compress_test.go b/pkg/middlewares/compress/compress_test.go index 8f7e85ae0..b3eb0e03c 100644 --- a/pkg/middlewares/compress/compress_test.go +++ b/pkg/middlewares/compress/compress_test.go @@ -1,12 +1,14 @@ package compress import ( + "compress/gzip" "context" "io" "net/http" "net/http/httptest" "testing" + "github.com/andybalholm/brotli" "github.com/klauspost/compress/gzhttp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,8 +22,81 @@ const ( contentTypeHeader = "Content-Type" varyHeader = "Vary" gzipValue = "gzip" + brotliValue = "br" ) +func TestNegotiation(t *testing.T) { + testCases := []struct { + desc string + acceptEncHeader string + expEncoding string + }{ + { + desc: "no accept header", + expEncoding: "br", + }, + { + desc: "unsupported accept header", + acceptEncHeader: "notreal", + expEncoding: "", + }, + { + desc: "accept any header", + acceptEncHeader: "*", + expEncoding: "br", + }, + { + desc: "gzip accept header", + acceptEncHeader: "gzip", + expEncoding: "gzip", + }, + { + desc: "br accept header", + acceptEncHeader: "br", + expEncoding: "br", + }, + { + desc: "multi accept header, prefer br", + acceptEncHeader: "br;q=0.8, gzip;q=0.6", + expEncoding: "br", + }, + { + desc: "multi accept header, prefer br", + acceptEncHeader: "gzip;q=1.0, br;q=0.8", + expEncoding: "br", + }, + { + desc: "multi accept header list, prefer br", + acceptEncHeader: "gzip, br", + expEncoding: "br", + }, + } + + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil) + if test.acceptEncHeader != "" { + req.Header.Add(acceptEncodingHeader, test.acceptEncHeader) + } + + next := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + _, _ = rw.Write(generateBytes(10)) + }) + handler, err := New(context.Background(), next, dynamic.Compress{MinResponseBodyBytes: 1}, "testing") + require.NoError(t, err) + + rw := httptest.NewRecorder() + handler.ServeHTTP(rw, req) + + assert.Equal(t, test.expEncoding, rw.Header().Get(contentEncodingHeader)) + }) + } +} + func TestShouldCompressWhenNoContentEncodingHeader(t *testing.T) { req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil) req.Header.Add(acceptEncodingHeader, gzipValue) @@ -41,9 +116,12 @@ func TestShouldCompressWhenNoContentEncodingHeader(t *testing.T) { assert.Equal(t, gzipValue, rw.Header().Get(contentEncodingHeader)) assert.Equal(t, acceptEncodingHeader, rw.Header().Get(varyHeader)) - if assert.ObjectsAreEqualValues(rw.Body.Bytes(), baseBody) { - assert.Fail(t, "expected a compressed body", "got %v", rw.Body.Bytes()) - } + gr, err := gzip.NewReader(rw.Body) + require.NoError(t, err) + + got, err := io.ReadAll(gr) + require.NoError(t, err) + assert.Equal(t, got, baseBody) } func TestShouldNotCompressWhenContentEncodingHeader(t *testing.T) { @@ -71,7 +149,7 @@ func TestShouldNotCompressWhenContentEncodingHeader(t *testing.T) { assert.EqualValues(t, rw.Body.Bytes(), fakeCompressedBody) } -func TestShouldNotCompressWhenNoAcceptEncodingHeader(t *testing.T) { +func TestShouldCompressWhenNoAcceptEncodingHeader(t *testing.T) { req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil) fakeBody := generateBytes(gzhttp.DefaultMinSize) @@ -87,7 +165,33 @@ func TestShouldNotCompressWhenNoAcceptEncodingHeader(t *testing.T) { rw := httptest.NewRecorder() handler.ServeHTTP(rw, req) + assert.Equal(t, brotliValue, rw.Header().Get(contentEncodingHeader)) + assert.Equal(t, acceptEncodingHeader, rw.Header().Get(varyHeader)) + + got, err := io.ReadAll(brotli.NewReader(rw.Body)) + require.NoError(t, err) + assert.Equal(t, got, fakeBody) +} + +func TestShouldNotCompressHeadRequest(t *testing.T) { + req := testhelpers.MustNewRequest(http.MethodHead, "http://localhost", nil) + req.Header.Add(acceptEncodingHeader, gzipValue) + + fakeBody := generateBytes(gzhttp.DefaultMinSize) + next := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + _, err := rw.Write(fakeBody) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + } + }) + handler, err := New(context.Background(), next, dynamic.Compress{}, "testing") + require.NoError(t, err) + + rw := httptest.NewRecorder() + handler.ServeHTTP(rw, req) + assert.Empty(t, rw.Header().Get(contentEncodingHeader)) + assert.Empty(t, rw.Header().Get(varyHeader)) assert.EqualValues(t, rw.Body.Bytes(), fakeBody) } diff --git a/pkg/middlewares/customerrors/custom_errors.go b/pkg/middlewares/customerrors/custom_errors.go index dd28aeccd..e98dfd7d6 100644 --- a/pkg/middlewares/customerrors/custom_errors.go +++ b/pkg/middlewares/customerrors/custom_errors.go @@ -21,9 +21,8 @@ import ( // Compile time validation that the response recorder implements http interfaces correctly. var ( - // TODO: maybe remove at least for codeModifierWithCloseNotify. - _ middlewares.Stateful = &codeModifierWithCloseNotify{} - _ middlewares.Stateful = &codeCatcherWithCloseNotify{} + _ middlewares.Stateful = &codeModifier{} + _ middlewares.Stateful = &codeCatcher{} ) const typeName = "customError" @@ -124,13 +123,6 @@ func newRequest(baseURL string) (*http.Request, error) { return req, nil } -type responseInterceptor interface { - http.ResponseWriter - http.Flusher - getCode() int - isFilteredCode() bool -} - // codeCatcher is a response writer that detects as soon as possible whether the // response is a code within the ranges of codes it watches for. If it is, it // simply drops the data from the response. Otherwise, it forwards it directly to @@ -144,27 +136,13 @@ type codeCatcher struct { headersSent bool } -type codeCatcherWithCloseNotify struct { - *codeCatcher -} - -// CloseNotify returns a channel that receives at most a -// single value (true) when the client connection has gone away. -func (cc *codeCatcherWithCloseNotify) CloseNotify() <-chan bool { - return cc.responseWriter.(http.CloseNotifier).CloseNotify() -} - -func newCodeCatcher(rw http.ResponseWriter, httpCodeRanges types.HTTPCodeRanges) responseInterceptor { - catcher := &codeCatcher{ +func newCodeCatcher(rw http.ResponseWriter, httpCodeRanges types.HTTPCodeRanges) *codeCatcher { + return &codeCatcher{ headerMap: make(http.Header), code: http.StatusOK, // If backend does not call WriteHeader on us, we consider it's a 200. responseWriter: rw, httpCodeRanges: httpCodeRanges, } - if _, ok := rw.(http.CloseNotifier); ok { - return &codeCatcherWithCloseNotify{catcher} - } - return catcher } func (cc *codeCatcher) Header() http.Header { @@ -240,24 +218,7 @@ func (cc *codeCatcher) Flush() { // codeModifier forwards a response back to the client, // while enforcing a given response code. -type codeModifier interface { - http.ResponseWriter -} - -// newCodeModifier returns a codeModifier that enforces the given code. -func newCodeModifier(rw http.ResponseWriter, code int) codeModifier { - codeMod := &codeModifierWithoutCloseNotify{ - headerMap: make(http.Header), - code: code, - responseWriter: rw, - } - if _, ok := rw.(http.CloseNotifier); ok { - return &codeModifierWithCloseNotify{codeMod} - } - return codeMod -} - -type codeModifierWithoutCloseNotify struct { +type codeModifier struct { code int // the code enforced in the response. // headerSent is whether the headers have already been sent, @@ -268,18 +229,17 @@ type codeModifierWithoutCloseNotify struct { responseWriter http.ResponseWriter } -type codeModifierWithCloseNotify struct { - *codeModifierWithoutCloseNotify -} - -// CloseNotify returns a channel that receives at most a -// single value (true) when the client connection has gone away. -func (r *codeModifierWithCloseNotify) CloseNotify() <-chan bool { - return r.responseWriter.(http.CloseNotifier).CloseNotify() +// newCodeModifier returns a codeModifier that enforces the given code. +func newCodeModifier(rw http.ResponseWriter, code int) *codeModifier { + return &codeModifier{ + headerMap: make(http.Header), + code: code, + responseWriter: rw, + } } // Header returns the response headers. -func (r *codeModifierWithoutCloseNotify) Header() http.Header { +func (r *codeModifier) Header() http.Header { if r.headerMap == nil { r.headerMap = make(http.Header) } @@ -289,14 +249,14 @@ func (r *codeModifierWithoutCloseNotify) Header() http.Header { // Write calls WriteHeader to send the enforced code, // then writes the data directly to r.responseWriter. -func (r *codeModifierWithoutCloseNotify) Write(buf []byte) (int, error) { +func (r *codeModifier) Write(buf []byte) (int, error) { r.WriteHeader(r.code) return r.responseWriter.Write(buf) } // WriteHeader sends the headers, with the enforced code (the code in argument // is always ignored), if it hasn't already been done. -func (r *codeModifierWithoutCloseNotify) WriteHeader(_ int) { +func (r *codeModifier) WriteHeader(_ int) { if r.headerSent { return } @@ -307,7 +267,7 @@ func (r *codeModifierWithoutCloseNotify) WriteHeader(_ int) { } // Hijack hijacks the connection. -func (r *codeModifierWithoutCloseNotify) Hijack() (net.Conn, *bufio.ReadWriter, error) { +func (r *codeModifier) Hijack() (net.Conn, *bufio.ReadWriter, error) { hijacker, ok := r.responseWriter.(http.Hijacker) if !ok { return nil, nil, fmt.Errorf("%T is not a http.Hijacker", r.responseWriter) @@ -316,7 +276,7 @@ func (r *codeModifierWithoutCloseNotify) Hijack() (net.Conn, *bufio.ReadWriter, } // Flush sends any buffered data to the client. -func (r *codeModifierWithoutCloseNotify) Flush() { +func (r *codeModifier) Flush() { r.WriteHeader(r.code) if flusher, ok := r.responseWriter.(http.Flusher); ok { diff --git a/pkg/middlewares/customerrors/custom_errors_test.go b/pkg/middlewares/customerrors/custom_errors_test.go index c291b15d5..c0de064c7 100644 --- a/pkg/middlewares/customerrors/custom_errors_test.go +++ b/pkg/middlewares/customerrors/custom_errors_test.go @@ -188,50 +188,3 @@ type mockServiceBuilder struct { func (m *mockServiceBuilder) BuildHTTP(_ context.Context, _ string) (http.Handler, error) { return m.handler, nil } - -func TestNewResponseRecorder(t *testing.T) { - testCases := []struct { - desc string - rw http.ResponseWriter - expected http.ResponseWriter - }{ - { - desc: "Without Close Notify", - rw: httptest.NewRecorder(), - expected: &codeModifierWithoutCloseNotify{}, - }, - { - desc: "With Close Notify", - rw: &mockRWCloseNotify{}, - expected: &codeModifierWithCloseNotify{}, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - rec := newCodeModifier(test.rw, 0) - assert.IsType(t, rec, test.expected) - }) - } -} - -type mockRWCloseNotify struct{} - -func (m *mockRWCloseNotify) CloseNotify() <-chan bool { - panic("implement me") -} - -func (m *mockRWCloseNotify) Header() http.Header { - panic("implement me") -} - -func (m *mockRWCloseNotify) Write([]byte) (int, error) { - panic("implement me") -} - -func (m *mockRWCloseNotify) WriteHeader(int) { - panic("implement me") -} diff --git a/pkg/middlewares/emptybackendhandler/empty_backend_handler.go b/pkg/middlewares/emptybackendhandler/empty_backend_handler.go deleted file mode 100644 index 3331bf3e3..000000000 --- a/pkg/middlewares/emptybackendhandler/empty_backend_handler.go +++ /dev/null @@ -1,34 +0,0 @@ -package emptybackendhandler - -import ( - "net/http" - - "github.com/traefik/traefik/v2/pkg/healthcheck" -) - -// EmptyBackend is a middleware that checks whether the current Backend -// has at least one active Server in respect to the healthchecks and if this -// is not the case, it will stop the middleware chain and respond with 503. -type emptyBackend struct { - healthcheck.BalancerStatusHandler -} - -// New creates a new EmptyBackend middleware. -func New(lb healthcheck.BalancerStatusHandler) http.Handler { - return &emptyBackend{BalancerStatusHandler: lb} -} - -// ServeHTTP responds with 503 when there is no active Server and otherwise -// invokes the next handler in the middleware chain. -func (e *emptyBackend) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - if len(e.BalancerStatusHandler.Servers()) != 0 { - e.BalancerStatusHandler.ServeHTTP(rw, req) - return - } - - rw.WriteHeader(http.StatusServiceUnavailable) - if _, err := rw.Write([]byte(http.StatusText(http.StatusServiceUnavailable))); err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } -} diff --git a/pkg/middlewares/emptybackendhandler/empty_backend_handler_test.go b/pkg/middlewares/emptybackendhandler/empty_backend_handler_test.go deleted file mode 100644 index b0d9714d2..000000000 --- a/pkg/middlewares/emptybackendhandler/empty_backend_handler_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package emptybackendhandler - -import ( - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/traefik/traefik/v2/pkg/testhelpers" - "github.com/vulcand/oxy/roundrobin" -) - -func TestEmptyBackendHandler(t *testing.T) { - testCases := []struct { - amountServer int - expectedStatusCode int - }{ - { - amountServer: 0, - expectedStatusCode: http.StatusServiceUnavailable, - }, - { - amountServer: 1, - expectedStatusCode: http.StatusOK, - }, - } - - for _, test := range testCases { - test := test - t.Run(fmt.Sprintf("amount servers %d", test.amountServer), func(t *testing.T) { - t.Parallel() - - handler := New(&healthCheckLoadBalancer{amountServer: test.amountServer}) - - recorder := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "http://localhost", nil) - - handler.ServeHTTP(recorder, req) - - assert.Equal(t, test.expectedStatusCode, recorder.Result().StatusCode) - }) - } -} - -type healthCheckLoadBalancer struct { - amountServer int -} - -func (lb *healthCheckLoadBalancer) RegisterStatusUpdater(fn func(up bool)) error { - return nil -} - -func (lb *healthCheckLoadBalancer) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) -} - -func (lb *healthCheckLoadBalancer) Servers() []*url.URL { - servers := make([]*url.URL, lb.amountServer) - for i := 0; i < lb.amountServer; i++ { - servers = append(servers, testhelpers.MustParseURL("http://localhost")) - } - return servers -} - -func (lb *healthCheckLoadBalancer) RemoveServer(u *url.URL) error { - return nil -} - -func (lb *healthCheckLoadBalancer) UpsertServer(u *url.URL, options ...roundrobin.ServerOption) error { - return nil -} - -func (lb *healthCheckLoadBalancer) ServerWeight(u *url.URL) (int, bool) { - return 0, false -} - -func (lb *healthCheckLoadBalancer) NextServer() (*url.URL, error) { - return nil, nil -} - -func (lb *healthCheckLoadBalancer) Next() http.Handler { - return nil -} diff --git a/pkg/middlewares/headers/responsewriter.go b/pkg/middlewares/headers/responsewriter.go index 15201b1fd..893af32c8 100644 --- a/pkg/middlewares/headers/responsewriter.go +++ b/pkg/middlewares/headers/responsewriter.go @@ -23,17 +23,12 @@ type responseModifier struct { // modifier can be nil. func newResponseModifier(w http.ResponseWriter, r *http.Request, modifier func(*http.Response) error) http.ResponseWriter { - rm := &responseModifier{ + return &responseModifier{ req: r, rw: w, modifier: modifier, code: http.StatusOK, } - - if _, ok := w.(http.CloseNotifier); ok { - return responseModifierWithCloseNotify{responseModifier: rm} - } - return rm } func (r *responseModifier) WriteHeader(code int) { @@ -97,12 +92,3 @@ func (r *responseModifier) Flush() { flusher.Flush() } } - -type responseModifierWithCloseNotify struct { - *responseModifier -} - -// CloseNotify implements http.CloseNotifier. -func (r *responseModifierWithCloseNotify) CloseNotify() <-chan bool { - return r.responseModifier.rw.(http.CloseNotifier).CloseNotify() -} diff --git a/pkg/middlewares/metrics/metrics.go b/pkg/middlewares/metrics/metrics.go index a2201667a..4286da165 100644 --- a/pkg/middlewares/metrics/metrics.go +++ b/pkg/middlewares/metrics/metrics.go @@ -104,13 +104,6 @@ func WrapRouterHandler(ctx context.Context, registry metrics.Registry, routerNam } } -// WrapServiceHandler Wraps metrics service to alice.Constructor. -func WrapServiceHandler(ctx context.Context, registry metrics.Registry, serviceName string) alice.Constructor { - return func(next http.Handler) (http.Handler, error) { - return NewServiceMiddleware(ctx, next, registry, serviceName), nil - } -} - func (m *metricsMiddleware) ServeHTTP(rw http.ResponseWriter, req *http.Request) { proto := getRequestProtocol(req) diff --git a/pkg/middlewares/metrics/metrics_test.go b/pkg/middlewares/metrics/metrics_test.go index eb194a9fb..541c34170 100644 --- a/pkg/middlewares/metrics/metrics_test.go +++ b/pkg/middlewares/metrics/metrics_test.go @@ -60,47 +60,6 @@ func (m *collectingRetryMetrics) ServiceRetriesCounter() metrics.Counter { return m.retriesCounter } -type rwWithCloseNotify struct { - *httptest.ResponseRecorder -} - -func (r *rwWithCloseNotify) CloseNotify() <-chan bool { - panic("implement me") -} - -func TestCloseNotifier(t *testing.T) { - testCases := []struct { - rw http.ResponseWriter - desc string - implementsCloseNotifier bool - }{ - { - rw: httptest.NewRecorder(), - desc: "does not implement CloseNotifier", - implementsCloseNotifier: false, - }, - { - rw: &rwWithCloseNotify{httptest.NewRecorder()}, - desc: "implements CloseNotifier", - implementsCloseNotifier: true, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - _, ok := test.rw.(http.CloseNotifier) - assert.Equal(t, test.implementsCloseNotifier, ok) - - rw := newResponseRecorder(test.rw) - _, impl := rw.(http.CloseNotifier) - assert.Equal(t, test.implementsCloseNotifier, impl) - }) - } -} - func Test_getMethod(t *testing.T) { testCases := []struct { method string diff --git a/pkg/middlewares/metrics/recorder.go b/pkg/middlewares/metrics/recorder.go deleted file mode 100644 index 66de624b4..000000000 --- a/pkg/middlewares/metrics/recorder.go +++ /dev/null @@ -1,63 +0,0 @@ -package metrics - -import ( - "bufio" - "net" - "net/http" -) - -type recorder interface { - http.ResponseWriter - http.Flusher - getCode() int -} - -func newResponseRecorder(rw http.ResponseWriter) recorder { - rec := &responseRecorder{ - ResponseWriter: rw, - statusCode: http.StatusOK, - } - if _, ok := rw.(http.CloseNotifier); !ok { - return rec - } - return &responseRecorderWithCloseNotify{rec} -} - -// responseRecorder captures information from the response and preserves it for -// later analysis. -type responseRecorder struct { - http.ResponseWriter - statusCode int -} - -type responseRecorderWithCloseNotify struct { - *responseRecorder -} - -// CloseNotify returns a channel that receives at most a -// single value (true) when the client connection has gone away. -func (r *responseRecorderWithCloseNotify) CloseNotify() <-chan bool { - return r.ResponseWriter.(http.CloseNotifier).CloseNotify() -} - -func (r *responseRecorder) getCode() int { - return r.statusCode -} - -// WriteHeader captures the status code for later retrieval. -func (r *responseRecorder) WriteHeader(status int) { - r.ResponseWriter.WriteHeader(status) - r.statusCode = status -} - -// Hijack hijacks the connection. -func (r *responseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { - return r.ResponseWriter.(http.Hijacker).Hijack() -} - -// Flush sends any buffered data to the client. -func (r *responseRecorder) Flush() { - if f, ok := r.ResponseWriter.(http.Flusher); ok { - f.Flush() - } -} diff --git a/pkg/middlewares/pipelining/pipelining.go b/pkg/middlewares/pipelining/pipelining.go deleted file mode 100644 index 284c4bd1c..000000000 --- a/pkg/middlewares/pipelining/pipelining.go +++ /dev/null @@ -1,70 +0,0 @@ -package pipelining - -import ( - "bufio" - "context" - "net" - "net/http" - - "github.com/traefik/traefik/v2/pkg/log" - "github.com/traefik/traefik/v2/pkg/middlewares" -) - -const ( - typeName = "Pipelining" -) - -// pipelining returns a middleware. -type pipelining struct { - next http.Handler -} - -// New returns a new pipelining instance. -func New(ctx context.Context, next http.Handler, name string) http.Handler { - log.FromContext(middlewares.GetLoggerCtx(ctx, name, typeName)).Debug("Creating middleware") - - return &pipelining{ - next: next, - } -} - -func (p *pipelining) ServeHTTP(rw http.ResponseWriter, r *http.Request) { - // https://github.com/golang/go/blob/3d59583836630cf13ec4bfbed977d27b1b7adbdc/src/net/http/server.go#L201-L218 - if r.Method == http.MethodPut || r.Method == http.MethodPost { - p.next.ServeHTTP(rw, r) - } else { - p.next.ServeHTTP(&writerWithoutCloseNotify{rw}, r) - } -} - -// writerWithoutCloseNotify helps to disable closeNotify. -type writerWithoutCloseNotify struct { - W http.ResponseWriter -} - -// Header returns the response headers. -func (w *writerWithoutCloseNotify) Header() http.Header { - return w.W.Header() -} - -// Write writes the data to the connection as part of an HTTP reply. -func (w *writerWithoutCloseNotify) Write(buf []byte) (int, error) { - return w.W.Write(buf) -} - -// WriteHeader sends an HTTP response header with the provided status code. -func (w *writerWithoutCloseNotify) WriteHeader(code int) { - w.W.WriteHeader(code) -} - -// Flush sends any buffered data to the client. -func (w *writerWithoutCloseNotify) Flush() { - if f, ok := w.W.(http.Flusher); ok { - f.Flush() - } -} - -// Hijack hijacks the connection. -func (w *writerWithoutCloseNotify) Hijack() (net.Conn, *bufio.ReadWriter, error) { - return w.W.(http.Hijacker).Hijack() -} diff --git a/pkg/middlewares/pipelining/pipelining_test.go b/pkg/middlewares/pipelining/pipelining_test.go deleted file mode 100644 index b26e5fc6b..000000000 --- a/pkg/middlewares/pipelining/pipelining_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package pipelining - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -type recorderWithCloseNotify struct { - *httptest.ResponseRecorder -} - -func (r *recorderWithCloseNotify) CloseNotify() <-chan bool { - panic("implement me") -} - -func TestNew(t *testing.T) { - testCases := []struct { - desc string - HTTPMethod string - implementCloseNotifier bool - }{ - { - desc: "should not implement CloseNotifier with GET method", - HTTPMethod: http.MethodGet, - implementCloseNotifier: false, - }, - { - desc: "should implement CloseNotifier with PUT method", - HTTPMethod: http.MethodPut, - implementCloseNotifier: true, - }, - { - desc: "should implement CloseNotifier with POST method", - HTTPMethod: http.MethodPost, - implementCloseNotifier: true, - }, - { - desc: "should not implement CloseNotifier with GET method", - HTTPMethod: http.MethodHead, - implementCloseNotifier: false, - }, - { - desc: "should not implement CloseNotifier with PROPFIND method", - HTTPMethod: "PROPFIND", - implementCloseNotifier: false, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, ok := w.(http.CloseNotifier) - assert.Equal(t, test.implementCloseNotifier, ok) - w.WriteHeader(http.StatusOK) - }) - handler := New(context.Background(), nextHandler, "pipe") - - req := httptest.NewRequest(test.HTTPMethod, "http://localhost", nil) - - handler.ServeHTTP(&recorderWithCloseNotify{httptest.NewRecorder()}, req) - }) - } -} diff --git a/pkg/middlewares/retry/retry.go b/pkg/middlewares/retry/retry.go index 37dd4abef..a05ced970 100644 --- a/pkg/middlewares/retry/retry.go +++ b/pkg/middlewares/retry/retry.go @@ -20,11 +20,9 @@ import ( ) // Compile time validation that the response writer implements http interfaces correctly. -var _ middlewares.Stateful = &responseWriterWithCloseNotify{} +var _ middlewares.Stateful = &responseWriter{} -const ( - typeName = "Retry" -) +const typeName = "Retry" // Listener is used to inform about retry attempts. type Listener interface { @@ -149,57 +147,44 @@ func (l Listeners) Retried(req *http.Request, attempt int) { } } -type responseWriter interface { - http.ResponseWriter - http.Flusher - ShouldRetry() bool - DisableRetries() -} - -func newResponseWriter(rw http.ResponseWriter, shouldRetry bool) responseWriter { - responseWriter := &responseWriterWithoutCloseNotify{ +func newResponseWriter(rw http.ResponseWriter, shouldRetry bool) *responseWriter { + return &responseWriter{ responseWriter: rw, headers: make(http.Header), shouldRetry: shouldRetry, } - if _, ok := rw.(http.CloseNotifier); ok { - return &responseWriterWithCloseNotify{ - responseWriterWithoutCloseNotify: responseWriter, - } - } - return responseWriter } -type responseWriterWithoutCloseNotify struct { +type responseWriter struct { responseWriter http.ResponseWriter headers http.Header shouldRetry bool written bool } -func (r *responseWriterWithoutCloseNotify) ShouldRetry() bool { +func (r *responseWriter) ShouldRetry() bool { return r.shouldRetry } -func (r *responseWriterWithoutCloseNotify) DisableRetries() { +func (r *responseWriter) DisableRetries() { r.shouldRetry = false } -func (r *responseWriterWithoutCloseNotify) Header() http.Header { +func (r *responseWriter) Header() http.Header { if r.written { return r.responseWriter.Header() } return r.headers } -func (r *responseWriterWithoutCloseNotify) Write(buf []byte) (int, error) { +func (r *responseWriter) Write(buf []byte) (int, error) { if r.ShouldRetry() { return len(buf), nil } return r.responseWriter.Write(buf) } -func (r *responseWriterWithoutCloseNotify) WriteHeader(code int) { +func (r *responseWriter) WriteHeader(code int) { if r.ShouldRetry() && code == http.StatusServiceUnavailable { // We get a 503 HTTP Status Code when there is no backend server in the pool // to which the request could be sent. Also, note that r.ShouldRetry() @@ -226,7 +211,7 @@ func (r *responseWriterWithoutCloseNotify) WriteHeader(code int) { r.written = true } -func (r *responseWriterWithoutCloseNotify) Hijack() (net.Conn, *bufio.ReadWriter, error) { +func (r *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { hijacker, ok := r.responseWriter.(http.Hijacker) if !ok { return nil, nil, fmt.Errorf("%T is not a http.Hijacker", r.responseWriter) @@ -234,16 +219,8 @@ func (r *responseWriterWithoutCloseNotify) Hijack() (net.Conn, *bufio.ReadWriter return hijacker.Hijack() } -func (r *responseWriterWithoutCloseNotify) Flush() { +func (r *responseWriter) Flush() { if flusher, ok := r.responseWriter.(http.Flusher); ok { flusher.Flush() } } - -type responseWriterWithCloseNotify struct { - *responseWriterWithoutCloseNotify -} - -func (r *responseWriterWithCloseNotify) CloseNotify() <-chan bool { - return r.responseWriter.(http.CloseNotifier).CloseNotify() -} diff --git a/pkg/middlewares/stateful.go b/pkg/middlewares/stateful.go index 1b75d5450..ccb1fe04a 100644 --- a/pkg/middlewares/stateful.go +++ b/pkg/middlewares/stateful.go @@ -8,5 +8,4 @@ type Stateful interface { http.ResponseWriter http.Hijacker http.Flusher - http.CloseNotifier } diff --git a/pkg/middlewares/tracing/entrypoint.go b/pkg/middlewares/tracing/entrypoint.go index 54f28cb86..71a1da6d3 100644 --- a/pkg/middlewares/tracing/entrypoint.go +++ b/pkg/middlewares/tracing/entrypoint.go @@ -48,7 +48,7 @@ func (e *entryPointMiddleware) ServeHTTP(rw http.ResponseWriter, req *http.Reque req = req.WithContext(tracing.WithTracing(req.Context(), e.Tracing)) - recorder := newStatusCodeRecoder(rw, http.StatusOK) + recorder := newStatusCodeRecorder(rw, http.StatusOK) e.next.ServeHTTP(recorder, req) tracing.LogResponseCode(span, recorder.Status()) diff --git a/pkg/middlewares/tracing/forwarder.go b/pkg/middlewares/tracing/forwarder.go index 51c313f4c..b57f62330 100644 --- a/pkg/middlewares/tracing/forwarder.go +++ b/pkg/middlewares/tracing/forwarder.go @@ -51,7 +51,7 @@ func (f *forwarderMiddleware) ServeHTTP(rw http.ResponseWriter, req *http.Reques tracing.InjectRequestHeaders(req) - recorder := newStatusCodeRecoder(rw, 200) + recorder := newStatusCodeRecorder(rw, 200) f.next.ServeHTTP(recorder, req) diff --git a/pkg/middlewares/tracing/status_code.go b/pkg/middlewares/tracing/status_code.go index f0c57f81f..b30e6d034 100644 --- a/pkg/middlewares/tracing/status_code.go +++ b/pkg/middlewares/tracing/status_code.go @@ -6,52 +6,35 @@ import ( "net/http" ) -type statusCodeRecoder interface { - http.ResponseWriter - Status() int +// newStatusCodeRecorder returns an initialized statusCodeRecoder. +func newStatusCodeRecorder(rw http.ResponseWriter, status int) *statusCodeRecorder { + return &statusCodeRecorder{rw, status} } -// newStatusCodeRecoder returns an initialized statusCodeRecoder. -func newStatusCodeRecoder(rw http.ResponseWriter, status int) statusCodeRecoder { - recorder := &statusCodeWithoutCloseNotify{rw, status} - if _, ok := rw.(http.CloseNotifier); ok { - return &statusCodeWithCloseNotify{recorder} - } - return recorder -} - -type statusCodeWithoutCloseNotify struct { +type statusCodeRecorder struct { http.ResponseWriter status int } // WriteHeader captures the status code for later retrieval. -func (s *statusCodeWithoutCloseNotify) WriteHeader(status int) { +func (s *statusCodeRecorder) WriteHeader(status int) { s.status = status s.ResponseWriter.WriteHeader(status) } // Status get response status. -func (s *statusCodeWithoutCloseNotify) Status() int { +func (s *statusCodeRecorder) Status() int { return s.status } // Hijack hijacks the connection. -func (s *statusCodeWithoutCloseNotify) Hijack() (net.Conn, *bufio.ReadWriter, error) { +func (s *statusCodeRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { return s.ResponseWriter.(http.Hijacker).Hijack() } // Flush sends any buffered data to the client. -func (s *statusCodeWithoutCloseNotify) Flush() { +func (s *statusCodeRecorder) Flush() { if flusher, ok := s.ResponseWriter.(http.Flusher); ok { flusher.Flush() } } - -type statusCodeWithCloseNotify struct { - *statusCodeWithoutCloseNotify -} - -func (s *statusCodeWithCloseNotify) CloseNotify() <-chan bool { - return s.ResponseWriter.(http.CloseNotifier).CloseNotify() -} diff --git a/pkg/provider/consulcatalog/config_test.go b/pkg/provider/consulcatalog/config_test.go index bb4653782..35e171a32 100644 --- a/pkg/provider/consulcatalog/config_test.go +++ b/pkg/provider/consulcatalog/config_test.go @@ -4,10 +4,12 @@ import ( "context" "fmt" "testing" + "time" "github.com/hashicorp/consul/api" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + ptypes "github.com/traefik/paerser/types" "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/tls" ) @@ -63,6 +65,9 @@ func TestDefaultRule(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -113,6 +118,9 @@ func TestDefaultRule(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -156,6 +164,9 @@ func TestDefaultRule(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -199,6 +210,9 @@ func TestDefaultRule(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -247,6 +261,9 @@ func TestDefaultRule(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -332,6 +349,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -384,7 +404,10 @@ func Test_buildConfiguration(t *testing.T) { URL: "https://127.0.0.1:443", }, }, - PassHostHeader: Bool(true), + PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, ServersTransport: "tls-ns-dc1-dev-Test", }, }, @@ -470,7 +493,10 @@ func Test_buildConfiguration(t *testing.T) { URL: "https://127.0.0.2:444", }, }, - PassHostHeader: Bool(true), + PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, ServersTransport: "tls-ns-dc1-dev-Test", }, }, @@ -547,6 +573,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "Test2": { @@ -557,6 +586,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -616,6 +648,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -672,6 +707,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -731,6 +769,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -779,6 +820,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -829,6 +873,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -871,6 +918,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -926,6 +976,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -971,6 +1024,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "Service2": { @@ -981,6 +1037,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1145,6 +1204,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1192,6 +1254,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1265,6 +1330,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1325,6 +1393,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1401,6 +1472,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1458,6 +1532,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1529,6 +1606,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1592,6 +1672,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1640,6 +1723,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1689,6 +1775,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1733,6 +1822,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "Service2": { @@ -1743,6 +1835,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1948,6 +2043,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2007,6 +2105,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2392,6 +2493,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2475,6 +2579,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2673,7 +2780,10 @@ func Test_buildConfiguration(t *testing.T) { URL: "https://127.0.0.1:80", }, }, - PassHostHeader: Bool(true), + PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, ServersTransport: "tls-ns-dc1-Test", }, }, @@ -2684,7 +2794,10 @@ func Test_buildConfiguration(t *testing.T) { URL: "https://127.0.0.2:80", }, }, - PassHostHeader: Bool(true), + PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, ServersTransport: "tls-ns-dc1-Test", }, }, diff --git a/pkg/provider/docker/config_test.go b/pkg/provider/docker/config_test.go index d5a9dca8c..6038cf4de 100644 --- a/pkg/provider/docker/config_test.go +++ b/pkg/provider/docker/config_test.go @@ -4,12 +4,14 @@ import ( "context" "strconv" "testing" + "time" docker "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" "github.com/docker/go-connections/nat" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + ptypes "github.com/traefik/paerser/types" "github.com/traefik/traefik/v2/pkg/config/dynamic" ) @@ -68,6 +70,9 @@ func TestDefaultRule(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -123,6 +128,9 @@ func TestDefaultRule(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -180,6 +188,9 @@ func TestDefaultRule(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -230,6 +241,9 @@ func TestDefaultRule(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -280,6 +294,9 @@ func TestDefaultRule(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -335,6 +352,9 @@ func TestDefaultRule(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -546,6 +566,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -620,6 +643,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "Test2": { @@ -630,6 +656,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -705,6 +734,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -761,6 +793,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -819,6 +854,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -869,6 +907,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -932,6 +973,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -985,6 +1029,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "Service2": { @@ -995,6 +1042,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1052,6 +1102,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1280,6 +1333,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1335,6 +1391,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1427,6 +1486,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1506,6 +1568,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1607,6 +1672,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1681,6 +1749,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1777,6 +1848,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1856,6 +1930,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1925,6 +2002,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "Test2": { @@ -1935,6 +2015,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1991,6 +2074,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2048,6 +2134,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2100,6 +2189,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "Service2": { @@ -2110,6 +2202,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2290,6 +2385,9 @@ func Test_buildConfiguration(t *testing.T) { "Test": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2529,6 +2627,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2596,6 +2697,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -3042,6 +3146,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -3208,6 +3315,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, diff --git a/pkg/provider/ecs/config_test.go b/pkg/provider/ecs/config_test.go index 3ab9c477d..c0bd40a78 100644 --- a/pkg/provider/ecs/config_test.go +++ b/pkg/provider/ecs/config_test.go @@ -3,10 +3,12 @@ package ecs import ( "context" "testing" + "time" "github.com/aws/aws-sdk-go/service/ec2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + ptypes "github.com/traefik/paerser/types" "github.com/traefik/traefik/v2/pkg/config/dynamic" ) @@ -64,6 +66,9 @@ func TestDefaultRule(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -114,6 +119,9 @@ func TestDefaultRule(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -166,6 +174,9 @@ func TestDefaultRule(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -211,6 +222,9 @@ func TestDefaultRule(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -256,6 +270,9 @@ func TestDefaultRule(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -306,6 +323,9 @@ func TestDefaultRule(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -496,6 +516,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -560,6 +583,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "Test2": { @@ -570,6 +596,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -635,6 +664,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -686,6 +718,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -739,6 +774,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -784,6 +822,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -842,6 +883,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -890,6 +934,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "Service2": { @@ -900,6 +947,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -952,6 +1002,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1145,6 +1198,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1195,6 +1251,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1277,6 +1336,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1346,6 +1408,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1432,6 +1497,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1496,6 +1564,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1577,6 +1648,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1647,6 +1721,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1706,6 +1783,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "Test2": { @@ -1716,6 +1796,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1767,6 +1850,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1819,6 +1905,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1871,6 +1960,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1932,6 +2024,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "Service2": { @@ -1942,6 +2037,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1989,6 +2087,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "Service2": { @@ -1999,6 +2100,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2257,6 +2361,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2319,6 +2426,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2725,6 +2835,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index 320b1f651..2fd8f17e6 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -307,7 +307,13 @@ func (c configBuilder) buildServersLB(namespace string, svc v1alpha1.LoadBalance passHostHeader := true lb.PassHostHeader = &passHostHeader } - lb.ResponseForwarding = conf.ResponseForwarding + + if conf.ResponseForwarding != nil && conf.ResponseForwarding.FlushInterval != "" { + err := lb.ResponseForwarding.FlushInterval.Set(conf.ResponseForwarding.FlushInterval) + if err != nil { + return nil, fmt.Errorf("unable to parse flushInterval: %w", err) + } + } lb.Sticky = svc.Sticky diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index 1189364e5..1542723a8 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -1283,7 +1283,10 @@ func TestLoadIngressRouteTCPs(t *testing.T) { URL: "http://[2001:db8:85a3:8d3:1319:8a2e:370:7348]:8080", }, }, - PassHostHeader: func(i bool) *bool { return &i }(true), + PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "default-external-svc-with-ipv6-8080": { @@ -1293,7 +1296,10 @@ func TestLoadIngressRouteTCPs(t *testing.T) { URL: "http://[2001:db8:85a3:8d3:1319:8a2e:370:7347]:8080", }, }, - PassHostHeader: func(i bool) *bool { return &i }(true), + PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "default-test-route-6b204d94623b3df4370c": { @@ -1510,6 +1516,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1578,6 +1587,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1629,6 +1641,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1686,6 +1701,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1734,6 +1752,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "default-test-route-77c62dfe9517144aeeaa": { @@ -1747,6 +1768,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1805,6 +1829,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "default-whoami2-8080": { @@ -1818,6 +1845,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1871,6 +1901,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1917,6 +1950,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1988,6 +2024,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "default-whoami5-8080": { @@ -2001,6 +2040,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "default-wrr2": { @@ -2028,6 +2070,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "default-whoami7-8080": { @@ -2041,6 +2086,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2108,6 +2156,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2187,6 +2238,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "default-whoami5-8080": { @@ -2200,6 +2254,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2270,6 +2327,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "foo-wrr1": { @@ -2305,6 +2365,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "foo-mirror1": { @@ -2328,6 +2391,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "bar-mirrored": { @@ -2430,6 +2496,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "default-whoami5-8080": { @@ -2443,6 +2512,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2514,6 +2586,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "default-whoami5-8080": { @@ -2527,6 +2602,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2584,6 +2662,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "default-whoami2-8080": { @@ -2597,6 +2678,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2717,6 +2801,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2786,6 +2873,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2834,6 +2924,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2903,6 +2996,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2973,6 +3069,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -3041,6 +3140,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -3098,6 +3200,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -3156,6 +3261,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -3200,6 +3308,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -3243,6 +3354,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -3286,6 +3400,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -3533,6 +3650,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -3575,7 +3695,7 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(false), - ResponseForwarding: &dynamic.ResponseForwarding{FlushInterval: "10s"}, + ResponseForwarding: &dynamic.ResponseForwarding{FlushInterval: ptypes.Duration(10 * time.Second)}, }, }, }, @@ -3630,6 +3750,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -3687,6 +3810,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -3733,6 +3859,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -3774,6 +3903,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -3813,6 +3945,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -3852,6 +3987,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -3949,7 +4087,10 @@ func TestLoadIngressRoutes(t *testing.T) { URL: "https://external.domain:443", }, }, - PassHostHeader: Bool(true), + PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, ServersTransport: "default-test", }, }, @@ -3963,7 +4104,10 @@ func TestLoadIngressRoutes(t *testing.T) { URL: "https://10.10.0.6:8443", }, }, - PassHostHeader: Bool(true), + PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, ServersTransport: "default-default-test", }, }, @@ -4036,6 +4180,9 @@ func TestLoadIngressRoutes(t *testing.T) { "default-test-route-6b204d94623b3df4370c": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -4923,6 +5070,9 @@ func TestCrossNamespace(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -4998,6 +5148,9 @@ func TestCrossNamespace(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "default-test-crossnamespace-route-9313b71dbe6a649d5049": { @@ -5011,6 +5164,9 @@ func TestCrossNamespace(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "default-test-errorpage-errorpage-service": { @@ -5024,6 +5180,9 @@ func TestCrossNamespace(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "default-test-crossnamespace-route-a1963878aac7331b7950": { @@ -5037,6 +5196,9 @@ func TestCrossNamespace(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -5111,7 +5273,10 @@ func TestCrossNamespace(t *testing.T) { URL: "http://10.10.0.2:80", }, }, - PassHostHeader: Bool(true), + PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, ServersTransport: "foo-test@kubernetescrd", }, }, @@ -5126,6 +5291,9 @@ func TestCrossNamespace(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "default-tr-svc-wrr1": { @@ -5181,6 +5349,9 @@ func TestCrossNamespace(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -5227,6 +5398,9 @@ func TestCrossNamespace(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "cross-ns-tr-svc-mirror2": { @@ -5251,6 +5425,9 @@ func TestCrossNamespace(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -5294,7 +5471,10 @@ func TestCrossNamespace(t *testing.T) { URL: "http://10.10.0.2:80", }, }, - PassHostHeader: Bool(true), + PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, ServersTransport: "cross-ns-st-cross-ns@kubernetescrd", }, }, @@ -5387,6 +5567,9 @@ func TestCrossNamespace(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -5431,6 +5614,9 @@ func TestCrossNamespace(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -5936,6 +6122,9 @@ func TestExternalNameService(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go index 4783c24f1..a4dbe2bfd 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go @@ -110,7 +110,7 @@ type LoadBalancerSpec struct { // By default, passHostHeader is true. PassHostHeader *bool `json:"passHostHeader,omitempty"` // ResponseForwarding defines how Traefik forwards the response from the upstream Kubernetes Service to the client. - ResponseForwarding *dynamic.ResponseForwarding `json:"responseForwarding,omitempty"` + ResponseForwarding *ResponseForwarding `json:"responseForwarding,omitempty"` // ServersTransport defines the name of ServersTransport resource to use. // It allows to configure the transport between Traefik and your servers. // Can only be used on a Kubernetes Service. @@ -121,6 +121,15 @@ type LoadBalancerSpec struct { Weight *int `json:"weight,omitempty"` } +type ResponseForwarding struct { + // FlushInterval defines the interval, in milliseconds, in between flushes to the client while copying the response body. + // A negative value means to flush immediately after each write to the client. + // This configuration is ignored when ReverseProxy recognizes a response as a streaming response; + // for such responses, writes are flushed to the client immediately. + // Default: 100ms + FlushInterval string `json:"flushInterval,omitempty"` +} + // Service defines an upstream HTTP service to proxy traffic to. type Service struct { LoadBalancerSpec `json:",inline"` diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go index 45ffd09fd..eaa5d450c 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go @@ -559,7 +559,7 @@ func (in *LoadBalancerSpec) DeepCopyInto(out *LoadBalancerSpec) { } if in.ResponseForwarding != nil { in, out := &in.ResponseForwarding, &out.ResponseForwarding - *out = new(dynamic.ResponseForwarding) + *out = new(ResponseForwarding) **out = **in } if in.Weight != nil { @@ -973,6 +973,22 @@ func (in *RateLimit) DeepCopy() *RateLimit { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResponseForwarding) DeepCopyInto(out *ResponseForwarding) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResponseForwarding. +func (in *ResponseForwarding) DeepCopy() *ResponseForwarding { + if in == nil { + return nil + } + out := new(ResponseForwarding) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Retry) DeepCopyInto(out *Retry) { *out = *in diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go index 7b2bc5b72..e9da54570 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes.go +++ b/pkg/provider/kubernetes/gateway/kubernetes.go @@ -1443,11 +1443,10 @@ func loadServices(client Client, namespace string, backendRefs []v1alpha2.HTTPBa return nil, nil, fmt.Errorf("unsupported HTTPBackendRef %s/%s/%s", *backendRef.Group, *backendRef.Kind, backendRef.Name) } - svc := dynamic.Service{ - LoadBalancer: &dynamic.ServersLoadBalancer{ - PassHostHeader: pointer.Bool(true), - }, - } + lb := &dynamic.ServersLoadBalancer{} + lb.SetDefaults() + + svc := dynamic.Service{LoadBalancer: lb} // TODO support cross namespace through ReferencePolicy service, exists, err := client.GetService(namespace, string(backendRef.Name)) diff --git a/pkg/provider/kubernetes/gateway/kubernetes_test.go b/pkg/provider/kubernetes/gateway/kubernetes_test.go index a3ce54924..803701476 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes_test.go +++ b/pkg/provider/kubernetes/gateway/kubernetes_test.go @@ -3,9 +3,11 @@ package gateway import ( "context" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + ptypes "github.com/traefik/paerser/types" "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/provider" "github.com/traefik/traefik/v2/pkg/tls" @@ -552,6 +554,9 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -642,6 +647,9 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -698,6 +706,9 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -762,6 +773,9 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -817,6 +831,9 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -872,6 +889,9 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -942,6 +962,9 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "default-whoami2-8080": { @@ -955,6 +978,9 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1014,6 +1040,9 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "default-whoami2-8080": { @@ -1027,6 +1056,9 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1103,6 +1135,9 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1188,6 +1223,9 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1267,6 +1305,9 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1322,6 +1363,9 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1392,6 +1436,9 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "bar-whoami-bar-80": { @@ -1405,6 +1452,9 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1460,6 +1510,9 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -3548,6 +3601,9 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -3722,6 +3778,9 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -3923,6 +3982,9 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "bar-whoami-bar-80": { @@ -3936,6 +3998,9 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "bar-http-app-bar-my-gateway-web-a431b128267aabc954fd-wrr": { @@ -4083,6 +4148,9 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "bar-http-app-bar-my-gateway-web-a431b128267aabc954fd-wrr": { @@ -4231,6 +4299,9 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, PassHostHeader: pointer.Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index 5605f97b9..4677b8b5d 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -517,11 +517,10 @@ func (p *Provider) loadService(client Client, namespace string, backend networki return nil, errors.New("service port not found") } - svc := &dynamic.Service{ - LoadBalancer: &dynamic.ServersLoadBalancer{ - PassHostHeader: func(v bool) *bool { return &v }(true), - }, - } + lb := &dynamic.ServersLoadBalancer{} + lb.SetDefaults() + + svc := &dynamic.Service{LoadBalancer: lb} svcConfig, err := parseServiceConfig(service.Annotations) if err != nil { diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go index 06a2a62fa..d1b76ec8a 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -7,8 +7,10 @@ import ( "os" "strings" "testing" + "time" "github.com/stretchr/testify/assert" + ptypes "github.com/traefik/paerser/types" "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/provider" "github.com/traefik/traefik/v2/pkg/tls" @@ -68,6 +70,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -115,6 +120,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Sticky: &dynamic.Sticky{ Cookie: &dynamic.Cookie{ Name: "foobar", @@ -157,6 +165,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -191,6 +202,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -225,6 +239,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -259,6 +276,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -289,6 +309,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -319,6 +342,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-example-com-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.11.0.1:80", @@ -350,6 +376,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -384,6 +413,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -418,6 +450,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -431,6 +466,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service2-8082": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.2:8080", @@ -462,6 +500,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -496,6 +537,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "default-backend": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -526,6 +570,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8089", @@ -556,6 +603,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-tchouk": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8089", @@ -586,6 +636,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-tchouk": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8089", @@ -620,6 +673,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-tchouk": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8089", @@ -633,6 +689,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-carotte": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8090", @@ -663,6 +722,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-tchouk": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8089", @@ -697,6 +759,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-tchouk": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8089", @@ -710,6 +775,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "toto-service1-tchouk": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.11.0.1:8089", @@ -762,6 +830,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-8080": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.0.0.1:8080", @@ -790,6 +861,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-example-com-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.11.0.1:80", @@ -827,6 +901,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-443": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "https://10.10.0.1:8443", @@ -857,6 +934,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-8443": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "https://10.10.0.1:8443", @@ -888,6 +968,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-8443": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "https://10.10.0.1:8443", @@ -919,6 +1002,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "default-backend": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.30.0.1:8080", @@ -949,6 +1035,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -1023,6 +1112,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -1053,6 +1145,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -1085,6 +1180,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -1113,6 +1211,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -1141,6 +1242,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -1169,6 +1273,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -1197,6 +1304,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -1225,6 +1335,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -1265,6 +1378,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -1294,6 +1410,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -1322,6 +1441,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -1350,6 +1472,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -1378,6 +1503,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -1406,6 +1534,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -1434,6 +1565,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -1462,6 +1596,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -1490,6 +1627,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -1518,6 +1658,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -1546,6 +1689,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "testing-service1-foobar": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:4711", @@ -1587,6 +1733,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "default-backend": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://10.10.0.1:8080", @@ -1678,6 +1827,9 @@ func TestLoadConfigurationFromIngressesWithExternalNameServices(t *testing.T) { "testing-service1-8080": { LoadBalancer: &dynamic.ServersLoadBalancer{ PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, Servers: []dynamic.Server{ { URL: "http://traefik.wtf:8080", @@ -1710,6 +1862,9 @@ func TestLoadConfigurationFromIngressesWithExternalNameServices(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1738,6 +1893,9 @@ func TestLoadConfigurationFromIngressesWithExternalNameServices(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, diff --git a/pkg/provider/kv/kv_test.go b/pkg/provider/kv/kv_test.go index ad8a5b865..d2c44797b 100644 --- a/pkg/provider/kv/kv_test.go +++ b/pkg/provider/kv/kv_test.go @@ -42,15 +42,15 @@ func Test_buildConfiguration(t *testing.T) { "traefik/http/routers/Router1/service": "foobar", "traefik/http/services/Service01/loadBalancer/healthCheck/path": "foobar", "traefik/http/services/Service01/loadBalancer/healthCheck/port": "42", - "traefik/http/services/Service01/loadBalancer/healthCheck/interval": "foobar", - "traefik/http/services/Service01/loadBalancer/healthCheck/timeout": "foobar", + "traefik/http/services/Service01/loadBalancer/healthCheck/interval": "1s", + "traefik/http/services/Service01/loadBalancer/healthCheck/timeout": "1s", "traefik/http/services/Service01/loadBalancer/healthCheck/hostname": "foobar", "traefik/http/services/Service01/loadBalancer/healthCheck/headers/name0": "foobar", "traefik/http/services/Service01/loadBalancer/healthCheck/headers/name1": "foobar", "traefik/http/services/Service01/loadBalancer/healthCheck/scheme": "foobar", "traefik/http/services/Service01/loadBalancer/healthCheck/mode": "foobar", "traefik/http/services/Service01/loadBalancer/healthCheck/followredirects": "true", - "traefik/http/services/Service01/loadBalancer/responseForwarding/flushInterval": "foobar", + "traefik/http/services/Service01/loadBalancer/responseForwarding/flushInterval": "1s", "traefik/http/services/Service01/loadBalancer/passHostHeader": "true", "traefik/http/services/Service01/loadBalancer/sticky/cookie/name": "foobar", "traefik/http/services/Service01/loadBalancer/sticky/cookie/secure": "true", @@ -646,8 +646,8 @@ func Test_buildConfiguration(t *testing.T) { Mode: "foobar", Path: "foobar", Port: 42, - Interval: "foobar", - Timeout: "foobar", + Interval: ptypes.Duration(time.Second), + Timeout: ptypes.Duration(time.Second), Hostname: "foobar", FollowRedirects: func(v bool) *bool { return &v }(true), Headers: map[string]string{ @@ -657,7 +657,7 @@ func Test_buildConfiguration(t *testing.T) { }, PassHostHeader: func(v bool) *bool { return &v }(true), ResponseForwarding: &dynamic.ResponseForwarding{ - FlushInterval: "foobar", + FlushInterval: ptypes.Duration(time.Second), }, }, }, diff --git a/pkg/provider/marathon/config_test.go b/pkg/provider/marathon/config_test.go index 038eae0b6..4e5b4d563 100644 --- a/pkg/provider/marathon/config_test.go +++ b/pkg/provider/marathon/config_test.go @@ -4,10 +4,12 @@ import ( "context" "math" "testing" + "time" "github.com/gambol99/go-marathon" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + ptypes "github.com/traefik/paerser/types" "github.com/traefik/traefik/v2/pkg/config/dynamic" ) @@ -71,6 +73,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }}, }, ServersTransports: map[string]*dynamic.ServersTransport{}, @@ -137,6 +142,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }}, }, ServersTransports: map[string]*dynamic.ServersTransport{}, @@ -189,6 +197,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }}, }, ServersTransports: map[string]*dynamic.ServersTransport{}, @@ -295,6 +306,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }}, }, ServersTransports: map[string]*dynamic.ServersTransport{}, @@ -356,6 +370,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }}, }, ServersTransports: map[string]*dynamic.ServersTransport{}, @@ -404,6 +421,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }}, "bar": {LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ @@ -412,6 +432,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }}, }, ServersTransports: map[string]*dynamic.ServersTransport{}, @@ -456,6 +479,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -498,6 +524,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }}, }, ServersTransports: map[string]*dynamic.ServersTransport{}, @@ -542,6 +571,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -579,6 +611,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -629,6 +664,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -669,6 +707,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "Service2": { @@ -679,6 +720,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -780,6 +824,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "app2": { @@ -790,6 +837,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -843,6 +893,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "app2": { @@ -853,6 +906,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -897,6 +953,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "app2": { @@ -907,6 +966,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -958,6 +1020,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1002,6 +1067,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "app2": { @@ -1012,6 +1080,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1055,6 +1126,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1099,6 +1173,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1138,6 +1215,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "Service2": { @@ -1148,6 +1228,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1355,6 +1438,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1399,6 +1485,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1442,6 +1531,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1766,6 +1858,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1826,6 +1921,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, diff --git a/pkg/provider/nomad/config_test.go b/pkg/provider/nomad/config_test.go index 653a5d28c..4002a156d 100644 --- a/pkg/provider/nomad/config_test.go +++ b/pkg/provider/nomad/config_test.go @@ -3,9 +3,11 @@ package nomad import ( "context" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + ptypes "github.com/traefik/paerser/types" "github.com/traefik/traefik/v2/pkg/config/dynamic" ) @@ -56,6 +58,9 @@ func Test_defaultRule(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -106,6 +111,9 @@ func Test_defaultRule(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -148,6 +156,9 @@ func Test_defaultRule(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -195,6 +206,9 @@ func Test_defaultRule(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -265,6 +279,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -323,6 +340,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "Test2": { @@ -333,6 +353,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -392,6 +415,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -448,6 +474,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -507,6 +536,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -555,6 +587,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -605,6 +640,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -647,6 +685,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -702,6 +743,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -747,6 +791,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "Service2": { @@ -757,6 +804,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -922,6 +972,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -969,6 +1022,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1043,6 +1099,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1104,6 +1163,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1160,6 +1222,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1221,6 +1286,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1269,6 +1337,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1318,6 +1389,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1362,6 +1436,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "Service2": { @@ -1372,6 +1449,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1547,6 +1627,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1606,6 +1689,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -1991,6 +2077,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2074,6 +2163,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -2272,6 +2364,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "Test-1234154071633021619": { @@ -2282,6 +2377,9 @@ func Test_buildConfig(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, diff --git a/pkg/provider/rancher/config_test.go b/pkg/provider/rancher/config_test.go index 29f47a51f..f1b080ecb 100644 --- a/pkg/provider/rancher/config_test.go +++ b/pkg/provider/rancher/config_test.go @@ -3,9 +3,11 @@ package rancher import ( "context" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + ptypes "github.com/traefik/paerser/types" "github.com/traefik/traefik/v2/pkg/config/dynamic" ) @@ -58,6 +60,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -116,6 +121,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "Test2": { @@ -126,6 +134,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -187,6 +198,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, "Test2": { @@ -197,6 +211,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -247,6 +264,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -353,6 +373,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -435,6 +458,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -494,6 +520,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -599,6 +628,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -910,6 +942,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, @@ -981,6 +1016,9 @@ func Test_buildConfiguration(t *testing.T) { }, }, PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, }, }, }, diff --git a/pkg/redactor/redactor_config_test.go b/pkg/redactor/redactor_config_test.go index fddc3f455..3ae6c0303 100644 --- a/pkg/redactor/redactor_config_test.go +++ b/pkg/redactor/redactor_config_test.go @@ -82,8 +82,8 @@ func init() { Scheme: "foo", Path: "foo", Port: 42, - Interval: "foo", - Timeout: "foo", + Interval: ptypes.Duration(111 * time.Second), + Timeout: ptypes.Duration(111 * time.Second), Hostname: "foo", FollowRedirects: boolPtr(true), Headers: map[string]string{ @@ -92,7 +92,7 @@ func init() { }, PassHostHeader: boolPtr(true), ResponseForwarding: &dynamic.ResponseForwarding{ - FlushInterval: "foo", + FlushInterval: ptypes.Duration(111 * time.Second), }, ServersTransport: "foo", Servers: []dynamic.Server{ diff --git a/pkg/redactor/testdata/anonymized-dynamic-config.json b/pkg/redactor/testdata/anonymized-dynamic-config.json index 15e8d4cd6..e82e4ab8f 100644 --- a/pkg/redactor/testdata/anonymized-dynamic-config.json +++ b/pkg/redactor/testdata/anonymized-dynamic-config.json @@ -75,8 +75,8 @@ "scheme": "foo", "path": "foo", "port": 42, - "interval": "foo", - "timeout": "foo", + "interval": "1m51s", + "timeout": "1m51s", "hostname": "xxxx", "followRedirects": true, "headers": { @@ -85,7 +85,7 @@ }, "passHostHeader": true, "responseForwarding": { - "flushInterval": "foo" + "flushInterval": "1m51s" }, "serversTransport": "foo" } diff --git a/pkg/redactor/testdata/secured-dynamic-config.json b/pkg/redactor/testdata/secured-dynamic-config.json index 996876708..cc7da86d3 100644 --- a/pkg/redactor/testdata/secured-dynamic-config.json +++ b/pkg/redactor/testdata/secured-dynamic-config.json @@ -75,8 +75,8 @@ "scheme": "foo", "path": "foo", "port": 42, - "interval": "foo", - "timeout": "foo", + "interval": "1m51s", + "timeout": "1m51s", "hostname": "foo", "followRedirects": true, "headers": { @@ -85,7 +85,7 @@ }, "passHostHeader": true, "responseForwarding": { - "flushInterval": "foo" + "flushInterval": "1m51s" }, "serversTransport": "foo" } diff --git a/pkg/server/router/router.go b/pkg/server/router/router.go index 6f1819dbe..e4acdef94 100644 --- a/pkg/server/router/router.go +++ b/pkg/server/router/router.go @@ -24,7 +24,7 @@ type middlewareBuilder interface { type serviceManager interface { BuildHTTP(rootCtx context.Context, serviceName string) (http.Handler, error) - LaunchHealthCheck() + LaunchHealthCheck(ctx context.Context) } // Manager A route/router manager. diff --git a/pkg/server/router/router_test.go b/pkg/server/router/router_test.go index 5c4f38ee1..da3fad57d 100644 --- a/pkg/server/router/router_test.go +++ b/pkg/server/router/router_test.go @@ -7,10 +7,12 @@ import ( "net/http/httptest" "strings" "testing" + "time" "github.com/containous/alice" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + ptypes "github.com/traefik/paerser/types" "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/config/runtime" "github.com/traefik/traefik/v2/pkg/metrics" @@ -478,7 +480,7 @@ func TestRuntimeConfiguration(t *testing.T) { }, }, HealthCheck: &dynamic.ServerHealthCheck{ - Interval: "500ms", + Interval: ptypes.Duration(500 * time.Millisecond), Path: "/health", }, }, diff --git a/pkg/server/router/tcp/postgres.go b/pkg/server/router/tcp/postgres.go new file mode 100644 index 000000000..857efb3ab --- /dev/null +++ b/pkg/server/router/tcp/postgres.go @@ -0,0 +1,161 @@ +package tcp + +import ( + "bufio" + "bytes" + "errors" + "sync" + + "github.com/traefik/traefik/v2/pkg/log" + tcpmuxer "github.com/traefik/traefik/v2/pkg/muxer/tcp" + "github.com/traefik/traefik/v2/pkg/tcp" +) + +var ( + PostgresStartTLSMsg = []byte{0, 0, 0, 8, 4, 210, 22, 47} // int32(8) + int32(80877103) + PostgresStartTLSReply = []byte{83} // S +) + +// isPostgres determines whether the buffer contains the Postgres STARTTLS message. +func isPostgres(br *bufio.Reader) (bool, error) { + // Peek the first 8 bytes individually to prevent blocking on peek + // if the underlying conn does not send enough bytes. + // It could happen if a protocol start by sending less than 8 bytes, + // and expect a response before proceeding. + for i := 1; i < len(PostgresStartTLSMsg)+1; i++ { + peeked, err := br.Peek(i) + if err != nil { + log.WithoutContext().Errorf("Error while Peeking first bytes: %s", err) + return false, err + } + + if !bytes.Equal(peeked, PostgresStartTLSMsg[:i]) { + return false, nil + } + } + return true, nil +} + +// servePostgres serves a connection with a Postgres client negotiating a STARTTLS session. +// It handles TCP TLS routing, after accepting to start the STARTTLS session. +func (r *Router) servePostgres(conn tcp.WriteCloser) { + _, err := conn.Write(PostgresStartTLSReply) + if err != nil { + conn.Close() + return + } + + br := bufio.NewReader(conn) + + b := make([]byte, len(PostgresStartTLSMsg)) + _, err = br.Read(b) + if err != nil { + conn.Close() + return + } + + hello, err := clientHelloInfo(br) + if err != nil { + conn.Close() + return + } + + if !hello.isTLS { + conn.Close() + return + } + + connData, err := tcpmuxer.NewConnData(hello.serverName, conn, hello.protos) + if err != nil { + log.WithoutContext().Errorf("Error while reading TCP connection data: %v", err) + conn.Close() + return + } + + // Contains also TCP TLS passthrough routes. + handlerTCPTLS, _ := r.muxerTCPTLS.Match(connData) + if handlerTCPTLS == nil { + conn.Close() + return + } + + // We are in TLS mode and if the handler is not TLSHandler, we are in passthrough. + proxiedConn := r.GetConn(conn, hello.peeked) + if _, ok := handlerTCPTLS.(*tcp.TLSHandler); !ok { + proxiedConn = &postgresConn{WriteCloser: proxiedConn} + } + + handlerTCPTLS.ServeTCP(proxiedConn) +} + +// postgresConn is a tcp.WriteCloser that will negotiate a TLS session (STARTTLS), +// before exchanging any data. +// It enforces that the STARTTLS negotiation with the peer is successful. +type postgresConn struct { + tcp.WriteCloser + + starttlsMsgSent bool // whether we have already sent the STARTTLS handshake to the backend. + starttlsReplyReceived bool // whether we have already received the STARTTLS handshake reply from the backend. + + // errChan makes sure that an error is returned if the first operation to ever + // happen on a postgresConn is a Write (because it should instead be a Read). + errChanMu sync.Mutex + errChan chan error +} + +// Read reads bytes from the underlying connection (tcp.WriteCloser). +// On first call, it actually only injects the PostgresStartTLSMsg, +// in order to behave as a Postgres TLS client that initiates a STARTTLS handshake. +// Read does not support concurrent calls. +func (c *postgresConn) Read(p []byte) (n int, err error) { + if c.starttlsMsgSent { + if err := <-c.errChan; err != nil { + return 0, err + } + + return c.WriteCloser.Read(p) + } + + defer func() { + c.starttlsMsgSent = true + c.errChanMu.Lock() + c.errChan = make(chan error) + c.errChanMu.Unlock() + }() + + copy(p, PostgresStartTLSMsg) + return len(PostgresStartTLSMsg), nil +} + +// Write writes bytes to the underlying connection (tcp.WriteCloser). +// On first call, it checks that the bytes to write (the ones provided by the backend) +// match the PostgresStartTLSReply, and if yes it drops them (as the STARTTLS +// handshake between the client and traefik has already taken place). Otherwise, an +// error is transmitted through c.errChan, so that the second Read call gets it and +// returns it up the stack. +// Write does not support concurrent calls. +func (c *postgresConn) Write(p []byte) (n int, err error) { + if c.starttlsReplyReceived { + return c.WriteCloser.Write(p) + } + + c.errChanMu.Lock() + if c.errChan == nil { + c.errChanMu.Unlock() + return 0, errors.New("initial read never happened") + } + c.errChanMu.Unlock() + + defer func() { + c.starttlsReplyReceived = true + }() + + if len(p) != 1 || p[0] != PostgresStartTLSReply[0] { + c.errChan <- errors.New("invalid response from Postgres server") + return len(p), nil + } + + close(c.errChan) + + return 1, nil +} diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index 0a41e80e2..841d52ae8 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -108,6 +108,17 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { // TODO -- Check if ProxyProtocol changes the first bytes of the request br := bufio.NewReader(conn) + postgres, err := isPostgres(br) + if err != nil { + conn.Close() + return + } + + if postgres { + r.servePostgres(r.GetConn(conn, getPeeked(br))) + return + } + hello, err := clientHelloInfo(br) if err != nil { conn.Close() @@ -277,7 +288,7 @@ func (r *Router) SetHTTPSHandler(handler http.Handler, config *tls.Config) { type Conn struct { // Peeked are the bytes that have been read from Conn for the // purposes of route matching, but have not yet been consumed - // by Read calls. It set to nil by Read when fully consumed. + // by Read calls. It is set to nil by Read when fully consumed. Peeked []byte // Conn is the underlying connection. diff --git a/pkg/server/router/tcp/router_test.go b/pkg/server/router/tcp/router_test.go index 26e799837..d97646749 100644 --- a/pkg/server/router/tcp/router_test.go +++ b/pkg/server/router/tcp/router_test.go @@ -922,3 +922,89 @@ func checkHTTPSTLS10(addr string, timeout time.Duration) error { func checkHTTPSTLS12(addr string, timeout time.Duration) error { return checkHTTPS(addr, timeout, tls.VersionTLS12) } + +func TestPostgres(t *testing.T) { + router, err := NewRouter() + require.NoError(t, err) + + // This test requires to have a TLS route, but does not actually check the + // content of the handler. It would require to code a TLS handshake to + // check the SNI and content of the handlerFunc. + err = router.AddRouteTLS("HostSNI(`test.localhost`)", 0, nil, &tls.Config{}) + require.NoError(t, err) + + err = router.AddRoute("HostSNI(`*`)", 0, tcp2.HandlerFunc(func(conn tcp2.WriteCloser) { + _, _ = conn.Write([]byte("OK")) + _ = conn.Close() + })) + require.NoError(t, err) + + mockConn := NewMockConn() + go router.ServeTCP(mockConn) + + mockConn.dataRead <- PostgresStartTLSMsg + b := <-mockConn.dataWrite + require.Equal(t, PostgresStartTLSReply, b) + + mockConn = NewMockConn() + go router.ServeTCP(mockConn) + + mockConn.dataRead <- []byte("HTTP") + b = <-mockConn.dataWrite + require.Equal(t, []byte("OK"), b) +} + +func NewMockConn() *MockConn { + return &MockConn{ + dataRead: make(chan []byte), + dataWrite: make(chan []byte), + } +} + +type MockConn struct { + dataRead chan []byte + dataWrite chan []byte +} + +func (m *MockConn) Read(b []byte) (n int, err error) { + temp := <-m.dataRead + copy(b, temp) + return len(temp), nil +} + +func (m *MockConn) Write(b []byte) (n int, err error) { + m.dataWrite <- b + return len(b), nil +} + +func (m *MockConn) Close() error { + close(m.dataRead) + close(m.dataWrite) + return nil +} + +func (m *MockConn) LocalAddr() net.Addr { + return nil +} + +func (m *MockConn) RemoteAddr() net.Addr { + return &net.TCPAddr{} +} + +func (m *MockConn) SetDeadline(t time.Time) error { + return nil +} + +func (m *MockConn) SetReadDeadline(t time.Time) error { + return nil +} + +func (m *MockConn) SetWriteDeadline(t time.Time) error { + return nil +} + +func (m *MockConn) CloseWrite() error { + close(m.dataRead) + close(m.dataWrite) + return nil +} diff --git a/pkg/server/routerfactory.go b/pkg/server/routerfactory.go index fff7e3eea..d2f0420f4 100644 --- a/pkg/server/routerfactory.go +++ b/pkg/server/routerfactory.go @@ -31,6 +31,8 @@ type RouterFactory struct { chainBuilder *middleware.ChainBuilder tlsManager *tls.Manager + + cancelPrevState func() } // NewRouterFactory creates a new RouterFactory. @@ -65,7 +67,12 @@ func NewRouterFactory(staticConfiguration static.Configuration, managerFactory * // CreateRouters creates new TCPRouters and UDPRouters. func (f *RouterFactory) CreateRouters(rtConf *runtime.Configuration) (map[string]*tcprouter.Router, map[string]udptypes.Handler) { - ctx := context.Background() + if f.cancelPrevState != nil { + f.cancelPrevState() + } + + var ctx context.Context + ctx, f.cancelPrevState = context.WithCancel(context.Background()) // HTTP serviceManager := f.managerFactory.Build(rtConf) @@ -77,7 +84,7 @@ func (f *RouterFactory) CreateRouters(rtConf *runtime.Configuration) (map[string handlersNonTLS := routerManager.BuildHandlers(ctx, f.entryPointsTCP, false) handlersTLS := routerManager.BuildHandlers(ctx, f.entryPointsTCP, true) - serviceManager.LaunchHealthCheck() + serviceManager.LaunchHealthCheck(ctx) // TCP svcTCPManager := tcp.NewManager(rtConf) diff --git a/pkg/server/service/internalhandler.go b/pkg/server/service/internalhandler.go index 4bd6fd258..e5c3ab26d 100644 --- a/pkg/server/service/internalhandler.go +++ b/pkg/server/service/internalhandler.go @@ -10,7 +10,7 @@ import ( type serviceManager interface { BuildHTTP(rootCtx context.Context, serviceName string) (http.Handler, error) - LaunchHealthCheck() + LaunchHealthCheck(ctx context.Context) } // InternalHandlers is the internal HTTP handlers builder. diff --git a/pkg/server/service/loadbalancer/wrr/wrr.go b/pkg/server/service/loadbalancer/wrr/wrr.go index d7261d95b..803764f49 100644 --- a/pkg/server/service/loadbalancer/wrr/wrr.go +++ b/pkg/server/service/loadbalancer/wrr/wrr.go @@ -4,7 +4,6 @@ import ( "container/heap" "context" "errors" - "fmt" "net/http" "sync" @@ -39,7 +38,7 @@ type Balancer struct { curDeadline float64 // status is a record of which child services of the Balancer are healthy, keyed // by name of child service. A service is initially added to the map when it is - // created via AddService, and it is later removed or added to the map as needed, + // created via Add, and it is later removed or added to the map as needed, // through the SetStatus method. status map[string]struct{} // updaters is the list of hooks that are run (to update the Balancer @@ -48,10 +47,10 @@ type Balancer struct { } // New creates a new load balancer. -func New(sticky *dynamic.Sticky, hc *dynamic.HealthCheck) *Balancer { +func New(sticky *dynamic.Sticky, wantHealthCheck bool) *Balancer { balancer := &Balancer{ status: make(map[string]struct{}), - wantsHealthCheck: hc != nil, + wantsHealthCheck: wantHealthCheck, } if sticky != nil && sticky.Cookie != nil { balancer.stickyCookie = &stickyCookie{ @@ -150,10 +149,7 @@ func (b *Balancer) nextServer() (*namedHandler, error) { b.mutex.Lock() defer b.mutex.Unlock() - if len(b.handlers) == 0 { - return nil, fmt.Errorf("no servers in the pool") - } - if len(b.status) == 0 { + if len(b.handlers) == 0 || len(b.status) == 0 { return nil, errNoAvailableServer } @@ -223,9 +219,9 @@ func (b *Balancer) ServeHTTP(w http.ResponseWriter, req *http.Request) { server.ServeHTTP(w, req) } -// AddService adds a handler. +// Add adds a handler. // A handler with a non-positive weight is ignored. -func (b *Balancer) AddService(name string, handler http.Handler, weight *int) { +func (b *Balancer) Add(name string, handler http.Handler, weight *int) { w := 1 if weight != nil { w = *weight diff --git a/pkg/server/service/loadbalancer/wrr/wrr_test.go b/pkg/server/service/loadbalancer/wrr/wrr_test.go index ac5f7e15b..7d6d6acad 100644 --- a/pkg/server/service/loadbalancer/wrr/wrr_test.go +++ b/pkg/server/service/loadbalancer/wrr/wrr_test.go @@ -10,31 +10,15 @@ import ( "github.com/traefik/traefik/v2/pkg/config/dynamic" ) -func Int(v int) *int { return &v } - -type responseRecorder struct { - *httptest.ResponseRecorder - save map[string]int - sequence []string - status []int -} - -func (r *responseRecorder) WriteHeader(statusCode int) { - r.save[r.Header().Get("server")]++ - r.sequence = append(r.sequence, r.Header().Get("server")) - r.status = append(r.status, statusCode) - r.ResponseRecorder.WriteHeader(statusCode) -} - func TestBalancer(t *testing.T) { - balancer := New(nil, nil) + balancer := New(nil, false) - balancer.AddService("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + balancer.Add("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("server", "first") rw.WriteHeader(http.StatusOK) }), Int(3)) - balancer.AddService("second", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + balancer.Add("second", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("server", "second") rw.WriteHeader(http.StatusOK) }), Int(1)) @@ -49,23 +33,23 @@ func TestBalancer(t *testing.T) { } func TestBalancerNoService(t *testing.T) { - balancer := New(nil, nil) + balancer := New(nil, false) recorder := httptest.NewRecorder() balancer.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/", nil)) - assert.Equal(t, http.StatusInternalServerError, recorder.Result().StatusCode) + assert.Equal(t, http.StatusServiceUnavailable, recorder.Result().StatusCode) } func TestBalancerOneServerZeroWeight(t *testing.T) { - balancer := New(nil, nil) + balancer := New(nil, false) - balancer.AddService("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + balancer.Add("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("server", "first") rw.WriteHeader(http.StatusOK) }), Int(1)) - balancer.AddService("second", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}), Int(0)) + balancer.Add("second", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}), Int(0)) recorder := &responseRecorder{ResponseRecorder: httptest.NewRecorder(), save: map[string]int{}} for i := 0; i < 3; i++ { @@ -80,13 +64,13 @@ type key string const serviceName key = "serviceName" func TestBalancerNoServiceUp(t *testing.T) { - balancer := New(nil, nil) + balancer := New(nil, false) - balancer.AddService("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + balancer.Add("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusInternalServerError) }), Int(1)) - balancer.AddService("second", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + balancer.Add("second", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusInternalServerError) }), Int(1)) @@ -100,14 +84,14 @@ func TestBalancerNoServiceUp(t *testing.T) { } func TestBalancerOneServerDown(t *testing.T) { - balancer := New(nil, nil) + balancer := New(nil, false) - balancer.AddService("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + balancer.Add("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("server", "first") rw.WriteHeader(http.StatusOK) }), Int(1)) - balancer.AddService("second", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + balancer.Add("second", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusInternalServerError) }), Int(1)) balancer.SetStatus(context.WithValue(context.Background(), serviceName, "parent"), "second", false) @@ -121,14 +105,14 @@ func TestBalancerOneServerDown(t *testing.T) { } func TestBalancerDownThenUp(t *testing.T) { - balancer := New(nil, nil) + balancer := New(nil, false) - balancer.AddService("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + balancer.Add("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("server", "first") rw.WriteHeader(http.StatusOK) }), Int(1)) - balancer.AddService("second", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + balancer.Add("second", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("server", "second") rw.WriteHeader(http.StatusOK) }), Int(1)) @@ -150,35 +134,35 @@ func TestBalancerDownThenUp(t *testing.T) { } func TestBalancerPropagate(t *testing.T) { - balancer1 := New(nil, &dynamic.HealthCheck{}) + balancer1 := New(nil, true) - balancer1.AddService("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + balancer1.Add("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("server", "first") rw.WriteHeader(http.StatusOK) }), Int(1)) - balancer1.AddService("second", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + balancer1.Add("second", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("server", "second") rw.WriteHeader(http.StatusOK) }), Int(1)) - balancer2 := New(nil, &dynamic.HealthCheck{}) - balancer2.AddService("third", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + balancer2 := New(nil, true) + balancer2.Add("third", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("server", "third") rw.WriteHeader(http.StatusOK) }), Int(1)) - balancer2.AddService("fourth", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + balancer2.Add("fourth", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("server", "fourth") rw.WriteHeader(http.StatusOK) }), Int(1)) - topBalancer := New(nil, &dynamic.HealthCheck{}) - topBalancer.AddService("balancer1", balancer1, Int(1)) + topBalancer := New(nil, true) + topBalancer.Add("balancer1", balancer1, Int(1)) _ = balancer1.RegisterStatusUpdater(func(up bool) { topBalancer.SetStatus(context.WithValue(context.Background(), serviceName, "top"), "balancer1", up) // TODO(mpl): if test gets flaky, add channel or something here to signal that // propagation is done, and wait on it before sending request. }) - topBalancer.AddService("balancer2", balancer2, Int(1)) + topBalancer.Add("balancer2", balancer2, Int(1)) _ = balancer2.RegisterStatusUpdater(func(up bool) { topBalancer.SetStatus(context.WithValue(context.Background(), serviceName, "top"), "balancer2", up) }) @@ -223,28 +207,28 @@ func TestBalancerPropagate(t *testing.T) { } func TestBalancerAllServersZeroWeight(t *testing.T) { - balancer := New(nil, nil) + balancer := New(nil, false) - balancer.AddService("test", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}), Int(0)) - balancer.AddService("test2", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}), Int(0)) + balancer.Add("test", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}), Int(0)) + balancer.Add("test2", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}), Int(0)) recorder := httptest.NewRecorder() balancer.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/", nil)) - assert.Equal(t, http.StatusInternalServerError, recorder.Result().StatusCode) + assert.Equal(t, http.StatusServiceUnavailable, recorder.Result().StatusCode) } func TestSticky(t *testing.T) { balancer := New(&dynamic.Sticky{ Cookie: &dynamic.Cookie{Name: "test"}, - }, nil) + }, false) - balancer.AddService("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + balancer.Add("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("server", "first") rw.WriteHeader(http.StatusOK) }), Int(1)) - balancer.AddService("second", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + balancer.Add("second", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("server", "second") rw.WriteHeader(http.StatusOK) }), Int(2)) @@ -268,14 +252,14 @@ func TestSticky(t *testing.T) { // TestBalancerBias makes sure that the WRR algorithm spreads elements evenly right from the start, // and that it does not "over-favor" the high-weighted ones with a biased start-up regime. func TestBalancerBias(t *testing.T) { - balancer := New(nil, nil) + balancer := New(nil, false) - balancer.AddService("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + balancer.Add("first", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("server", "A") rw.WriteHeader(http.StatusOK) }), Int(11)) - balancer.AddService("second", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + balancer.Add("second", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("server", "B") rw.WriteHeader(http.StatusOK) }), Int(3)) @@ -290,3 +274,19 @@ func TestBalancerBias(t *testing.T) { assert.Equal(t, wantSequence, recorder.sequence) } + +func Int(v int) *int { return &v } + +type responseRecorder struct { + *httptest.ResponseRecorder + save map[string]int + sequence []string + status []int +} + +func (r *responseRecorder) WriteHeader(statusCode int) { + r.save[r.Header().Get("server")]++ + r.sequence = append(r.sequence, r.Header().Get("server")) + r.status = append(r.status, statusCode) + r.ResponseRecorder.WriteHeader(statusCode) +} diff --git a/pkg/server/service/proxy.go b/pkg/server/service/proxy.go index 12ce95ce7..43a2c0f28 100644 --- a/pkg/server/service/proxy.go +++ b/pkg/server/service/proxy.go @@ -3,7 +3,6 @@ package service import ( "context" "errors" - "fmt" "io" "net" "net/http" @@ -12,8 +11,6 @@ import ( "strings" "time" - ptypes "github.com/traefik/paerser/types" - "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/log" "golang.org/x/net/http/httpguts" ) @@ -24,103 +21,107 @@ const StatusClientClosedRequest = 499 // StatusClientClosedRequestText non-standard HTTP status for client disconnection. const StatusClientClosedRequestText = "Client Closed Request" -func buildProxy(passHostHeader *bool, responseForwarding *dynamic.ResponseForwarding, roundTripper http.RoundTripper, bufferPool httputil.BufferPool) (http.Handler, error) { - var flushInterval ptypes.Duration - if responseForwarding != nil { - err := flushInterval.Set(responseForwarding.FlushInterval) - if err != nil { - return nil, fmt.Errorf("error creating flush interval: %w", err) - } - } - if flushInterval == 0 { - flushInterval = ptypes.Duration(100 * time.Millisecond) - } - - proxy := &httputil.ReverseProxy{ - Director: func(outReq *http.Request) { - u := outReq.URL - if outReq.RequestURI != "" { - parsedURL, err := url.ParseRequestURI(outReq.RequestURI) - if err == nil { - u = parsedURL - } - } - - outReq.URL.Path = u.Path - outReq.URL.RawPath = u.RawPath - outReq.URL.RawQuery = strings.ReplaceAll(u.RawQuery, ";", "&") - outReq.RequestURI = "" // Outgoing request should not have RequestURI - - outReq.Proto = "HTTP/1.1" - outReq.ProtoMajor = 1 - outReq.ProtoMinor = 1 - - if _, ok := outReq.Header["User-Agent"]; !ok { - outReq.Header.Set("User-Agent", "") - } - - // Do not pass client Host header unless optsetter PassHostHeader is set. - if passHostHeader != nil && !*passHostHeader { - outReq.Host = outReq.URL.Host - } - - // Even if the websocket RFC says that headers should be case-insensitive, - // some servers need Sec-WebSocket-Key, Sec-WebSocket-Extensions, Sec-WebSocket-Accept, - // Sec-WebSocket-Protocol and Sec-WebSocket-Version to be case-sensitive. - // https://tools.ietf.org/html/rfc6455#page-20 - if isWebSocketUpgrade(outReq) { - outReq.Header["Sec-WebSocket-Key"] = outReq.Header["Sec-Websocket-Key"] - outReq.Header["Sec-WebSocket-Extensions"] = outReq.Header["Sec-Websocket-Extensions"] - outReq.Header["Sec-WebSocket-Accept"] = outReq.Header["Sec-Websocket-Accept"] - outReq.Header["Sec-WebSocket-Protocol"] = outReq.Header["Sec-Websocket-Protocol"] - outReq.Header["Sec-WebSocket-Version"] = outReq.Header["Sec-Websocket-Version"] - delete(outReq.Header, "Sec-Websocket-Key") - delete(outReq.Header, "Sec-Websocket-Extensions") - delete(outReq.Header, "Sec-Websocket-Accept") - delete(outReq.Header, "Sec-Websocket-Protocol") - delete(outReq.Header, "Sec-Websocket-Version") - } - }, +func buildSingleHostProxy(target *url.URL, passHostHeader bool, flushInterval time.Duration, roundTripper http.RoundTripper, bufferPool httputil.BufferPool) http.Handler { + return &httputil.ReverseProxy{ + Director: directorBuilder(target, passHostHeader), Transport: roundTripper, - FlushInterval: time.Duration(flushInterval), + FlushInterval: flushInterval, BufferPool: bufferPool, - ErrorHandler: func(w http.ResponseWriter, request *http.Request, err error) { - statusCode := http.StatusInternalServerError + ErrorHandler: errorHandler, + } +} - switch { - case errors.Is(err, io.EOF): - statusCode = http.StatusBadGateway - case errors.Is(err, context.Canceled): - statusCode = StatusClientClosedRequest - default: - var netErr net.Error - if errors.As(err, &netErr) { - if netErr.Timeout() { - statusCode = http.StatusGatewayTimeout - } else { - statusCode = http.StatusBadGateway - } - } - } +func directorBuilder(target *url.URL, passHostHeader bool) func(req *http.Request) { + return func(outReq *http.Request) { + outReq.URL.Scheme = target.Scheme + outReq.URL.Host = target.Host - log.Debugf("'%d %s' caused by: %v", statusCode, statusText(statusCode), err) - w.WriteHeader(statusCode) - _, werr := w.Write([]byte(statusText(statusCode))) - if werr != nil { - log.Debugf("Error while writing status code", werr) + u := outReq.URL + if outReq.RequestURI != "" { + parsedURL, err := url.ParseRequestURI(outReq.RequestURI) + if err == nil { + u = parsedURL } - }, + } + + outReq.URL.Path = u.Path + outReq.URL.RawPath = u.RawPath + outReq.URL.RawQuery = strings.ReplaceAll(u.RawQuery, ";", "&") + outReq.RequestURI = "" // Outgoing request should not have RequestURI + + outReq.Proto = "HTTP/1.1" + outReq.ProtoMajor = 1 + outReq.ProtoMinor = 1 + + if _, ok := outReq.Header["User-Agent"]; !ok { + outReq.Header.Set("User-Agent", "") + } + + // Do not pass client Host header unless PassHostHeader is set. + if !passHostHeader { + outReq.Host = outReq.URL.Host + } + + cleanWebSocketHeaders(outReq) + } +} + +// cleanWebSocketHeaders Even if the websocket RFC says that headers should be case-insensitive, +// some servers need Sec-WebSocket-Key, Sec-WebSocket-Extensions, Sec-WebSocket-Accept, +// Sec-WebSocket-Protocol and Sec-WebSocket-Version to be case-sensitive. +// https://tools.ietf.org/html/rfc6455#page-20 +func cleanWebSocketHeaders(req *http.Request) { + if !isWebSocketUpgrade(req) { + return } - return proxy, nil + req.Header["Sec-WebSocket-Key"] = req.Header["Sec-Websocket-Key"] + delete(req.Header, "Sec-Websocket-Key") + + req.Header["Sec-WebSocket-Extensions"] = req.Header["Sec-Websocket-Extensions"] + delete(req.Header, "Sec-Websocket-Extensions") + + req.Header["Sec-WebSocket-Accept"] = req.Header["Sec-Websocket-Accept"] + delete(req.Header, "Sec-Websocket-Accept") + + req.Header["Sec-WebSocket-Protocol"] = req.Header["Sec-Websocket-Protocol"] + delete(req.Header, "Sec-Websocket-Protocol") + + req.Header["Sec-WebSocket-Version"] = req.Header["Sec-Websocket-Version"] + delete(req.Header, "Sec-Websocket-Version") } func isWebSocketUpgrade(req *http.Request) bool { - if !httpguts.HeaderValuesContainsToken(req.Header["Connection"], "Upgrade") { - return false + return httpguts.HeaderValuesContainsToken(req.Header["Connection"], "Upgrade") && + strings.EqualFold(req.Header.Get("Upgrade"), "websocket") +} + +func errorHandler(w http.ResponseWriter, req *http.Request, err error) { + statusCode := http.StatusInternalServerError + + switch { + case errors.Is(err, io.EOF): + statusCode = http.StatusBadGateway + case errors.Is(err, context.Canceled): + statusCode = StatusClientClosedRequest + default: + var netErr net.Error + if errors.As(err, &netErr) { + if netErr.Timeout() { + statusCode = http.StatusGatewayTimeout + } else { + statusCode = http.StatusBadGateway + } + } } - return strings.EqualFold(req.Header.Get("Upgrade"), "websocket") + logger := log.FromContext(req.Context()) + logger.Debugf("'%d %s' caused by: %v", statusCode, statusText(statusCode), err) + + w.WriteHeader(statusCode) + if _, werr := w.Write([]byte(statusText(statusCode))); werr != nil { + logger.Debugf("Error while writing status code", werr) + } } func statusText(statusCode int) string { diff --git a/pkg/server/service/proxy_test.go b/pkg/server/service/proxy_test.go index 3a0a34553..8abc51cd2 100644 --- a/pkg/server/service/proxy_test.go +++ b/pkg/server/service/proxy_test.go @@ -28,7 +28,7 @@ func BenchmarkProxy(b *testing.B) { req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil) pool := newBufferPool() - handler, _ := buildProxy(Bool(false), nil, &staticTransport{res}, pool) + handler := buildSingleHostProxy(req.URL, false, 0, &staticTransport{res}, pool) b.ReportAllocs() for i := 0; i < b.N; i++ { diff --git a/pkg/server/service/proxy_websocket_test.go b/pkg/server/service/proxy_websocket_test.go index 84e6a1aef..d8726a795 100644 --- a/pkg/server/service/proxy_websocket_test.go +++ b/pkg/server/service/proxy_websocket_test.go @@ -18,12 +18,7 @@ import ( "golang.org/x/net/websocket" ) -func Bool(v bool) *bool { return &v } - func TestWebSocketTCPClose(t *testing.T) { - f, err := buildProxy(Bool(true), nil, http.DefaultTransport, nil) - require.NoError(t, err) - errChan := make(chan error, 1) upgrader := gorillawebsocket.Upgrader{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -42,7 +37,7 @@ func TestWebSocketTCPClose(t *testing.T) { })) defer srv.Close() - proxy := createProxyWithForwarder(t, f, srv.URL) + proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport) proxyAddr := proxy.Listener.Addr().String() _, conn, err := newWebsocketRequest( @@ -61,10 +56,6 @@ func TestWebSocketTCPClose(t *testing.T) { } func TestWebSocketPingPong(t *testing.T) { - f, err := buildProxy(Bool(true), nil, http.DefaultTransport, nil) - - require.NoError(t, err) - upgrader := gorillawebsocket.Upgrader{ HandshakeTimeout: 10 * time.Second, CheckOrigin: func(*http.Request) bool { @@ -86,17 +77,10 @@ func TestWebSocketPingPong(t *testing.T) { _, _, _ = ws.ReadMessage() }) - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - mux.ServeHTTP(w, req) - })) + srv := httptest.NewServer(mux) defer srv.Close() - proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - req.URL = parseURI(t, srv.URL) - f.ServeHTTP(w, req) - })) - defer proxy.Close() - + proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport) serverAddr := proxy.Listener.Addr().String() headers := http.Header{} @@ -127,9 +111,6 @@ func TestWebSocketPingPong(t *testing.T) { } func TestWebSocketEcho(t *testing.T) { - f, err := buildProxy(Bool(true), nil, http.DefaultTransport, nil) - require.NoError(t, err) - mux := http.NewServeMux() mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) { msg := make([]byte, 4) @@ -145,17 +126,10 @@ func TestWebSocketEcho(t *testing.T) { require.NoError(t, err) })) - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - mux.ServeHTTP(w, req) - })) + srv := httptest.NewServer(mux) defer srv.Close() - proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - req.URL = parseURI(t, srv.URL) - f.ServeHTTP(w, req) - })) - defer proxy.Close() - + proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport) serverAddr := proxy.Listener.Addr().String() headers := http.Header{} @@ -193,10 +167,6 @@ func TestWebSocketPassHost(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - f, err := buildProxy(Bool(test.passHost), nil, http.DefaultTransport, nil) - - require.NoError(t, err) - mux := http.NewServeMux() mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) { req := conn.Request() @@ -208,7 +178,7 @@ func TestWebSocketPassHost(t *testing.T) { } msg := make([]byte, 4) - _, err = conn.Read(msg) + _, err := conn.Read(msg) require.NoError(t, err) fmt.Println(string(msg)) @@ -219,16 +189,10 @@ func TestWebSocketPassHost(t *testing.T) { require.NoError(t, err) })) - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - mux.ServeHTTP(w, req) - })) + srv := httptest.NewServer(mux) defer srv.Close() - proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - req.URL = parseURI(t, srv.URL) - f.ServeHTTP(w, req) - })) - defer proxy.Close() + proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport) serverAddr := proxy.Listener.Addr().String() @@ -252,9 +216,6 @@ func TestWebSocketPassHost(t *testing.T) { } func TestWebSocketServerWithoutCheckOrigin(t *testing.T) { - f, err := buildProxy(Bool(true), nil, http.DefaultTransport, nil) - require.NoError(t, err) - upgrader := gorillawebsocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }} @@ -277,7 +238,7 @@ func TestWebSocketServerWithoutCheckOrigin(t *testing.T) { })) defer srv.Close() - proxy := createProxyWithForwarder(t, f, srv.URL) + proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport) defer proxy.Close() proxyAddr := proxy.Listener.Addr().String() @@ -293,9 +254,6 @@ func TestWebSocketServerWithoutCheckOrigin(t *testing.T) { } func TestWebSocketRequestWithOrigin(t *testing.T) { - f, err := buildProxy(Bool(true), nil, http.DefaultTransport, nil) - require.NoError(t, err) - upgrader := gorillawebsocket.Upgrader{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := upgrader.Upgrade(w, r, nil) @@ -316,11 +274,11 @@ func TestWebSocketRequestWithOrigin(t *testing.T) { })) defer srv.Close() - proxy := createProxyWithForwarder(t, f, srv.URL) + proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport) defer proxy.Close() proxyAddr := proxy.Listener.Addr().String() - _, err = newWebsocketRequest( + _, err := newWebsocketRequest( withServer(proxyAddr), withPath("/ws"), withData("echo"), @@ -339,9 +297,6 @@ func TestWebSocketRequestWithOrigin(t *testing.T) { } func TestWebSocketRequestWithQueryParams(t *testing.T) { - f, err := buildProxy(Bool(true), nil, http.DefaultTransport, nil) - require.NoError(t, err) - upgrader := gorillawebsocket.Upgrader{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) @@ -363,7 +318,7 @@ func TestWebSocketRequestWithQueryParams(t *testing.T) { })) defer srv.Close() - proxy := createProxyWithForwarder(t, f, srv.URL) + proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport) defer proxy.Close() proxyAddr := proxy.Listener.Addr().String() @@ -379,18 +334,14 @@ func TestWebSocketRequestWithQueryParams(t *testing.T) { } func TestWebSocketRequestWithHeadersInResponseWriter(t *testing.T) { - f, err := buildProxy(Bool(true), nil, http.DefaultTransport, nil) - require.NoError(t, err) - mux := http.NewServeMux() mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) { conn.Close() })) - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - mux.ServeHTTP(w, req) - })) + srv := httptest.NewServer(mux) defer srv.Close() + f := buildSingleHostProxy(parseURI(t, srv.URL), true, 0, http.DefaultTransport, nil) proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { req.URL = parseURI(t, srv.URL) w.Header().Set("HEADER-KEY", "HEADER-VALUE") @@ -403,6 +354,7 @@ func TestWebSocketRequestWithHeadersInResponseWriter(t *testing.T) { headers := http.Header{} webSocketURL := "ws://" + serverAddr + "/ws" headers.Add("Origin", webSocketURL) + conn, resp, err := gorillawebsocket.DefaultDialer.Dial(webSocketURL, headers) require.NoError(t, err, "Error during Dial with response: %+v", err, resp) defer conn.Close() @@ -411,9 +363,6 @@ func TestWebSocketRequestWithHeadersInResponseWriter(t *testing.T) { } func TestWebSocketRequestWithEncodedChar(t *testing.T) { - f, err := buildProxy(Bool(true), nil, http.DefaultTransport, nil) - require.NoError(t, err) - upgrader := gorillawebsocket.Upgrader{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) @@ -435,7 +384,7 @@ func TestWebSocketRequestWithEncodedChar(t *testing.T) { })) defer srv.Close() - proxy := createProxyWithForwarder(t, f, srv.URL) + proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport) defer proxy.Close() proxyAddr := proxy.Listener.Addr().String() @@ -451,18 +400,14 @@ func TestWebSocketRequestWithEncodedChar(t *testing.T) { } func TestWebSocketUpgradeFailed(t *testing.T) { - f, err := buildProxy(Bool(true), nil, http.DefaultTransport, nil) - require.NoError(t, err) - mux := http.NewServeMux() mux.HandleFunc("/ws", func(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusBadRequest) }) - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - mux.ServeHTTP(w, req) - })) + srv := httptest.NewServer(mux) defer srv.Close() + f := buildSingleHostProxy(parseURI(t, srv.URL), true, 0, http.DefaultTransport, nil) proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { path := req.URL.Path // keep the original path @@ -501,9 +446,6 @@ func TestWebSocketUpgradeFailed(t *testing.T) { } func TestForwardsWebsocketTraffic(t *testing.T) { - f, err := buildProxy(Bool(true), nil, http.DefaultTransport, nil) - require.NoError(t, err) - mux := http.NewServeMux() mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) { _, err := conn.Write([]byte("ok")) @@ -512,12 +454,10 @@ func TestForwardsWebsocketTraffic(t *testing.T) { err = conn.Close() require.NoError(t, err) })) - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - mux.ServeHTTP(w, req) - })) + srv := httptest.NewServer(mux) defer srv.Close() - proxy := createProxyWithForwarder(t, f, srv.URL) + proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport) defer proxy.Close() proxyAddr := proxy.Listener.Addr().String() @@ -557,15 +497,12 @@ func TestWebSocketTransferTLSConfig(t *testing.T) { srv := createTLSWebsocketServer() defer srv.Close() - forwarderWithoutTLSConfig, err := buildProxy(Bool(true), nil, http.DefaultTransport, nil) - require.NoError(t, err) - - proxyWithoutTLSConfig := createProxyWithForwarder(t, forwarderWithoutTLSConfig, srv.URL) + proxyWithoutTLSConfig := createProxyWithForwarder(t, srv.URL, http.DefaultTransport) defer proxyWithoutTLSConfig.Close() proxyAddr := proxyWithoutTLSConfig.Listener.Addr().String() - _, err = newWebsocketRequest( + _, err := newWebsocketRequest( withServer(proxyAddr), withPath("/ws"), withData("ok"), @@ -576,10 +513,8 @@ func TestWebSocketTransferTLSConfig(t *testing.T) { transport := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } - forwarderWithTLSConfig, err := buildProxy(Bool(true), nil, transport, nil) - require.NoError(t, err) - proxyWithTLSConfig := createProxyWithForwarder(t, forwarderWithTLSConfig, srv.URL) + proxyWithTLSConfig := createProxyWithForwarder(t, srv.URL, transport) defer proxyWithTLSConfig.Close() proxyAddr = proxyWithTLSConfig.Listener.Addr().String() @@ -597,10 +532,7 @@ func TestWebSocketTransferTLSConfig(t *testing.T) { defaultTransport := http.DefaultTransport.(*http.Transport).Clone() defaultTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - forwarderWithTLSConfigFromDefaultTransport, err := buildProxy(Bool(true), nil, defaultTransport, nil) - require.NoError(t, err) - - proxyWithTLSConfigFromDefaultTransport := createProxyWithForwarder(t, forwarderWithTLSConfigFromDefaultTransport, srv.URL) + proxyWithTLSConfigFromDefaultTransport := createProxyWithForwarder(t, srv.URL, defaultTransport) defer proxyWithTLSConfig.Close() proxyAddr = proxyWithTLSConfigFromDefaultTransport.Listener.Addr().String() @@ -705,15 +637,19 @@ func parseURI(t *testing.T, uri string) *url.URL { return out } -func createProxyWithForwarder(t *testing.T, proxy http.Handler, url string) *httptest.Server { +func createProxyWithForwarder(t *testing.T, uri string, transport http.RoundTripper) *httptest.Server { t.Helper() - return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + u := parseURI(t, uri) + proxy := buildSingleHostProxy(u, true, 0, transport, nil) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { path := req.URL.Path // keep the original path // Set new backend URL - req.URL = parseURI(t, url) + req.URL = u req.URL.Path = path proxy.ServeHTTP(w, req) })) + t.Cleanup(srv.Close) + return srv } diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go index 2c46da74f..d872a3607 100644 --- a/pkg/server/service/service.go +++ b/pkg/server/service/service.go @@ -4,36 +4,28 @@ import ( "context" "errors" "fmt" + "hash/fnv" "math/rand" "net/http" "net/http/httputil" "net/url" "reflect" + "strings" "time" - "github.com/containous/alice" "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/config/runtime" "github.com/traefik/traefik/v2/pkg/healthcheck" "github.com/traefik/traefik/v2/pkg/log" "github.com/traefik/traefik/v2/pkg/metrics" "github.com/traefik/traefik/v2/pkg/middlewares/accesslog" - "github.com/traefik/traefik/v2/pkg/middlewares/emptybackendhandler" metricsMiddle "github.com/traefik/traefik/v2/pkg/middlewares/metrics" - "github.com/traefik/traefik/v2/pkg/middlewares/pipelining" "github.com/traefik/traefik/v2/pkg/safe" "github.com/traefik/traefik/v2/pkg/server/cookie" "github.com/traefik/traefik/v2/pkg/server/provider" "github.com/traefik/traefik/v2/pkg/server/service/loadbalancer/failover" "github.com/traefik/traefik/v2/pkg/server/service/loadbalancer/mirror" "github.com/traefik/traefik/v2/pkg/server/service/loadbalancer/wrr" - "github.com/vulcand/oxy/roundrobin" - "github.com/vulcand/oxy/roundrobin/stickycookie" -) - -const ( - defaultHealthCheckInterval = 30 * time.Second - defaultHealthCheckTimeout = 5 * time.Second ) const defaultMaxBodySize int64 = -1 @@ -43,6 +35,19 @@ type RoundTripperGetter interface { Get(name string) (http.RoundTripper, error) } +// Manager The service manager. +type Manager struct { + routinePool *safe.Pool + metricsRegistry metrics.Registry + bufferPool httputil.BufferPool + roundTripperManager RoundTripperGetter + + services map[string]http.Handler + configs map[string]*runtime.ServiceInfo + healthCheckers map[string]*healthcheck.ServiceHealthChecker + rand *rand.Rand // For the initial shuffling of load-balancers. +} + // NewManager creates a new Manager. func NewManager(configs map[string]*runtime.ServiceInfo, metricsRegistry metrics.Registry, routinePool *safe.Pool, roundTripperManager RoundTripperGetter) *Manager { return &Manager{ @@ -50,27 +55,13 @@ func NewManager(configs map[string]*runtime.ServiceInfo, metricsRegistry metrics metricsRegistry: metricsRegistry, bufferPool: newBufferPool(), roundTripperManager: roundTripperManager, - balancers: make(map[string]healthcheck.Balancers), + services: make(map[string]http.Handler), configs: configs, + healthCheckers: make(map[string]*healthcheck.ServiceHealthChecker), rand: rand.New(rand.NewSource(time.Now().UnixNano())), } } -// Manager The service manager. -type Manager struct { - routinePool *safe.Pool - metricsRegistry metrics.Registry - bufferPool httputil.BufferPool - roundTripperManager RoundTripperGetter - // balancers is the map of all Balancers, keyed by service name. - // There is one Balancer per service handler, and there is one service handler per reference to a service - // (e.g. if 2 routers refer to the same service name, 2 service handlers are created), - // which is why there is not just one Balancer per service name. - balancers map[string]healthcheck.Balancers - configs map[string]*runtime.ServiceInfo - rand *rand.Rand // For the initial shuffling of load-balancers. -} - // BuildHTTP Creates a http.Handler for a service configuration. func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string) (http.Handler, error) { ctx := log.With(rootCtx, log.Str(log.ServiceName, serviceName)) @@ -78,11 +69,20 @@ func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string) (http.H serviceName = provider.GetQualifiedName(ctx, serviceName) ctx = provider.AddInContext(ctx, serviceName) + handler, ok := m.services[serviceName] + if ok { + return handler, nil + } + conf, ok := m.configs[serviceName] if !ok { return nil, fmt.Errorf("the service %q does not exist", serviceName) } + if conf.Status == runtime.StatusDisabled { + return nil, errors.New(strings.Join(conf.Err, ", ")) + } + value := reflect.ValueOf(*conf.Service) var count int for i := 0; i < value.NumField(); i++ { @@ -101,7 +101,7 @@ func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string) (http.H switch { case conf.LoadBalancer != nil: var err error - lb, err = m.getLoadBalancerServiceHandler(ctx, serviceName, conf.LoadBalancer) + lb, err = m.getLoadBalancerServiceHandler(ctx, serviceName, conf) if err != nil { conf.AddError(err, true) return nil, err @@ -133,6 +133,8 @@ func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string) (http.H return nil, sErr } + m.services[serviceName] = lb + return lb, nil } @@ -214,14 +216,14 @@ func (m *Manager) getWRRServiceHandler(ctx context.Context, serviceName string, config.Sticky.Cookie.Name = cookie.GetName(config.Sticky.Cookie.Name, serviceName) } - balancer := wrr.New(config.Sticky, config.HealthCheck) + balancer := wrr.New(config.Sticky, config.HealthCheck != nil) for _, service := range shuffle(config.Services, m.rand) { serviceHandler, err := m.BuildHTTP(ctx, service.Name) if err != nil { return nil, err } - balancer.AddService(service.Name, serviceHandler, service.Weight) + balancer.Add(service.Name, serviceHandler, service.Weight) if config.HealthCheck == nil { continue @@ -245,216 +247,91 @@ func (m *Manager) getWRRServiceHandler(ctx context.Context, serviceName string, return balancer, nil } -func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName string, service *dynamic.ServersLoadBalancer) (http.Handler, error) { - if service.PassHostHeader == nil { - defaultPassHostHeader := true - service.PassHostHeader = &defaultPassHostHeader +func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName string, info *runtime.ServiceInfo) (http.Handler, error) { + service := info.LoadBalancer + + logger := log.FromContext(ctx) + logger.Debug("Creating load-balancer") + + // TODO: should we keep this config value as Go is now handling stream response correctly? + flushInterval := dynamic.DefaultFlushInterval + if service.ResponseForwarding != nil { + flushInterval = service.ResponseForwarding.FlushInterval } if len(service.ServersTransport) > 0 { service.ServersTransport = provider.GetQualifiedName(ctx, service.ServersTransport) } + if service.Sticky != nil && service.Sticky.Cookie != nil { + service.Sticky.Cookie.Name = cookie.GetName(service.Sticky.Cookie.Name, serviceName) + } + + // We make sure that the PassHostHeader value is defined to avoid panics. + passHostHeader := dynamic.DefaultPassHostHeader + if service.PassHostHeader != nil { + passHostHeader = *service.PassHostHeader + } + roundTripper, err := m.roundTripperManager.Get(service.ServersTransport) if err != nil { return nil, err } - fwd, err := buildProxy(service.PassHostHeader, service.ResponseForwarding, roundTripper, m.bufferPool) - if err != nil { - return nil, err + lb := wrr.New(service.Sticky, service.HealthCheck != nil) + healthCheckTargets := make(map[string]*url.URL) + + for _, server := range shuffle(service.Servers, m.rand) { + hasher := fnv.New64a() + _, _ = hasher.Write([]byte(server.URL)) // this will never return an error. + + proxyName := fmt.Sprintf("%x", hasher.Sum(nil)) + + target, err := url.Parse(server.URL) + if err != nil { + return nil, fmt.Errorf("error parsing server URL %s: %w", server.URL, err) + } + + logger.WithField(log.ServerName, proxyName).Debugf("Creating server %s", target) + + proxy := buildSingleHostProxy(target, passHostHeader, time.Duration(flushInterval), roundTripper, m.bufferPool) + + proxy = accesslog.NewFieldHandler(proxy, accesslog.ServiceURL, target.String(), nil) + proxy = accesslog.NewFieldHandler(proxy, accesslog.ServiceAddr, target.Host, nil) + proxy = accesslog.NewFieldHandler(proxy, accesslog.ServiceName, serviceName, nil) + + if m.metricsRegistry != nil && m.metricsRegistry.IsSvcEnabled() { + proxy = metricsMiddle.NewServiceMiddleware(ctx, proxy, m.metricsRegistry, serviceName) + } + + lb.Add(proxyName, proxy, nil) + + // servers are considered UP by default. + info.UpdateServerStatus(target.String(), runtime.StatusUp) + + healthCheckTargets[proxyName] = target } - alHandler := func(next http.Handler) (http.Handler, error) { - return accesslog.NewFieldHandler(next, accesslog.ServiceName, serviceName, accesslog.AddServiceFields), nil - } - chain := alice.New() - if m.metricsRegistry != nil && m.metricsRegistry.IsSvcEnabled() { - chain = chain.Append(metricsMiddle.WrapServiceHandler(ctx, m.metricsRegistry, serviceName)) + if service.HealthCheck != nil { + m.healthCheckers[serviceName] = healthcheck.NewServiceHealthChecker( + ctx, + m.metricsRegistry, + service.HealthCheck, + lb, + info, + roundTripper, + healthCheckTargets, + ) } - handler, err := chain.Append(alHandler).Then(pipelining.New(ctx, fwd, "pipelining")) - if err != nil { - return nil, err - } - - balancer, err := m.getLoadBalancer(ctx, serviceName, service, handler) - if err != nil { - return nil, err - } - - // TODO rename and checks - m.balancers[serviceName] = append(m.balancers[serviceName], balancer) - - // Empty (backend with no servers) - return emptybackendhandler.New(balancer), nil + return lb, nil } // LaunchHealthCheck launches the health checks. -func (m *Manager) LaunchHealthCheck() { - backendConfigs := make(map[string]*healthcheck.BackendConfig) - - for serviceName, balancers := range m.balancers { - ctx := log.With(context.Background(), log.Str(log.ServiceName, serviceName)) - - service := m.configs[serviceName].LoadBalancer - - // Health Check - hcOpts := buildHealthCheckOptions(ctx, balancers, serviceName, service.HealthCheck) - if hcOpts == nil { - continue - } - hcOpts.Transport, _ = m.roundTripperManager.Get(service.ServersTransport) - log.FromContext(ctx).Debugf("Setting up healthcheck for service %s with %s", serviceName, *hcOpts) - - backendConfigs[serviceName] = healthcheck.NewBackendConfig(*hcOpts, serviceName) - } - - healthcheck.GetHealthCheck(m.metricsRegistry).SetBackendsConfiguration(context.Background(), backendConfigs) -} - -func buildHealthCheckOptions(ctx context.Context, lb healthcheck.Balancer, backend string, hc *dynamic.ServerHealthCheck) *healthcheck.Options { - if hc == nil { - return nil - } - - logger := log.FromContext(ctx) - - if hc.Path == "" { - logger.Errorf("Ignoring heath check configuration for '%s': no path provided", backend) - return nil - } - - interval := defaultHealthCheckInterval - if hc.Interval != "" { - intervalOverride, err := time.ParseDuration(hc.Interval) - switch { - case err != nil: - logger.Errorf("Illegal health check interval for '%s': %s", backend, err) - case intervalOverride <= 0: - logger.Errorf("Health check interval smaller than zero for service '%s'", backend) - default: - interval = intervalOverride - } - } - - timeout := defaultHealthCheckTimeout - if hc.Timeout != "" { - timeoutOverride, err := time.ParseDuration(hc.Timeout) - switch { - case err != nil: - logger.Errorf("Illegal health check timeout for backend '%s': %s", backend, err) - case timeoutOverride <= 0: - logger.Errorf("Health check timeout smaller than zero for backend '%s', backend", backend) - default: - timeout = timeoutOverride - } - } - - if timeout >= interval { - logger.Warnf("Health check timeout for backend '%s' should be lower than the health check interval. Interval set to timeout + 1 second (%s).", backend, interval) - } - - followRedirects := true - if hc.FollowRedirects != nil { - followRedirects = *hc.FollowRedirects - } - - mode := healthcheck.HTTPMode - switch hc.Mode { - case "": - mode = healthcheck.HTTPMode - case healthcheck.GRPCMode, healthcheck.HTTPMode: - mode = hc.Mode - default: - logger.Errorf("Illegal health check mode for backend '%s'", backend) - } - - return &healthcheck.Options{ - Scheme: hc.Scheme, - Mode: mode, - Path: hc.Path, - Method: hc.Method, - Port: hc.Port, - Interval: interval, - Timeout: timeout, - LB: lb, - Hostname: hc.Hostname, - Headers: hc.Headers, - FollowRedirects: followRedirects, - } -} - -func (m *Manager) getLoadBalancer(ctx context.Context, serviceName string, service *dynamic.ServersLoadBalancer, fwd http.Handler) (healthcheck.BalancerStatusHandler, error) { - logger := log.FromContext(ctx) - logger.Debug("Creating load-balancer") - - var options []roundrobin.LBOption - - var cookieName string - if service.Sticky != nil && service.Sticky.Cookie != nil { - cookieName = cookie.GetName(service.Sticky.Cookie.Name, serviceName) - - opts := roundrobin.CookieOptions{ - HTTPOnly: service.Sticky.Cookie.HTTPOnly, - Secure: service.Sticky.Cookie.Secure, - SameSite: convertSameSite(service.Sticky.Cookie.SameSite), - } - - // Sticky Cookie Value - cv, err := stickycookie.NewFallbackValue(&stickycookie.RawValue{}, &stickycookie.HashValue{}) - if err != nil { - return nil, err - } - - options = append(options, roundrobin.EnableStickySession(roundrobin.NewStickySessionWithOptions(cookieName, opts).SetCookieValue(cv))) - - logger.Debugf("Sticky session cookie name: %v", cookieName) - } - - lb, err := roundrobin.New(fwd, options...) - if err != nil { - return nil, err - } - - lbsu := healthcheck.NewLBStatusUpdater(lb, m.configs[serviceName], service.HealthCheck) - if err := m.upsertServers(ctx, lbsu, service.Servers); err != nil { - return nil, fmt.Errorf("error configuring load balancer for service %s: %w", serviceName, err) - } - - return lbsu, nil -} - -func (m *Manager) upsertServers(ctx context.Context, lb healthcheck.BalancerHandler, servers []dynamic.Server) error { - logger := log.FromContext(ctx) - - for name, srv := range shuffle(servers, m.rand) { - u, err := url.Parse(srv.URL) - if err != nil { - return fmt.Errorf("error parsing server URL %s: %w", srv.URL, err) - } - - logger.WithField(log.ServerName, name).Debugf("Creating server %d %s", name, u) - - if err := lb.UpsertServer(u, roundrobin.Weight(1)); err != nil { - return fmt.Errorf("error adding server %s to load balancer: %w", srv.URL, err) - } - - // TODO Handle Metrics - } - return nil -} - -func convertSameSite(sameSite string) http.SameSite { - switch sameSite { - case "none": - return http.SameSiteNoneMode - case "lax": - return http.SameSiteLaxMode - case "strict": - return http.SameSiteStrictMode - default: - return 0 +func (m *Manager) LaunchHealthCheck(ctx context.Context) { + for serviceName, hc := range m.healthCheckers { + ctx = log.With(ctx, log.Str(log.ServiceName, serviceName)) + go hc.Launch(ctx) } } diff --git a/pkg/server/service/service_test.go b/pkg/server/service/service_test.go index 4d7ef7268..36501cace 100644 --- a/pkg/server/service/service_test.go +++ b/pkg/server/service/service_test.go @@ -15,14 +15,10 @@ import ( "github.com/traefik/traefik/v2/pkg/testhelpers" ) -type MockForwarder struct{} - -func (MockForwarder) ServeHTTP(http.ResponseWriter, *http.Request) { - panic("implement me") -} - func TestGetLoadBalancer(t *testing.T) { - sm := Manager{} + sm := Manager{ + roundTripperManager: newRtMock(), + } testCases := []struct { desc string @@ -67,7 +63,8 @@ func TestGetLoadBalancer(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - handler, err := sm.getLoadBalancer(context.Background(), test.serviceName, test.service, test.fwd) + serviceInfo := &runtime.ServiceInfo{Service: &dynamic.Service{LoadBalancer: test.service}} + handler, err := sm.getLoadBalancerServiceHandler(context.Background(), test.serviceName, serviceInfo) if test.expectError { require.Error(t, err) assert.Nil(t, handler) @@ -129,6 +126,7 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) { desc: "Load balances between the two servers", serviceName: "test", service: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), Servers: []dynamic.Server{ { URL: server1.URL, @@ -258,40 +256,13 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) { }, }, }, - { - desc: "Cookie value is backward compatible", - serviceName: "test", - service: &dynamic.ServersLoadBalancer{ - Sticky: &dynamic.Sticky{ - Cookie: &dynamic.Cookie{}, - }, - Servers: []dynamic.Server{ - { - URL: server1.URL, - }, - { - URL: server2.URL, - }, - }, - }, - cookieRawValue: "_6f743=" + server1.URL, - expected: []ExpectedResult{ - { - StatusCode: http.StatusOK, - XFrom: "first", - }, - { - StatusCode: http.StatusOK, - XFrom: "first", - }, - }, - }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { - handler, err := sm.getLoadBalancerServiceHandler(context.Background(), test.serviceName, test.service) + serviceInfo := &runtime.ServiceInfo{Service: &dynamic.Service{LoadBalancer: test.service}} + handler, err := sm.getLoadBalancerServiceHandler(context.Background(), test.serviceName, serviceInfo) assert.NoError(t, err) assert.NotNil(t, handler) @@ -419,3 +390,21 @@ func TestMultipleTypeOnBuildHTTP(t *testing.T) { _, err := manager.BuildHTTP(context.Background(), "test@file") assert.Error(t, err, "cannot create service: multi-types service not supported, consider declaring two different pieces of service instead") } + +func Bool(v bool) *bool { return &v } + +type MockForwarder struct{} + +func (MockForwarder) ServeHTTP(http.ResponseWriter, *http.Request) { + panic("not available") +} + +type rtMock struct{} + +func newRtMock() RoundTripperGetter { + return &rtMock{} +} + +func (r *rtMock) Get(_ string) (http.RoundTripper, error) { + return http.DefaultTransport, nil +}