Merge github.com:traefik/traefik

This commit is contained in:
baalajimaestro 2022-11-16 21:28:55 +05:30
commit ac7aa6aacc
Signed by: baalajimaestro
GPG key ID: F93C394FE9BBAFD5
85 changed files with 3581 additions and 2263 deletions

View file

@ -134,7 +134,6 @@ issues:
exclude: exclude:
- 'Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked' - '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" - "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.SSLRedirect is deprecated'
- 'SA1019: cfg.SSLTemporaryRedirect is deprecated' - 'SA1019: cfg.SSLTemporaryRedirect is deprecated'
- 'SA1019: cfg.SSLHost is deprecated' - 'SA1019: cfg.SSLHost is deprecated'

View file

@ -5,23 +5,24 @@ description: "Traefik Proxy's HTTP middleware lets you compress responses before
# Compress # Compress
Compress Responses before Sending them to the Client Compress Allows Compressing Responses before Sending them to the Client
{: .subtitle } {: .subtitle }
![Compress](../../assets/img/middleware/compress.png) ![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 ## Configuration Examples
```yaml tab="Docker" ```yaml tab="Docker"
# Enable gzip compression # Enable compression
labels: labels:
- "traefik.http.middlewares.test-compress.compress=true" - "traefik.http.middlewares.test-compress.compress=true"
``` ```
```yaml tab="Kubernetes" ```yaml tab="Kubernetes"
# Enable gzip compression # Enable compression
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1
kind: Middleware kind: Middleware
metadata: metadata:
@ -31,7 +32,7 @@ spec:
``` ```
```yaml tab="Consul Catalog" ```yaml tab="Consul Catalog"
# Enable gzip compression # Enable compression
- "traefik.http.middlewares.test-compress.compress=true" - "traefik.http.middlewares.test-compress.compress=true"
``` ```
@ -42,13 +43,13 @@ spec:
``` ```
```yaml tab="Rancher" ```yaml tab="Rancher"
# Enable gzip compression # Enable compression
labels: labels:
- "traefik.http.middlewares.test-compress.compress=true" - "traefik.http.middlewares.test-compress.compress=true"
``` ```
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
# Enable gzip compression # Enable compression
http: http:
middlewares: middlewares:
test-compress: test-compress:
@ -56,7 +57,7 @@ http:
``` ```
```toml tab="File (TOML)" ```toml tab="File (TOML)"
# Enable gzip compression # Enable compression
[http.middlewares] [http.middlewares]
[http.middlewares.test-compress.compress] [http.middlewares.test-compress.compress]
``` ```
@ -65,23 +66,34 @@ http:
Responses are compressed when the following criteria are all met: 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`, `*`, and/or `br` with or without [quality values](https://developer.mozilla.org/en-US/docs/Glossary/Quality_values).
* The `Accept-Encoding` request header contains `gzip`. 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. * The response is not already compressed, i.e. the `Content-Encoding` response header is not already set.
* The response`Content-Type` header is not one among the [excludedContentTypes options](#excludedcontenttypes).
If the `Content-Type` header is not defined, or empty, the compress middleware will automatically [detect](https://mimesniff.spec.whatwg.org/) a content type. * The response body is larger than the [configured minimum amount of bytes](#minresponsebodybytes) (default is `1024`).
It will also set the `Content-Type` header according to the detected MIME type.
## Configuration Options ## Configuration Options
### `excludedContentTypes` ### `excludedContentTypes`
_Optional, Default=""_
`excludedContentTypes` specifies a list of content types to compare the `Content-Type` header of the incoming requests and responses before compressing. `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. The responses with content types defined in `excludedContentTypes` are not compressed.
Content types are compared in a case-insensitive, whitespace-ignored manner. 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" ```yaml tab="Docker"
labels: labels:
- "traefik.http.middlewares.test-compress.compress.excludedcontenttypes=text/event-stream" - "traefik.http.middlewares.test-compress.compress.excludedcontenttypes=text/event-stream"
@ -130,9 +142,9 @@ http:
### `minResponseBodyBytes` ### `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. Responses smaller than the specified values will not be compressed.

View file

@ -57,15 +57,15 @@
path = "foobar" path = "foobar"
method = "foobar" method = "foobar"
port = 42 port = 42
interval = "foobar" interval = "42s"
timeout = "foobar" timeout = "42s"
hostname = "foobar" hostname = "foobar"
followRedirects = true followRedirects = true
[http.services.Service01.loadBalancer.healthCheck.headers] [http.services.Service01.loadBalancer.healthCheck.headers]
name0 = "foobar" name0 = "foobar"
name1 = "foobar" name1 = "foobar"
[http.services.Service01.loadBalancer.responseForwarding] [http.services.Service01.loadBalancer.responseForwarding]
flushInterval = "foobar" flushInterval = "42s"
[http.services.Service02] [http.services.Service02]
[http.services.Service02.mirroring] [http.services.Service02.mirroring]
service = "foobar" service = "foobar"

View file

@ -62,8 +62,8 @@ http:
path: foobar path: foobar
method: foobar method: foobar
port: 42 port: 42
interval: foobar interval: 42s
timeout: foobar timeout: 42s
hostname: foobar hostname: foobar
followRedirects: true followRedirects: true
headers: headers:
@ -71,7 +71,7 @@ http:
name1: foobar name1: foobar
passHostHeader: true passHostHeader: true
responseForwarding: responseForwarding:
flushInterval: foobar flushInterval: 42s
serversTransport: foobar serversTransport: foobar
Service02: Service02:
mirroring: mirroring:

View file

@ -749,7 +749,8 @@ spec:
excludedContentTypes: excludedContentTypes:
description: ExcludedContentTypes defines the list of content description: ExcludedContentTypes defines the list of content
types to compare the Content-Type header of the incoming requests 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: items:
type: string type: string
type: array type: array

View file

@ -214,15 +214,15 @@
| `traefik/http/services/Service01/loadBalancer/healthCheck/headers/name0` | `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/headers/name1` | `foobar` |
| `traefik/http/services/Service01/loadBalancer/healthCheck/hostname` | `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/method` | `foobar` |
| `traefik/http/services/Service01/loadBalancer/healthCheck/mode` | `foobar` | | `traefik/http/services/Service01/loadBalancer/healthCheck/mode` | `foobar` |
| `traefik/http/services/Service01/loadBalancer/healthCheck/path` | `foobar` | | `traefik/http/services/Service01/loadBalancer/healthCheck/path` | `foobar` |
| `traefik/http/services/Service01/loadBalancer/healthCheck/port` | `42` | | `traefik/http/services/Service01/loadBalancer/healthCheck/port` | `42` |
| `traefik/http/services/Service01/loadBalancer/healthCheck/scheme` | `foobar` | | `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/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/0/url` | `foobar` |
| `traefik/http/services/Service01/loadBalancer/servers/1/url` | `foobar` | | `traefik/http/services/Service01/loadBalancer/servers/1/url` | `foobar` |
| `traefik/http/services/Service01/loadBalancer/serversTransport` | `foobar` | | `traefik/http/services/Service01/loadBalancer/serversTransport` | `foobar` |

View file

@ -150,15 +150,15 @@
"traefik.http.services.service01.loadbalancer.healthcheck.headers.name0": "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.headers.name1": "foobar",
"traefik.http.services.service01.loadbalancer.healthcheck.hostname": "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.path": "foobar",
"traefik.http.services.service01.loadbalancer.healthcheck.method": "foobar", "traefik.http.services.service01.loadbalancer.healthcheck.method": "foobar",
"traefik.http.services.service01.loadbalancer.healthcheck.port": "42", "traefik.http.services.service01.loadbalancer.healthcheck.port": "42",
"traefik.http.services.service01.loadbalancer.healthcheck.scheme": "foobar", "traefik.http.services.service01.loadbalancer.healthcheck.scheme": "foobar",
"traefik.http.services.service01.loadbalancer.healthcheck.mode": "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.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.serverstransport": "foobar",
"traefik.http.services.service01.loadbalancer.sticky.cookie": "true", "traefik.http.services.service01.loadbalancer.sticky.cookie": "true",
"traefik.http.services.service01.loadbalancer.sticky.cookie.httponly": "true", "traefik.http.services.service01.loadbalancer.sticky.cookie.httponly": "true",

View file

@ -172,7 +172,8 @@ spec:
excludedContentTypes: excludedContentTypes:
description: ExcludedContentTypes defines the list of content description: ExcludedContentTypes defines the list of content
types to compare the Content-Type header of the incoming requests 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: items:
type: string type: string
type: array type: array

View file

@ -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: The table below lists all the available matchers:
| Rule | Description | | Rule | Description |
|--------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------| |------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|
| ```Headers(`key`, `value`)``` | Check if there is a key `key`defined in the headers, with the value `value` | | ```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` | | ```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`. | | ```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. | | ```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. | | ```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`) | | ```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. | | ```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. | | ```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. | | ```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. | | ```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" !!! 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] [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` #### `passthrough`
As seen above, a TLS router will terminate the TLS connection by default. As seen above, a TLS router will terminate the TLS connection by default.

1
go.mod
View file

@ -7,6 +7,7 @@ require (
github.com/ExpediaDotCom/haystack-client-go v0.0.0-20190315171017-e7edbdf53a61 github.com/ExpediaDotCom/haystack-client-go v0.0.0-20190315171017-e7edbdf53a61
github.com/Masterminds/sprig/v3 v3.2.2 github.com/Masterminds/sprig/v3 v3.2.2
github.com/abbot/go-http-auth v0.0.0-00010101000000-000000000000 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/aws/aws-sdk-go v1.44.47
github.com/cenkalti/backoff/v4 v4.1.3 github.com/cenkalti/backoff/v4 v4.1.3
github.com/compose-spec/compose-go v1.0.3 github.com/compose-spec/compose-go v1.0.3

2
go.sum
View file

@ -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 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI=
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= 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.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/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/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= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=

View file

@ -749,7 +749,8 @@ spec:
excludedContentTypes: excludedContentTypes:
description: ExcludedContentTypes defines the list of content description: ExcludedContentTypes defines the list of content
types to compare the Content-Type header of the incoming requests 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: items:
type: string type: string
type: array type: array

View file

@ -179,7 +179,10 @@
"url": "http://10.0.1.1:8889" "url": "http://10.0.1.1:8889"
} }
], ],
"passHostHeader": true "passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
}
}, },
"status": "enabled", "status": "enabled",
"usedBy": [ "usedBy": [
@ -201,7 +204,10 @@
"url": "http://10.0.1.2:8889" "url": "http://10.0.1.2:8889"
} }
], ],
"passHostHeader": true "passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
}
}, },
"status": "enabled" "status": "enabled"
}, },
@ -215,7 +221,10 @@
"url": "http://10.0.1.3:8889" "url": "http://10.0.1.3:8889"
} }
], ],
"passHostHeader": true "passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
}
}, },
"status": "enabled" "status": "enabled"
} }

View file

@ -47,7 +47,10 @@
"url": "http://10.42.0.4:80" "url": "http://10.42.0.4:80"
} }
], ],
"passHostHeader": true "passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
}
}, },
"status": "enabled", "status": "enabled",
"usedBy": [ "usedBy": [

View file

@ -137,7 +137,10 @@
"url": "http://10.42.0.7:80" "url": "http://10.42.0.7:80"
} }
], ],
"passHostHeader": true "passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
}
}, },
"status": "enabled", "status": "enabled",
"usedBy": [ "usedBy": [
@ -158,7 +161,10 @@
"url": "http://10.42.0.7:80" "url": "http://10.42.0.7:80"
} }
], ],
"passHostHeader": true "passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
}
}, },
"status": "enabled", "status": "enabled",
"usedBy": [ "usedBy": [
@ -180,6 +186,9 @@
} }
], ],
"passHostHeader": true, "passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
},
"serversTransport": "default-mytransport@kubernetescrd" "serversTransport": "default-mytransport@kubernetescrd"
}, },
"status": "enabled", "status": "enabled",
@ -201,7 +210,10 @@
"url": "http://10.42.0.7:80" "url": "http://10.42.0.7:80"
} }
], ],
"passHostHeader": true "passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
}
}, },
"status": "enabled", "status": "enabled",
"serverStatus": { "serverStatus": {

View file

@ -179,7 +179,10 @@
"url": "http://10.0.1.1:8889" "url": "http://10.0.1.1:8889"
} }
], ],
"passHostHeader": true "passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
}
}, },
"status": "enabled", "status": "enabled",
"usedBy": [ "usedBy": [
@ -201,7 +204,10 @@
"url": "http://10.0.1.2:8889" "url": "http://10.0.1.2:8889"
} }
], ],
"passHostHeader": true "passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
}
}, },
"status": "enabled" "status": "enabled"
}, },
@ -215,7 +221,10 @@
"url": "http://10.0.1.3:8889" "url": "http://10.0.1.3:8889"
} }
], ],
"passHostHeader": true "passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
}
}, },
"status": "enabled" "status": "enabled"
} }

View file

@ -128,7 +128,10 @@
"url": "http://10.42.0.7:80" "url": "http://10.42.0.7:80"
} }
], ],
"passHostHeader": true "passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
}
}, },
"status": "enabled", "status": "enabled",
"serverStatus": { "serverStatus": {

View file

@ -88,7 +88,10 @@
"url": "http://10.42.0.7:80" "url": "http://10.42.0.7:80"
} }
], ],
"passHostHeader": true "passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
}
}, },
"status": "enabled", "status": "enabled",
"usedBy": [ "usedBy": [

View file

@ -121,7 +121,10 @@
"url": "XXXX" "url": "XXXX"
} }
], ],
"passHostHeader": true "passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
}
}, },
"status": "enabled", "status": "enabled",
"usedBy": [ "usedBy": [
@ -143,7 +146,10 @@
"url": "http://10.42.0.8:80" "url": "http://10.42.0.8:80"
} }
], ],
"passHostHeader": true "passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
}
}, },
"status": "enabled", "status": "enabled",
"usedBy": [ "usedBy": [

View file

@ -88,7 +88,10 @@
"url": "http://10.42.0.5:80" "url": "http://10.42.0.5:80"
} }
], ],
"passHostHeader": true "passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
}
}, },
"status": "enabled", "status": "enabled",
"usedBy": [ "usedBy": [

View file

@ -179,7 +179,10 @@
"url": "http://10.0.1.1:8889" "url": "http://10.0.1.1:8889"
} }
], ],
"passHostHeader": true "passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
}
}, },
"status": "enabled", "status": "enabled",
"usedBy": [ "usedBy": [
@ -201,7 +204,10 @@
"url": "http://10.0.1.2:8889" "url": "http://10.0.1.2:8889"
} }
], ],
"passHostHeader": true "passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
}
}, },
"status": "enabled" "status": "enabled"
}, },
@ -215,7 +221,10 @@
"url": "http://10.0.1.3:8889" "url": "http://10.0.1.3:8889"
} }
], ],
"passHostHeader": true "passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
}
}, },
"status": "enabled" "status": "enabled"
} }

View file

@ -179,7 +179,10 @@
"url": "http://10.0.1.1:8889" "url": "http://10.0.1.1:8889"
} }
], ],
"passHostHeader": true "passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
}
}, },
"status": "enabled", "status": "enabled",
"usedBy": [ "usedBy": [
@ -201,7 +204,10 @@
"url": "http://10.0.1.2:8889" "url": "http://10.0.1.2:8889"
} }
], ],
"passHostHeader": true "passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
}
}, },
"status": "enabled" "status": "enabled"
}, },
@ -215,7 +221,10 @@
"url": "http://10.0.1.3:8889" "url": "http://10.0.1.3:8889"
} }
], ],
"passHostHeader": true "passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
}
}, },
"status": "enabled" "status": "enabled"
} }

View file

@ -425,14 +425,14 @@
mode = "foobar" mode = "foobar"
path = "foobar" path = "foobar"
port = 42 port = 42
interval = "foobar" interval = "10s"
timeout = "foobar" timeout = "10s"
hostname = "foobar" hostname = "foobar"
[http.services.Service0.loadBalancer.healthCheck.headers] [http.services.Service0.loadBalancer.healthCheck.headers]
name0 = "foobar" name0 = "foobar"
name1 = "foobar" name1 = "foobar"
[http.services.Service0.loadBalancer.responseForwarding] [http.services.Service0.loadBalancer.responseForwarding]
flushInterval = "foobar" flushInterval = "10s"
[tcp] [tcp]
[tcp.routers] [tcp.routers]

View file

@ -9,6 +9,19 @@ import (
"github.com/traefik/traefik/v2/pkg/types" "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 // +k8s:deepcopy-gen=true
// HTTPConfiguration contains all the HTTP configuration parameters. // HTTPConfiguration contains all the HTTP configuration parameters.
@ -178,8 +191,11 @@ func (l *ServersLoadBalancer) Mergeable(loadBalancer *ServersLoadBalancer) bool
// SetDefaults Default values for a ServersLoadBalancer. // SetDefaults Default values for a ServersLoadBalancer.
func (l *ServersLoadBalancer) SetDefaults() { func (l *ServersLoadBalancer) SetDefaults() {
defaultPassHostHeader := true defaultPassHostHeader := DefaultPassHostHeader
l.PassHostHeader = &defaultPassHostHeader l.PassHostHeader = &defaultPassHostHeader
l.ResponseForwarding = &ResponseForwarding{}
l.ResponseForwarding.SetDefaults()
} }
// +k8s:deepcopy-gen=true // +k8s:deepcopy-gen=true
@ -191,7 +207,12 @@ type ResponseForwarding struct {
// This configuration is ignored when ReverseProxy recognizes a response as a streaming response; // This configuration is ignored when ReverseProxy recognizes a response as a streaming response;
// for such responses, writes are flushed to the client immediately. // for such responses, writes are flushed to the client immediately.
// Default: 100ms // 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 // +k8s:deepcopy-gen=true
@ -212,15 +233,13 @@ func (s *Server) SetDefaults() {
// ServerHealthCheck holds the HealthCheck configuration. // ServerHealthCheck holds the HealthCheck configuration.
type ServerHealthCheck struct { type ServerHealthCheck struct {
Scheme string `json:"scheme,omitempty" toml:"scheme,omitempty" yaml:"scheme,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"` 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"` 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"` 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"` Port int `json:"port,omitempty" toml:"port,omitempty,omitzero" yaml:"port,omitempty" export:"true"`
// TODO change string to ptypes.Duration Interval ptypes.Duration `json:"interval,omitempty" toml:"interval,omitempty" yaml:"interval,omitempty" export:"true"`
Interval string `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"`
// TODO change string to ptypes.Duration
Timeout string `json:"timeout,omitempty" toml:"timeout,omitempty" yaml:"timeout,omitempty" export:"true"`
Hostname string `json:"hostname,omitempty" toml:"hostname,omitempty" yaml:"hostname,omitempty"` Hostname string `json:"hostname,omitempty" toml:"hostname,omitempty" yaml:"hostname,omitempty"`
FollowRedirects *bool `json:"followRedirects" toml:"followRedirects" yaml:"followRedirects" export:"true"` 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"` 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 fr := true
h.FollowRedirects = &fr h.FollowRedirects = &fr
h.Mode = "http" h.Mode = "http"
h.Interval = DefaultHealthCheckInterval
h.Timeout = DefaultHealthCheckTimeout
} }
// +k8s:deepcopy-gen=true // +k8s:deepcopy-gen=true

View file

@ -161,6 +161,7 @@ func (c *CircuitBreaker) SetDefaults() {
// More info: https://doc.traefik.io/traefik/v2.9/middlewares/http/compress/ // More info: https://doc.traefik.io/traefik/v2.9/middlewares/http/compress/
type Compress struct { type Compress struct {
// ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing. // 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"` 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. // MinResponseBodyBytes defines the minimum amount of bytes a response body must have to be compressed.
// Default: 1024. // Default: 1024.

View file

@ -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.name0": "foobar",
"traefik.http.services.Service0.loadbalancer.healthcheck.headers.name1": "foobar", "traefik.http.services.Service0.loadbalancer.healthcheck.headers.name1": "foobar",
"traefik.http.services.Service0.loadbalancer.healthcheck.hostname": "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.path": "foobar",
"traefik.http.services.Service0.loadbalancer.healthcheck.method": "foobar", "traefik.http.services.Service0.loadbalancer.healthcheck.method": "foobar",
"traefik.http.services.Service0.loadbalancer.healthcheck.port": "42", "traefik.http.services.Service0.loadbalancer.healthcheck.port": "42",
"traefik.http.services.Service0.loadbalancer.healthcheck.scheme": "foobar", "traefik.http.services.Service0.loadbalancer.healthcheck.scheme": "foobar",
"traefik.http.services.Service0.loadbalancer.healthcheck.mode": "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.healthcheck.followredirects": "true",
"traefik.http.services.Service0.loadbalancer.passhostheader": "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.scheme": "foobar",
"traefik.http.services.Service0.loadbalancer.server.port": "8080", "traefik.http.services.Service0.loadbalancer.server.port": "8080",
"traefik.http.services.Service0.loadbalancer.sticky.cookie.name": "foobar", "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.name0": "foobar",
"traefik.http.services.Service1.loadbalancer.healthcheck.headers.name1": "foobar", "traefik.http.services.Service1.loadbalancer.healthcheck.headers.name1": "foobar",
"traefik.http.services.Service1.loadbalancer.healthcheck.hostname": "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.path": "foobar",
"traefik.http.services.Service1.loadbalancer.healthcheck.method": "foobar", "traefik.http.services.Service1.loadbalancer.healthcheck.method": "foobar",
"traefik.http.services.Service1.loadbalancer.healthcheck.port": "42", "traefik.http.services.Service1.loadbalancer.healthcheck.port": "42",
"traefik.http.services.Service1.loadbalancer.healthcheck.scheme": "foobar", "traefik.http.services.Service1.loadbalancer.healthcheck.scheme": "foobar",
"traefik.http.services.Service1.loadbalancer.healthcheck.mode": "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.healthcheck.followredirects": "true",
"traefik.http.services.Service1.loadbalancer.passhostheader": "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.scheme": "foobar",
"traefik.http.services.Service1.loadbalancer.server.port": "8080", "traefik.http.services.Service1.loadbalancer.server.port": "8080",
"traefik.http.services.Service1.loadbalancer.sticky": "false", "traefik.http.services.Service1.loadbalancer.sticky": "false",
@ -656,8 +656,8 @@ func TestDecodeConfiguration(t *testing.T) {
Path: "foobar", Path: "foobar",
Method: "foobar", Method: "foobar",
Port: 42, Port: 42,
Interval: "foobar", Interval: ptypes.Duration(time.Second),
Timeout: "foobar", Timeout: ptypes.Duration(time.Second),
Hostname: "foobar", Hostname: "foobar",
Headers: map[string]string{ Headers: map[string]string{
"name0": "foobar", "name0": "foobar",
@ -667,7 +667,7 @@ func TestDecodeConfiguration(t *testing.T) {
}, },
PassHostHeader: func(v bool) *bool { return &v }(true), PassHostHeader: func(v bool) *bool { return &v }(true),
ResponseForwarding: &dynamic.ResponseForwarding{ ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: "foobar", FlushInterval: ptypes.Duration(time.Second),
}, },
}, },
}, },
@ -685,8 +685,8 @@ func TestDecodeConfiguration(t *testing.T) {
Path: "foobar", Path: "foobar",
Method: "foobar", Method: "foobar",
Port: 42, Port: 42,
Interval: "foobar", Interval: ptypes.Duration(time.Second),
Timeout: "foobar", Timeout: ptypes.Duration(time.Second),
Hostname: "foobar", Hostname: "foobar",
Headers: map[string]string{ Headers: map[string]string{
"name0": "foobar", "name0": "foobar",
@ -696,7 +696,7 @@ func TestDecodeConfiguration(t *testing.T) {
}, },
PassHostHeader: func(v bool) *bool { return &v }(true), PassHostHeader: func(v bool) *bool { return &v }(true),
ResponseForwarding: &dynamic.ResponseForwarding{ ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: "foobar", FlushInterval: ptypes.Duration(time.Second),
}, },
}, },
}, },
@ -1148,8 +1148,8 @@ func TestEncodeConfiguration(t *testing.T) {
Path: "foobar", Path: "foobar",
Method: "foobar", Method: "foobar",
Port: 42, Port: 42,
Interval: "foobar", Interval: ptypes.Duration(time.Second),
Timeout: "foobar", Timeout: ptypes.Duration(time.Second),
Hostname: "foobar", Hostname: "foobar",
Headers: map[string]string{ Headers: map[string]string{
"name0": "foobar", "name0": "foobar",
@ -1158,7 +1158,7 @@ func TestEncodeConfiguration(t *testing.T) {
}, },
PassHostHeader: func(v bool) *bool { return &v }(true), PassHostHeader: func(v bool) *bool { return &v }(true),
ResponseForwarding: &dynamic.ResponseForwarding{ ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: "foobar", FlushInterval: ptypes.Duration(time.Second),
}, },
}, },
}, },
@ -1175,8 +1175,8 @@ func TestEncodeConfiguration(t *testing.T) {
Path: "foobar", Path: "foobar",
Method: "foobar", Method: "foobar",
Port: 42, Port: 42,
Interval: "foobar", Interval: ptypes.Duration(time.Second),
Timeout: "foobar", Timeout: ptypes.Duration(time.Second),
Hostname: "foobar", Hostname: "foobar",
Headers: map[string]string{ Headers: map[string]string{
"name0": "foobar", "name0": "foobar",
@ -1185,7 +1185,7 @@ func TestEncodeConfiguration(t *testing.T) {
}, },
PassHostHeader: func(v bool) *bool { return &v }(true), PassHostHeader: func(v bool) *bool { return &v }(true),
ResponseForwarding: &dynamic.ResponseForwarding{ 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.Headers.name1": "foobar",
"traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Hostname": "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.Path": "foobar",
"traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Method": "foobar", "traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Method": "foobar",
"traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Port": "42", "traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Port": "42",
"traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Scheme": "foobar", "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.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.Port": "8080",
"traefik.HTTP.Services.Service0.LoadBalancer.server.Scheme": "foobar", "traefik.HTTP.Services.Service0.LoadBalancer.server.Scheme": "foobar",
"traefik.HTTP.Services.Service0.LoadBalancer.Sticky.Cookie.Name": "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.name0": "foobar",
"traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Headers.name1": "foobar", "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Headers.name1": "foobar",
"traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Hostname": "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.Path": "foobar",
"traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Method": "foobar", "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Method": "foobar",
"traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Port": "42", "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Port": "42",
"traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Scheme": "foobar", "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.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.Port": "8080",
"traefik.HTTP.Services.Service1.LoadBalancer.server.Scheme": "foobar", "traefik.HTTP.Services.Service1.LoadBalancer.server.Scheme": "foobar",
"traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Headers.name0": "foobar", "traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Headers.name0": "foobar",

View file

@ -15,6 +15,12 @@ const (
StatusWarning = "warning" StatusWarning = "warning"
) )
// Status of the servers.
const (
StatusUp = "UP"
StatusDown = "DOWN"
)
// Configuration holds the information about the currently running traefik instance. // Configuration holds the information about the currently running traefik instance.
type Configuration struct { type Configuration struct {
Routers map[string]*RouterInfo `json:"routers,omitempty"` Routers map[string]*RouterInfo `json:"routers,omitempty"`

View file

@ -2,9 +2,11 @@ package runtime_test
import ( import (
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "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/dynamic"
"github.com/traefik/traefik/v2/pkg/config/runtime" "github.com/traefik/traefik/v2/pkg/config/runtime"
) )
@ -49,7 +51,7 @@ func TestPopulateUsedBy(t *testing.T) {
{URL: "http://127.0.0.1:8086"}, {URL: "http://127.0.0.1:8086"},
}, },
HealthCheck: &dynamic.ServerHealthCheck{ HealthCheck: &dynamic.ServerHealthCheck{
Interval: "500ms", Interval: ptypes.Duration(500 * time.Millisecond),
Path: "/health", Path: "/health",
}, },
}, },
@ -159,7 +161,7 @@ func TestPopulateUsedBy(t *testing.T) {
}, },
}, },
HealthCheck: &dynamic.ServerHealthCheck{ HealthCheck: &dynamic.ServerHealthCheck{
Interval: "500ms", Interval: ptypes.Duration(500 * time.Millisecond),
Path: "/health", Path: "/health",
}, },
}, },
@ -177,7 +179,7 @@ func TestPopulateUsedBy(t *testing.T) {
}, },
}, },
HealthCheck: &dynamic.ServerHealthCheck{ HealthCheck: &dynamic.ServerHealthCheck{
Interval: "500ms", Interval: ptypes.Duration(500 * time.Millisecond),
Path: "/health", Path: "/health",
}, },
}, },

View file

@ -8,17 +8,12 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings"
"sync"
"time" "time"
gokitmetrics "github.com/go-kit/kit/metrics" gokitmetrics "github.com/go-kit/kit/metrics"
"github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/config/runtime" "github.com/traefik/traefik/v2/pkg/config/runtime"
"github.com/traefik/traefik/v2/pkg/log" "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"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/credentials/insecure"
@ -26,267 +21,153 @@ import (
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
) )
const ( const modeGRPC = "grpc"
serverUp = "UP"
serverDown = "DOWN"
)
const ( // StatusSetter should be implemented by a service that, when the status of a
HTTPMode = "http" // registered target change, needs to be notified of that change.
GRPCMode = "grpc" type StatusSetter interface {
) SetStatus(ctx context.Context, childName string, up bool)
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
} }
// BalancerHandler includes functionality for load-balancing management. // StatusUpdater should be implemented by a service that, when its status
type BalancerHandler interface { // changes (e.g. all if its children are down), needs to propagate upwards (to
ServeHTTP(w http.ResponseWriter, req *http.Request) // their parent(s)) that change.
Balancer type StatusUpdater interface {
RegisterStatusUpdater(fn func(up bool)) error
} }
// BalancerStatusHandler is an http Handler that does load-balancing, type metricsHealthCheck interface {
// and updates its parents of its status. ServiceServerUpGauge() gokitmetrics.Gauge
type BalancerStatusHandler interface {
BalancerHandler
StatusUpdater
} }
type metricsHealthcheck struct { type ServiceHealthChecker struct {
serverUpGauge gokitmetrics.Gauge 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. func NewServiceHealthChecker(ctx context.Context, metrics metricsHealthCheck, config *dynamic.ServerHealthCheck, service StatusSetter, info *runtime.ServiceInfo, transport http.RoundTripper, targets map[string]*url.URL) *ServiceHealthChecker {
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) {
logger := log.FromContext(ctx) logger := log.FromContext(ctx)
logger.Debugf("Initial health check for backend: %q", backend.name) interval := time.Duration(config.Interval)
hc.checkServersLB(ctx, backend) if interval <= 0 {
logger.Error("Health check interval smaller than zero")
ticker := time.NewTicker(backend.Interval) interval = time.Duration(dynamic.DefaultHealthCheckInterval)
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)
} }
backend.disabledURLs = newDisabledURLs timeout := time.Duration(config.Timeout)
if timeout <= 0 {
for _, enabledURL := range enabledURLs { logger.Error("Health check timeout smaller than zero")
serverUpMetricValue := float64(1) timeout = time.Duration(dynamic.DefaultHealthCheckTimeout)
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)
} }
req = backend.setRequestOptions(req) if timeout >= interval {
logger.Warnf("Health check timeout should be lower than the health check interval. Interval set to timeout + 1 second (%s).", interval)
client := http.Client{ interval = timeout + time.Second
Timeout: backend.Options.Timeout,
Transport: backend.Options.Transport,
} }
if !backend.FollowRedirects { client := &http.Client{
Transport: transport,
}
if config.FollowRedirects != nil && !*config.FollowRedirects {
client.CheckRedirect = func(req *http.Request, via []*http.Request) error { client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse 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 { if err != nil {
return fmt.Errorf("HTTP request failed: %w", err) return fmt.Errorf("HTTP request failed: %w", err)
} }
@ -300,34 +181,61 @@ func checkHealthHTTP(serverURL *url.URL, backend *BackendConfig) error {
return nil 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. // checkHealthGRPC returns an error with a meaningful description if the health check failed.
// Dedicated to gRPC servers implementing gRPC Health Checking Protocol v1. // Dedicated to gRPC servers implementing gRPC Health Checking Protocol v1.
func checkHealthGRPC(serverURL *url.URL, backend *BackendConfig) error { func (shc *ServiceHealthChecker) checkHealthGRPC(ctx context.Context, serverURL *url.URL) error {
u, err := serverURL.Parse(backend.Path) u, err := serverURL.Parse(shc.config.Path)
if err != nil { if err != nil {
return fmt.Errorf("failed to parse server URL: %w", err) return fmt.Errorf("failed to parse server URL: %w", err)
} }
port := u.Port() port := u.Port()
if backend.Options.Port != 0 { if shc.config.Port != 0 {
port = strconv.Itoa(backend.Options.Port) port = strconv.Itoa(shc.config.Port)
} }
serverAddr := net.JoinHostPort(u.Hostname(), port) serverAddr := net.JoinHostPort(u.Hostname(), port)
var opts []grpc.DialOption var opts []grpc.DialOption
switch backend.Options.Scheme { switch shc.config.Scheme {
case "http", "h2c", "": case "http", "h2c", "":
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) 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...) conn, err := grpc.DialContext(ctx, serverAddr, opts...)
if err != nil { if err != nil {
if errors.Is(err, context.DeadlineExceeded) { 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) 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) return fmt.Errorf("gRPC server does not implement the health protocol: %w", err)
case codes.DeadlineExceeded: case codes.DeadlineExceeded:
return fmt.Errorf("gRPC health check timeout: %w", err) 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 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
}

View file

@ -11,127 +11,324 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "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/config/runtime"
"github.com/traefik/traefik/v2/pkg/testhelpers" "github.com/traefik/traefik/v2/pkg/testhelpers"
"github.com/vulcand/oxy/roundrobin"
healthpb "google.golang.org/grpc/health/grpc_health_v1" healthpb "google.golang.org/grpc/health/grpc_health_v1"
) )
const ( func TestServiceHealthChecker_newRequest(t *testing.T) {
healthCheckInterval = 200 * time.Millisecond
healthCheckTimeout = 100 * time.Millisecond
)
func TestSetBackendsConfiguration(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
startHealthy bool targetURL string
mode string config dynamic.ServerHealthCheck
server StartTestServer expTarget string
expectedNumRemovedServers int expError bool
expectedNumUpsertedServers int expHostname string
expectedGaugeValue float64 expHeader string
expMethod string
}{ }{
{ {
desc: "healthy server staying healthy", desc: "no port override",
startHealthy: true, targetURL: "http://backend1:80",
server: newHTTPServer(http.StatusOK), config: dynamic.ServerHealthCheck{
expectedNumRemovedServers: 0, Path: "/test",
expectedNumUpsertedServers: 0, Port: 0,
expectedGaugeValue: 1, },
expError: false,
expTarget: "http://backend1:80/test",
expHostname: "backend1:80",
expMethod: http.MethodGet,
}, },
{ {
desc: "healthy server staying healthy (StatusNoContent)", desc: "port override",
startHealthy: true, targetURL: "http://backend2:80",
server: newHTTPServer(http.StatusNoContent), config: dynamic.ServerHealthCheck{
expectedNumRemovedServers: 0, Path: "/test",
expectedNumUpsertedServers: 0, Port: 8080,
expectedGaugeValue: 1, },
expError: false,
expTarget: "http://backend2:8080/test",
expHostname: "backend2:8080",
expMethod: http.MethodGet,
}, },
{ {
desc: "healthy server staying healthy (StatusPermanentRedirect)", desc: "no port override with no port in server URL",
startHealthy: true, targetURL: "http://backend1",
server: newHTTPServer(http.StatusPermanentRedirect), config: dynamic.ServerHealthCheck{
expectedNumRemovedServers: 0, Path: "/health",
expectedNumUpsertedServers: 0, Port: 0,
expectedGaugeValue: 1, },
expError: false,
expTarget: "http://backend1/health",
expHostname: "backend1",
expMethod: http.MethodGet,
}, },
{ {
desc: "healthy server becoming sick", desc: "port override with no port in server URL",
startHealthy: true, targetURL: "http://backend2",
server: newHTTPServer(http.StatusServiceUnavailable), config: dynamic.ServerHealthCheck{
expectedNumRemovedServers: 1, Path: "/health",
expectedNumUpsertedServers: 0, Port: 8080,
expectedGaugeValue: 0, },
expError: false,
expTarget: "http://backend2:8080/health",
expHostname: "backend2:8080",
expMethod: http.MethodGet,
}, },
{ {
desc: "sick server becoming healthy", desc: "scheme override",
startHealthy: false, targetURL: "https://backend1:80",
server: newHTTPServer(http.StatusOK), config: dynamic.ServerHealthCheck{
expectedNumRemovedServers: 0, Scheme: "http",
expectedNumUpsertedServers: 1, Path: "/test",
expectedGaugeValue: 1, Port: 0,
},
expError: false,
expTarget: "http://backend1:80/test",
expHostname: "backend1:80",
expMethod: http.MethodGet,
}, },
{ {
desc: "sick server staying sick", desc: "path with param",
startHealthy: false, targetURL: "http://backend1:80",
server: newHTTPServer(http.StatusServiceUnavailable), config: dynamic.ServerHealthCheck{
expectedNumRemovedServers: 0, Path: "/health?powpow=do",
expectedNumUpsertedServers: 0, Port: 0,
expectedGaugeValue: 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", desc: "path with params",
startHealthy: true, targetURL: "http://backend1:80",
server: newHTTPServer(http.StatusServiceUnavailable, http.StatusOK), config: dynamic.ServerHealthCheck{
expectedNumRemovedServers: 1, Path: "/health?powpow=do&do=powpow",
expectedNumUpsertedServers: 1, Port: 0,
expectedGaugeValue: 1, },
expError: false,
expTarget: "http://backend1:80/health?powpow=do&do=powpow",
expHostname: "backend1:80",
expMethod: http.MethodGet,
}, },
{ {
desc: "healthy grpc server staying healthy", desc: "path with invalid path",
mode: "grpc", targetURL: "http://backend1:80",
startHealthy: true, config: dynamic.ServerHealthCheck{
server: newGRPCServer(healthpb.HealthCheckResponse_SERVING), Path: ":",
expectedNumRemovedServers: 0, Port: 0,
expectedNumUpsertedServers: 0, },
expectedGaugeValue: 1, expError: true,
expTarget: "",
expHostname: "backend1:80",
expMethod: http.MethodGet,
}, },
{ {
desc: "healthy grpc server becoming sick", desc: "override hostname",
mode: "grpc", targetURL: "http://backend1:80",
startHealthy: true, config: dynamic.ServerHealthCheck{
server: newGRPCServer(healthpb.HealthCheckResponse_NOT_SERVING), Hostname: "myhost",
expectedNumRemovedServers: 1, Path: "/",
expectedNumUpsertedServers: 0, },
expectedGaugeValue: 0, expTarget: "http://backend1:80/",
expHostname: "myhost",
expHeader: "",
expMethod: http.MethodGet,
}, },
{ {
desc: "sick grpc server becoming healthy", desc: "not override hostname",
mode: "grpc", targetURL: "http://backend1:80",
startHealthy: false, config: dynamic.ServerHealthCheck{
server: newGRPCServer(healthpb.HealthCheckResponse_SERVING), Hostname: "",
expectedNumRemovedServers: 0, Path: "/",
expectedNumUpsertedServers: 1, },
expectedGaugeValue: 1, expTarget: "http://backend1:80/",
expHostname: "backend1:80",
expHeader: "",
expMethod: http.MethodGet,
}, },
{ {
desc: "sick grpc server staying sick", desc: "custom header",
mode: "grpc", targetURL: "http://backend1:80",
startHealthy: false, config: dynamic.ServerHealthCheck{
server: newGRPCServer(healthpb.HealthCheckResponse_NOT_SERVING), Headers: map[string]string{"Custom-Header": "foo"},
expectedNumRemovedServers: 0, Hostname: "",
expectedNumUpsertedServers: 0, Path: "/",
expectedGaugeValue: 0, },
expTarget: "http://backend1:80/",
expHostname: "backend1:80",
expHeader: "foo",
expMethod: http.MethodGet,
}, },
{ {
desc: "healthy grpc server toggling to sick and back to healthy", desc: "custom header with hostname override",
mode: "grpc", targetURL: "http://backend1:80",
startHealthy: true, config: dynamic.ServerHealthCheck{
server: newGRPCServer(healthpb.HealthCheckResponse_NOT_SERVING, healthpb.HealthCheckResponse_SERVING), Headers: map[string]string{"Custom-Header": "foo"},
expectedNumRemovedServers: 1, Hostname: "myhost",
expectedNumUpsertedServers: 1, Path: "/",
expectedGaugeValue: 1, },
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()) ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel) t.Cleanup(cancel)
serverURL, timeout := test.server.Start(t, cancel) targetURL, timeout := test.server.Start(t, cancel)
lb := &testLoadBalancer{RWMutex: &sync.RWMutex{}} lb := &testLoadBalancer{RWMutex: &sync.RWMutex{}}
options := Options{ config := &dynamic.ServerHealthCheck{
Mode: test.mode, Mode: test.mode,
Path: "/path", Path: "/path",
Interval: healthCheckInterval, Interval: ptypes.Duration(500 * time.Millisecond),
Timeout: healthCheckTimeout, Timeout: ptypes.Duration(499 * time.Millisecond),
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})
} }
collectingMetrics := &testhelpers.CollectingGauge{} gauge := &testhelpers.CollectingGauge{}
serviceInfo := &runtime.ServiceInfo{}
check := HealthCheck{ hc := NewServiceHealthChecker(ctx, &MetricsMock{gauge}, config, lb, serviceInfo, http.DefaultTransport, map[string]*url.URL{"test": targetURL})
Backends: make(map[string]*BackendConfig),
metrics: metricsHealthcheck{serverUpGauge: collectingMetrics},
}
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
wg.Add(1) wg.Add(1)
go func() { go func() {
check.execute(ctx, backend) hc.Launch(ctx)
wg.Done() wg.Done()
}() }()
@ -189,392 +375,14 @@ func TestSetBackendsConfiguration(t *testing.T) {
lb.Lock() lb.Lock()
defer lb.Unlock() defer lb.Unlock()
assert.Equal(t, test.expectedNumRemovedServers, lb.numRemovedServers, "removed servers") assert.Equal(t, test.expNumRemovedServers, lb.numRemovedServers, "removed servers")
assert.Equal(t, test.expectedNumUpsertedServers, lb.numUpsertedServers, "upserted servers") assert.Equal(t, test.expNumUpsertedServers, lb.numUpsertedServers, "upserted servers")
assert.Equal(t, test.expectedGaugeValue, collectingMetrics.GaugeValue, "ServerUp Gauge") 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) { func Bool(b bool) *bool {
type expected struct { return &b
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")
} }

View file

@ -10,9 +10,10 @@ import (
"testing" "testing"
"time" "time"
gokitmetrics "github.com/go-kit/kit/metrics"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/testhelpers" "github.com/traefik/traefik/v2/pkg/testhelpers"
"github.com/vulcand/oxy/roundrobin"
"google.golang.org/grpc" "google.golang.org/grpc"
healthpb "google.golang.org/grpc/health/grpc_health_v1" 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) { func (s *GRPCServer) Check(_ context.Context, _ *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) {
stat := s.status.Pop()
if s.status.IsEmpty() { if s.status.IsEmpty() {
s.done() s.done()
return &healthpb.HealthCheckResponse{
Status: healthpb.HealthCheckResponse_SERVICE_UNKNOWN,
}, nil
} }
stat := s.status.Pop()
return &healthpb.HealthCheckResponse{ return &healthpb.HealthCheckResponse{
Status: stat, 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 { func (s *GRPCServer) Watch(_ *healthpb.HealthCheckRequest, server healthpb.Health_WatchServer) error {
stat := s.status.Pop()
if s.status.IsEmpty() { if s.status.IsEmpty() {
s.done() s.done()
return server.Send(&healthpb.HealthCheckResponse{
Status: healthpb.HealthCheckResponse_SERVICE_UNKNOWN,
})
} }
stat := s.status.Pop()
return server.Send(&healthpb.HealthCheckResponse{ return server.Send(&healthpb.HealthCheckResponse{
Status: stat, 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. // 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 { type HTTPServer struct {
@ -126,13 +133,14 @@ func newHTTPServer(healthSequence ...int) *HTTPServer {
// ServeHTTP returns HTTP response codes following a status sequences. // ServeHTTP returns HTTP response codes following a status sequences.
// It calls the given 'done' function once all request health indicators have been depleted. // It calls the given 'done' function once all request health indicators have been depleted.
func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, _ *http.Request) { func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
if s.status.IsEmpty() {
s.done()
return
}
stat := s.status.Pop() stat := s.status.Pop()
w.WriteHeader(stat) w.WriteHeader(stat)
if s.status.IsEmpty() {
s.done()
}
} }
func (s *HTTPServer) Start(t *testing.T, done func()) (*url.URL, time.Duration) { 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) t.Cleanup(ts.Close)
// Make test timeout dependent on number of expected requests, health check interval, and a safety margin. // 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 { type testLoadBalancer struct {
@ -153,53 +161,20 @@ type testLoadBalancer struct {
*sync.RWMutex *sync.RWMutex
numRemovedServers int numRemovedServers int
numUpsertedServers 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) { func (lb *testLoadBalancer) SetStatus(ctx context.Context, childName string, up bool) {
// noop if up {
} lb.numUpsertedServers++
} else {
func (lb *testLoadBalancer) RemoveServer(u *url.URL) error { lb.numRemovedServers++
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
}
} }
if !found { }
return
} type MetricsMock struct {
Gauge gokitmetrics.Gauge
lb.servers = append(lb.servers[:i], lb.servers[i+1:]...) }
func (m *MetricsMock) ServiceServerUpGauge() gokitmetrics.Gauge {
return m.Gauge
} }

View file

@ -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. // AddOriginFields add origin fields.
func AddOriginFields(rw http.ResponseWriter, req *http.Request, next http.Handler, data *LogData) { func AddOriginFields(rw http.ResponseWriter, req *http.Request, next http.Handler, data *LogData) {
start := time.Now().UTC() start := time.Now().UTC()

View file

@ -62,13 +62,13 @@ func FromContext(ctx context.Context) (Capture, error) {
// Capture is the object populated by the capture middleware, // Capture is the object populated by the capture middleware,
// holding probes that allow to gather information about the request and response. // holding probes that allow to gather information about the request and response.
type Capture struct { type Capture struct {
rr *readCounter rr *readCounter
rw responseWriter crw *captureResponseWriter
} }
// NeedsReset returns whether the given http.ResponseWriter is the capture's probe. // NeedsReset returns whether the given http.ResponseWriter is the capture's probe.
func (c *Capture) NeedsReset(rw http.ResponseWriter) bool { 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 // 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 c.rr = readCounter
newReq.Body = 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 { func (c *Capture) ResponseSize() int64 {
return c.rw.Size() return c.crw.Size()
} }
func (c *Capture) StatusCode() int { 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, // 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() return r.source.Close()
} }
var _ middlewares.Stateful = &responseWriterWithCloseNotify{} var _ middlewares.Stateful = &captureResponseWriter{}
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}
}
// captureResponseWriter is a wrapper of type http.ResponseWriter // captureResponseWriter is a wrapper of type http.ResponseWriter
// that tracks response status and size. // 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) 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()
}

View file

@ -189,44 +189,3 @@ func TestRequestReader(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, int64(3), rr.size) 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)
})
}
}

View file

@ -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
}

View file

@ -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))
})
}
}

View file

@ -1,22 +1,26 @@
package compress package compress
import ( import (
"compress/gzip"
"context" "context"
"fmt"
"mime" "mime"
"net/http" "net/http"
"strings"
"github.com/klauspost/compress/gzhttp" "github.com/klauspost/compress/gzhttp"
"github.com/opentracing/opentracing-go/ext" "github.com/opentracing/opentracing-go/ext"
"github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/log" "github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/middlewares" "github.com/traefik/traefik/v2/pkg/middlewares"
"github.com/traefik/traefik/v2/pkg/middlewares/compress/brotli"
"github.com/traefik/traefik/v2/pkg/tracing" "github.com/traefik/traefik/v2/pkg/tracing"
) )
const ( const typeName = "Compress"
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. // Compress is a middleware that allows to compress the response.
type compress struct { type compress struct {
@ -24,6 +28,9 @@ type compress struct {
name string name string
excludes []string excludes []string
minSize int minSize int
brotliHandler http.Handler
gzipHandler http.Handler
} }
// New creates a new compress middleware. // 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) excludes = append(excludes, mediaType)
} }
minSize := gzhttp.DefaultMinSize minSize := DefaultMinSize
if conf.MinResponseBodyBytes > 0 { if conf.MinResponseBodyBytes > 0 {
minSize = conf.MinResponseBodyBytes 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) { func (c *compress) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
mediaType, _, err := mime.ParseMediaType(req.Header.Get("Content-Type")) logger := log.FromContext(middlewares.GetLoggerCtx(req.Context(), c.name, typeName))
if err != nil {
log.FromContext(middlewares.GetLoggerCtx(context.Background(), c.name, typeName)).Debug(err) 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) { if contains(c.excludes, mediaType) {
c.next.ServeHTTP(rw, req) c.next.ServeHTTP(rw, req)
} else { return
ctx := middlewares.GetLoggerCtx(req.Context(), c.name, typeName)
c.gzipHandler(ctx).ServeHTTP(rw, req)
} }
// 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) { func (c *compress) GetTracingInformation() (string, ext.SpanKindEnum) {
return c.name, tracing.SpanKindNoneEnum 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( wrapper, err := gzhttp.NewWrapper(
gzhttp.ExceptContentTypes(c.excludes), gzhttp.ExceptContentTypes(c.excludes),
gzhttp.CompressionLevel(gzip.DefaultCompression), gzhttp.MinSize(c.minSize),
gzhttp.MinSize(c.minSize)) )
if err != nil { 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 { func contains(values []string, val string) bool {
@ -84,5 +166,6 @@ func contains(values []string, val string) bool {
return true return true
} }
} }
return false return false
} }

View file

@ -1,12 +1,14 @@
package compress package compress
import ( import (
"compress/gzip"
"context" "context"
"io" "io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/andybalholm/brotli"
"github.com/klauspost/compress/gzhttp" "github.com/klauspost/compress/gzhttp"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -20,8 +22,81 @@ const (
contentTypeHeader = "Content-Type" contentTypeHeader = "Content-Type"
varyHeader = "Vary" varyHeader = "Vary"
gzipValue = "gzip" 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) { func TestShouldCompressWhenNoContentEncodingHeader(t *testing.T) {
req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil) req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil)
req.Header.Add(acceptEncodingHeader, gzipValue) 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, gzipValue, rw.Header().Get(contentEncodingHeader))
assert.Equal(t, acceptEncodingHeader, rw.Header().Get(varyHeader)) assert.Equal(t, acceptEncodingHeader, rw.Header().Get(varyHeader))
if assert.ObjectsAreEqualValues(rw.Body.Bytes(), baseBody) { gr, err := gzip.NewReader(rw.Body)
assert.Fail(t, "expected a compressed body", "got %v", rw.Body.Bytes()) require.NoError(t, err)
}
got, err := io.ReadAll(gr)
require.NoError(t, err)
assert.Equal(t, got, baseBody)
} }
func TestShouldNotCompressWhenContentEncodingHeader(t *testing.T) { func TestShouldNotCompressWhenContentEncodingHeader(t *testing.T) {
@ -71,7 +149,7 @@ func TestShouldNotCompressWhenContentEncodingHeader(t *testing.T) {
assert.EqualValues(t, rw.Body.Bytes(), fakeCompressedBody) 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) req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil)
fakeBody := generateBytes(gzhttp.DefaultMinSize) fakeBody := generateBytes(gzhttp.DefaultMinSize)
@ -87,7 +165,33 @@ func TestShouldNotCompressWhenNoAcceptEncodingHeader(t *testing.T) {
rw := httptest.NewRecorder() rw := httptest.NewRecorder()
handler.ServeHTTP(rw, req) 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(contentEncodingHeader))
assert.Empty(t, rw.Header().Get(varyHeader))
assert.EqualValues(t, rw.Body.Bytes(), fakeBody) assert.EqualValues(t, rw.Body.Bytes(), fakeBody)
} }

View file

@ -21,9 +21,8 @@ import (
// Compile time validation that the response recorder implements http interfaces correctly. // Compile time validation that the response recorder implements http interfaces correctly.
var ( var (
// TODO: maybe remove at least for codeModifierWithCloseNotify. _ middlewares.Stateful = &codeModifier{}
_ middlewares.Stateful = &codeModifierWithCloseNotify{} _ middlewares.Stateful = &codeCatcher{}
_ middlewares.Stateful = &codeCatcherWithCloseNotify{}
) )
const typeName = "customError" const typeName = "customError"
@ -124,13 +123,6 @@ func newRequest(baseURL string) (*http.Request, error) {
return req, nil 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 // 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 // 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 // simply drops the data from the response. Otherwise, it forwards it directly to
@ -144,27 +136,13 @@ type codeCatcher struct {
headersSent bool headersSent bool
} }
type codeCatcherWithCloseNotify struct { func newCodeCatcher(rw http.ResponseWriter, httpCodeRanges types.HTTPCodeRanges) *codeCatcher {
*codeCatcher return &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{
headerMap: make(http.Header), headerMap: make(http.Header),
code: http.StatusOK, // If backend does not call WriteHeader on us, we consider it's a 200. code: http.StatusOK, // If backend does not call WriteHeader on us, we consider it's a 200.
responseWriter: rw, responseWriter: rw,
httpCodeRanges: httpCodeRanges, httpCodeRanges: httpCodeRanges,
} }
if _, ok := rw.(http.CloseNotifier); ok {
return &codeCatcherWithCloseNotify{catcher}
}
return catcher
} }
func (cc *codeCatcher) Header() http.Header { func (cc *codeCatcher) Header() http.Header {
@ -240,24 +218,7 @@ func (cc *codeCatcher) Flush() {
// codeModifier forwards a response back to the client, // codeModifier forwards a response back to the client,
// while enforcing a given response code. // while enforcing a given response code.
type codeModifier interface { type codeModifier struct {
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 {
code int // the code enforced in the response. code int // the code enforced in the response.
// headerSent is whether the headers have already been sent, // headerSent is whether the headers have already been sent,
@ -268,18 +229,17 @@ type codeModifierWithoutCloseNotify struct {
responseWriter http.ResponseWriter responseWriter http.ResponseWriter
} }
type codeModifierWithCloseNotify struct { // newCodeModifier returns a codeModifier that enforces the given code.
*codeModifierWithoutCloseNotify func newCodeModifier(rw http.ResponseWriter, code int) *codeModifier {
} return &codeModifier{
headerMap: make(http.Header),
// CloseNotify returns a channel that receives at most a code: code,
// single value (true) when the client connection has gone away. responseWriter: rw,
func (r *codeModifierWithCloseNotify) CloseNotify() <-chan bool { }
return r.responseWriter.(http.CloseNotifier).CloseNotify()
} }
// Header returns the response headers. // Header returns the response headers.
func (r *codeModifierWithoutCloseNotify) Header() http.Header { func (r *codeModifier) Header() http.Header {
if r.headerMap == nil { if r.headerMap == nil {
r.headerMap = make(http.Header) r.headerMap = make(http.Header)
} }
@ -289,14 +249,14 @@ func (r *codeModifierWithoutCloseNotify) Header() http.Header {
// Write calls WriteHeader to send the enforced code, // Write calls WriteHeader to send the enforced code,
// then writes the data directly to r.responseWriter. // 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) r.WriteHeader(r.code)
return r.responseWriter.Write(buf) return r.responseWriter.Write(buf)
} }
// WriteHeader sends the headers, with the enforced code (the code in argument // WriteHeader sends the headers, with the enforced code (the code in argument
// is always ignored), if it hasn't already been done. // is always ignored), if it hasn't already been done.
func (r *codeModifierWithoutCloseNotify) WriteHeader(_ int) { func (r *codeModifier) WriteHeader(_ int) {
if r.headerSent { if r.headerSent {
return return
} }
@ -307,7 +267,7 @@ func (r *codeModifierWithoutCloseNotify) WriteHeader(_ int) {
} }
// Hijack hijacks the connection. // 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) hijacker, ok := r.responseWriter.(http.Hijacker)
if !ok { if !ok {
return nil, nil, fmt.Errorf("%T is not a http.Hijacker", r.responseWriter) 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. // Flush sends any buffered data to the client.
func (r *codeModifierWithoutCloseNotify) Flush() { func (r *codeModifier) Flush() {
r.WriteHeader(r.code) r.WriteHeader(r.code)
if flusher, ok := r.responseWriter.(http.Flusher); ok { if flusher, ok := r.responseWriter.(http.Flusher); ok {

View file

@ -188,50 +188,3 @@ type mockServiceBuilder struct {
func (m *mockServiceBuilder) BuildHTTP(_ context.Context, _ string) (http.Handler, error) { func (m *mockServiceBuilder) BuildHTTP(_ context.Context, _ string) (http.Handler, error) {
return m.handler, nil 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")
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -23,17 +23,12 @@ type responseModifier struct {
// modifier can be nil. // modifier can be nil.
func newResponseModifier(w http.ResponseWriter, r *http.Request, modifier func(*http.Response) error) http.ResponseWriter { func newResponseModifier(w http.ResponseWriter, r *http.Request, modifier func(*http.Response) error) http.ResponseWriter {
rm := &responseModifier{ return &responseModifier{
req: r, req: r,
rw: w, rw: w,
modifier: modifier, modifier: modifier,
code: http.StatusOK, code: http.StatusOK,
} }
if _, ok := w.(http.CloseNotifier); ok {
return responseModifierWithCloseNotify{responseModifier: rm}
}
return rm
} }
func (r *responseModifier) WriteHeader(code int) { func (r *responseModifier) WriteHeader(code int) {
@ -97,12 +92,3 @@ func (r *responseModifier) Flush() {
flusher.Flush() flusher.Flush()
} }
} }
type responseModifierWithCloseNotify struct {
*responseModifier
}
// CloseNotify implements http.CloseNotifier.
func (r *responseModifierWithCloseNotify) CloseNotify() <-chan bool {
return r.responseModifier.rw.(http.CloseNotifier).CloseNotify()
}

View file

@ -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) { func (m *metricsMiddleware) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
proto := getRequestProtocol(req) proto := getRequestProtocol(req)

View file

@ -60,47 +60,6 @@ func (m *collectingRetryMetrics) ServiceRetriesCounter() metrics.Counter {
return m.retriesCounter 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) { func Test_getMethod(t *testing.T) {
testCases := []struct { testCases := []struct {
method string method string

View file

@ -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()
}
}

View file

@ -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()
}

View file

@ -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)
})
}
}

View file

@ -20,11 +20,9 @@ import (
) )
// Compile time validation that the response writer implements http interfaces correctly. // Compile time validation that the response writer implements http interfaces correctly.
var _ middlewares.Stateful = &responseWriterWithCloseNotify{} var _ middlewares.Stateful = &responseWriter{}
const ( const typeName = "Retry"
typeName = "Retry"
)
// Listener is used to inform about retry attempts. // Listener is used to inform about retry attempts.
type Listener interface { type Listener interface {
@ -149,57 +147,44 @@ func (l Listeners) Retried(req *http.Request, attempt int) {
} }
} }
type responseWriter interface { func newResponseWriter(rw http.ResponseWriter, shouldRetry bool) *responseWriter {
http.ResponseWriter return &responseWriter{
http.Flusher
ShouldRetry() bool
DisableRetries()
}
func newResponseWriter(rw http.ResponseWriter, shouldRetry bool) responseWriter {
responseWriter := &responseWriterWithoutCloseNotify{
responseWriter: rw, responseWriter: rw,
headers: make(http.Header), headers: make(http.Header),
shouldRetry: shouldRetry, shouldRetry: shouldRetry,
} }
if _, ok := rw.(http.CloseNotifier); ok {
return &responseWriterWithCloseNotify{
responseWriterWithoutCloseNotify: responseWriter,
}
}
return responseWriter
} }
type responseWriterWithoutCloseNotify struct { type responseWriter struct {
responseWriter http.ResponseWriter responseWriter http.ResponseWriter
headers http.Header headers http.Header
shouldRetry bool shouldRetry bool
written bool written bool
} }
func (r *responseWriterWithoutCloseNotify) ShouldRetry() bool { func (r *responseWriter) ShouldRetry() bool {
return r.shouldRetry return r.shouldRetry
} }
func (r *responseWriterWithoutCloseNotify) DisableRetries() { func (r *responseWriter) DisableRetries() {
r.shouldRetry = false r.shouldRetry = false
} }
func (r *responseWriterWithoutCloseNotify) Header() http.Header { func (r *responseWriter) Header() http.Header {
if r.written { if r.written {
return r.responseWriter.Header() return r.responseWriter.Header()
} }
return r.headers return r.headers
} }
func (r *responseWriterWithoutCloseNotify) Write(buf []byte) (int, error) { func (r *responseWriter) Write(buf []byte) (int, error) {
if r.ShouldRetry() { if r.ShouldRetry() {
return len(buf), nil return len(buf), nil
} }
return r.responseWriter.Write(buf) return r.responseWriter.Write(buf)
} }
func (r *responseWriterWithoutCloseNotify) WriteHeader(code int) { func (r *responseWriter) WriteHeader(code int) {
if r.ShouldRetry() && code == http.StatusServiceUnavailable { if r.ShouldRetry() && code == http.StatusServiceUnavailable {
// We get a 503 HTTP Status Code when there is no backend server in the pool // 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() // 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 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) hijacker, ok := r.responseWriter.(http.Hijacker)
if !ok { if !ok {
return nil, nil, fmt.Errorf("%T is not a http.Hijacker", r.responseWriter) 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() return hijacker.Hijack()
} }
func (r *responseWriterWithoutCloseNotify) Flush() { func (r *responseWriter) Flush() {
if flusher, ok := r.responseWriter.(http.Flusher); ok { if flusher, ok := r.responseWriter.(http.Flusher); ok {
flusher.Flush() flusher.Flush()
} }
} }
type responseWriterWithCloseNotify struct {
*responseWriterWithoutCloseNotify
}
func (r *responseWriterWithCloseNotify) CloseNotify() <-chan bool {
return r.responseWriter.(http.CloseNotifier).CloseNotify()
}

View file

@ -8,5 +8,4 @@ type Stateful interface {
http.ResponseWriter http.ResponseWriter
http.Hijacker http.Hijacker
http.Flusher http.Flusher
http.CloseNotifier
} }

View file

@ -48,7 +48,7 @@ func (e *entryPointMiddleware) ServeHTTP(rw http.ResponseWriter, req *http.Reque
req = req.WithContext(tracing.WithTracing(req.Context(), e.Tracing)) req = req.WithContext(tracing.WithTracing(req.Context(), e.Tracing))
recorder := newStatusCodeRecoder(rw, http.StatusOK) recorder := newStatusCodeRecorder(rw, http.StatusOK)
e.next.ServeHTTP(recorder, req) e.next.ServeHTTP(recorder, req)
tracing.LogResponseCode(span, recorder.Status()) tracing.LogResponseCode(span, recorder.Status())

View file

@ -51,7 +51,7 @@ func (f *forwarderMiddleware) ServeHTTP(rw http.ResponseWriter, req *http.Reques
tracing.InjectRequestHeaders(req) tracing.InjectRequestHeaders(req)
recorder := newStatusCodeRecoder(rw, 200) recorder := newStatusCodeRecorder(rw, 200)
f.next.ServeHTTP(recorder, req) f.next.ServeHTTP(recorder, req)

View file

@ -6,52 +6,35 @@ import (
"net/http" "net/http"
) )
type statusCodeRecoder interface { // newStatusCodeRecorder returns an initialized statusCodeRecoder.
http.ResponseWriter func newStatusCodeRecorder(rw http.ResponseWriter, status int) *statusCodeRecorder {
Status() int return &statusCodeRecorder{rw, status}
} }
// newStatusCodeRecoder returns an initialized statusCodeRecoder. type statusCodeRecorder struct {
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 {
http.ResponseWriter http.ResponseWriter
status int status int
} }
// WriteHeader captures the status code for later retrieval. // WriteHeader captures the status code for later retrieval.
func (s *statusCodeWithoutCloseNotify) WriteHeader(status int) { func (s *statusCodeRecorder) WriteHeader(status int) {
s.status = status s.status = status
s.ResponseWriter.WriteHeader(status) s.ResponseWriter.WriteHeader(status)
} }
// Status get response status. // Status get response status.
func (s *statusCodeWithoutCloseNotify) Status() int { func (s *statusCodeRecorder) Status() int {
return s.status return s.status
} }
// Hijack hijacks the connection. // 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() return s.ResponseWriter.(http.Hijacker).Hijack()
} }
// Flush sends any buffered data to the client. // Flush sends any buffered data to the client.
func (s *statusCodeWithoutCloseNotify) Flush() { func (s *statusCodeRecorder) Flush() {
if flusher, ok := s.ResponseWriter.(http.Flusher); ok { if flusher, ok := s.ResponseWriter.(http.Flusher); ok {
flusher.Flush() flusher.Flush()
} }
} }
type statusCodeWithCloseNotify struct {
*statusCodeWithoutCloseNotify
}
func (s *statusCodeWithCloseNotify) CloseNotify() <-chan bool {
return s.ResponseWriter.(http.CloseNotifier).CloseNotify()
}

View file

@ -4,10 +4,12 @@ import (
"context" "context"
"fmt" "fmt"
"testing" "testing"
"time"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "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/dynamic"
"github.com/traefik/traefik/v2/pkg/tls" "github.com/traefik/traefik/v2/pkg/tls"
) )
@ -63,6 +65,9 @@ func TestDefaultRule(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -113,6 +118,9 @@ func TestDefaultRule(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -156,6 +164,9 @@ func TestDefaultRule(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -199,6 +210,9 @@ func TestDefaultRule(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -247,6 +261,9 @@ func TestDefaultRule(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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), 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", 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", ServersTransport: "tls-ns-dc1-dev-Test",
}, },
}, },
@ -470,7 +493,10 @@ func Test_buildConfiguration(t *testing.T) {
URL: "https://127.0.0.2:444", 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", ServersTransport: "tls-ns-dc1-dev-Test",
}, },
}, },
@ -547,6 +573,9 @@ func Test_buildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"Test2": { "Test2": {
@ -557,6 +586,9 @@ func Test_buildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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), 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), 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), 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), 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), 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), 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), 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), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"Service2": { "Service2": {
@ -981,6 +1037,9 @@ func Test_buildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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), 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), 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), 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), 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), 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), 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), 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), 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), 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), 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), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"Service2": { "Service2": {
@ -1743,6 +1835,9 @@ func Test_buildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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), 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), 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), 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), 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", 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", ServersTransport: "tls-ns-dc1-Test",
}, },
}, },
@ -2684,7 +2794,10 @@ func Test_buildConfiguration(t *testing.T) {
URL: "https://127.0.0.2:80", 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", ServersTransport: "tls-ns-dc1-Test",
}, },
}, },

View file

@ -4,12 +4,14 @@ import (
"context" "context"
"strconv" "strconv"
"testing" "testing"
"time"
docker "github.com/docker/docker/api/types" docker "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "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/dynamic"
) )
@ -68,6 +70,9 @@ func TestDefaultRule(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -123,6 +128,9 @@ func TestDefaultRule(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -180,6 +188,9 @@ func TestDefaultRule(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -230,6 +241,9 @@ func TestDefaultRule(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -280,6 +294,9 @@ func TestDefaultRule(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -335,6 +352,9 @@ func TestDefaultRule(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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), 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), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"Test2": { "Test2": {
@ -630,6 +656,9 @@ func Test_buildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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), 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), 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), 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), 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), 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), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"Service2": { "Service2": {
@ -995,6 +1042,9 @@ func Test_buildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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), 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), 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), 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), 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), 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), 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), 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), 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), 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), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"Test2": { "Test2": {
@ -1935,6 +2015,9 @@ func Test_buildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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), 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), 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), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"Service2": { "Service2": {
@ -2110,6 +2202,9 @@ func Test_buildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -2290,6 +2385,9 @@ func Test_buildConfiguration(t *testing.T) {
"Test": { "Test": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), 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), 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), 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), 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), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },

View file

@ -3,10 +3,12 @@ package ecs
import ( import (
"context" "context"
"testing" "testing"
"time"
"github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "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/dynamic"
) )
@ -64,6 +66,9 @@ func TestDefaultRule(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -114,6 +119,9 @@ func TestDefaultRule(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -166,6 +174,9 @@ func TestDefaultRule(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -211,6 +222,9 @@ func TestDefaultRule(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -256,6 +270,9 @@ func TestDefaultRule(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -306,6 +323,9 @@ func TestDefaultRule(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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), 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), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"Test2": { "Test2": {
@ -570,6 +596,9 @@ func Test_buildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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), 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), 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), 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), 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), 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), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"Service2": { "Service2": {
@ -900,6 +947,9 @@ func Test_buildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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), 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), 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), 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), 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), 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), 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), 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), 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), 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), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"Test2": { "Test2": {
@ -1716,6 +1796,9 @@ func Test_buildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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), 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), 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), 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), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"Service2": { "Service2": {
@ -1942,6 +2037,9 @@ func Test_buildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"Service2": { "Service2": {
@ -1999,6 +2100,9 @@ func Test_buildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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), 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), 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), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },

View file

@ -307,7 +307,13 @@ func (c configBuilder) buildServersLB(namespace string, svc v1alpha1.LoadBalance
passHostHeader := true passHostHeader := true
lb.PassHostHeader = &passHostHeader 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 lb.Sticky = svc.Sticky

View file

@ -1283,7 +1283,10 @@ func TestLoadIngressRouteTCPs(t *testing.T) {
URL: "http://[2001:db8:85a3:8d3:1319:8a2e:370:7348]:8080", 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": { "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", 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": { "default-test-route-6b204d94623b3df4370c": {
@ -1510,6 +1516,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -1578,6 +1587,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -1629,6 +1641,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -1686,6 +1701,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -1734,6 +1752,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"default-test-route-77c62dfe9517144aeeaa": { "default-test-route-77c62dfe9517144aeeaa": {
@ -1747,6 +1768,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -1805,6 +1829,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"default-whoami2-8080": { "default-whoami2-8080": {
@ -1818,6 +1845,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -1871,6 +1901,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -1917,6 +1950,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -1988,6 +2024,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"default-whoami5-8080": { "default-whoami5-8080": {
@ -2001,6 +2040,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"default-wrr2": { "default-wrr2": {
@ -2028,6 +2070,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"default-whoami7-8080": { "default-whoami7-8080": {
@ -2041,6 +2086,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -2108,6 +2156,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -2187,6 +2238,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"default-whoami5-8080": { "default-whoami5-8080": {
@ -2200,6 +2254,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -2270,6 +2327,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"foo-wrr1": { "foo-wrr1": {
@ -2305,6 +2365,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"foo-mirror1": { "foo-mirror1": {
@ -2328,6 +2391,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"bar-mirrored": { "bar-mirrored": {
@ -2430,6 +2496,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"default-whoami5-8080": { "default-whoami5-8080": {
@ -2443,6 +2512,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -2514,6 +2586,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"default-whoami5-8080": { "default-whoami5-8080": {
@ -2527,6 +2602,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -2584,6 +2662,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"default-whoami2-8080": { "default-whoami2-8080": {
@ -2597,6 +2678,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -2717,6 +2801,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -2786,6 +2873,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -2834,6 +2924,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -2903,6 +2996,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -2973,6 +3069,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -3041,6 +3140,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -3098,6 +3200,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -3156,6 +3261,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -3200,6 +3308,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -3243,6 +3354,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -3286,6 +3400,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -3533,6 +3650,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -3575,7 +3695,7 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(false), 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), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -3687,6 +3810,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -3733,6 +3859,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -3774,6 +3903,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -3813,6 +3945,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -3852,6 +3987,9 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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", URL: "https://external.domain:443",
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
ServersTransport: "default-test", ServersTransport: "default-test",
}, },
}, },
@ -3963,7 +4104,10 @@ func TestLoadIngressRoutes(t *testing.T) {
URL: "https://10.10.0.6:8443", 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", ServersTransport: "default-default-test",
}, },
}, },
@ -4036,6 +4180,9 @@ func TestLoadIngressRoutes(t *testing.T) {
"default-test-route-6b204d94623b3df4370c": { "default-test-route-6b204d94623b3df4370c": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -4923,6 +5070,9 @@ func TestCrossNamespace(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -4998,6 +5148,9 @@ func TestCrossNamespace(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"default-test-crossnamespace-route-9313b71dbe6a649d5049": { "default-test-crossnamespace-route-9313b71dbe6a649d5049": {
@ -5011,6 +5164,9 @@ func TestCrossNamespace(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"default-test-errorpage-errorpage-service": { "default-test-errorpage-errorpage-service": {
@ -5024,6 +5180,9 @@ func TestCrossNamespace(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"default-test-crossnamespace-route-a1963878aac7331b7950": { "default-test-crossnamespace-route-a1963878aac7331b7950": {
@ -5037,6 +5196,9 @@ func TestCrossNamespace(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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", 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", ServersTransport: "foo-test@kubernetescrd",
}, },
}, },
@ -5126,6 +5291,9 @@ func TestCrossNamespace(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"default-tr-svc-wrr1": { "default-tr-svc-wrr1": {
@ -5181,6 +5349,9 @@ func TestCrossNamespace(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -5227,6 +5398,9 @@ func TestCrossNamespace(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"cross-ns-tr-svc-mirror2": { "cross-ns-tr-svc-mirror2": {
@ -5251,6 +5425,9 @@ func TestCrossNamespace(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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", 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", ServersTransport: "cross-ns-st-cross-ns@kubernetescrd",
}, },
}, },
@ -5387,6 +5567,9 @@ func TestCrossNamespace(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -5431,6 +5614,9 @@ func TestCrossNamespace(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -5936,6 +6122,9 @@ func TestExternalNameService(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },

View file

@ -110,7 +110,7 @@ type LoadBalancerSpec struct {
// By default, passHostHeader is true. // By default, passHostHeader is true.
PassHostHeader *bool `json:"passHostHeader,omitempty"` PassHostHeader *bool `json:"passHostHeader,omitempty"`
// ResponseForwarding defines how Traefik forwards the response from the upstream Kubernetes Service to the client. // 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. // ServersTransport defines the name of ServersTransport resource to use.
// It allows to configure the transport between Traefik and your servers. // It allows to configure the transport between Traefik and your servers.
// Can only be used on a Kubernetes Service. // Can only be used on a Kubernetes Service.
@ -121,6 +121,15 @@ type LoadBalancerSpec struct {
Weight *int `json:"weight,omitempty"` 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. // Service defines an upstream HTTP service to proxy traffic to.
type Service struct { type Service struct {
LoadBalancerSpec `json:",inline"` LoadBalancerSpec `json:",inline"`

View file

@ -559,7 +559,7 @@ func (in *LoadBalancerSpec) DeepCopyInto(out *LoadBalancerSpec) {
} }
if in.ResponseForwarding != nil { if in.ResponseForwarding != nil {
in, out := &in.ResponseForwarding, &out.ResponseForwarding in, out := &in.ResponseForwarding, &out.ResponseForwarding
*out = new(dynamic.ResponseForwarding) *out = new(ResponseForwarding)
**out = **in **out = **in
} }
if in.Weight != nil { if in.Weight != nil {
@ -973,6 +973,22 @@ func (in *RateLimit) DeepCopy() *RateLimit {
return out 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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Retry) DeepCopyInto(out *Retry) { func (in *Retry) DeepCopyInto(out *Retry) {
*out = *in *out = *in

View file

@ -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) return nil, nil, fmt.Errorf("unsupported HTTPBackendRef %s/%s/%s", *backendRef.Group, *backendRef.Kind, backendRef.Name)
} }
svc := dynamic.Service{ lb := &dynamic.ServersLoadBalancer{}
LoadBalancer: &dynamic.ServersLoadBalancer{ lb.SetDefaults()
PassHostHeader: pointer.Bool(true),
}, svc := dynamic.Service{LoadBalancer: lb}
}
// TODO support cross namespace through ReferencePolicy // TODO support cross namespace through ReferencePolicy
service, exists, err := client.GetService(namespace, string(backendRef.Name)) service, exists, err := client.GetService(namespace, string(backendRef.Name))

View file

@ -3,9 +3,11 @@ package gateway
import ( import (
"context" "context"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "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/dynamic"
"github.com/traefik/traefik/v2/pkg/provider" "github.com/traefik/traefik/v2/pkg/provider"
"github.com/traefik/traefik/v2/pkg/tls" "github.com/traefik/traefik/v2/pkg/tls"
@ -552,6 +554,9 @@ func TestLoadHTTPRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: pointer.Bool(true), 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), 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), 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), 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), 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), 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), PassHostHeader: pointer.Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"default-whoami2-8080": { "default-whoami2-8080": {
@ -955,6 +978,9 @@ func TestLoadHTTPRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: pointer.Bool(true), 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), PassHostHeader: pointer.Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"default-whoami2-8080": { "default-whoami2-8080": {
@ -1027,6 +1056,9 @@ func TestLoadHTTPRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: pointer.Bool(true), 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), 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), 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), 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), 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), PassHostHeader: pointer.Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"bar-whoami-bar-80": { "bar-whoami-bar-80": {
@ -1405,6 +1452,9 @@ func TestLoadHTTPRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: pointer.Bool(true), 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), 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), 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), 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), PassHostHeader: pointer.Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"bar-whoami-bar-80": { "bar-whoami-bar-80": {
@ -3936,6 +3998,9 @@ func TestLoadMixedRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: pointer.Bool(true), PassHostHeader: pointer.Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"bar-http-app-bar-my-gateway-web-a431b128267aabc954fd-wrr": { "bar-http-app-bar-my-gateway-web-a431b128267aabc954fd-wrr": {
@ -4083,6 +4148,9 @@ func TestLoadMixedRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: pointer.Bool(true), PassHostHeader: pointer.Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"bar-http-app-bar-my-gateway-web-a431b128267aabc954fd-wrr": { "bar-http-app-bar-my-gateway-web-a431b128267aabc954fd-wrr": {
@ -4231,6 +4299,9 @@ func TestLoadMixedRoutes(t *testing.T) {
}, },
}, },
PassHostHeader: pointer.Bool(true), PassHostHeader: pointer.Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },

View file

@ -517,11 +517,10 @@ func (p *Provider) loadService(client Client, namespace string, backend networki
return nil, errors.New("service port not found") return nil, errors.New("service port not found")
} }
svc := &dynamic.Service{ lb := &dynamic.ServersLoadBalancer{}
LoadBalancer: &dynamic.ServersLoadBalancer{ lb.SetDefaults()
PassHostHeader: func(v bool) *bool { return &v }(true),
}, svc := &dynamic.Service{LoadBalancer: lb}
}
svcConfig, err := parseServiceConfig(service.Annotations) svcConfig, err := parseServiceConfig(service.Annotations)
if err != nil { if err != nil {

View file

@ -7,8 +7,10 @@ import (
"os" "os"
"strings" "strings"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "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/config/dynamic"
"github.com/traefik/traefik/v2/pkg/provider" "github.com/traefik/traefik/v2/pkg/provider"
"github.com/traefik/traefik/v2/pkg/tls" "github.com/traefik/traefik/v2/pkg/tls"
@ -68,6 +70,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -115,6 +120,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Sticky: &dynamic.Sticky{ Sticky: &dynamic.Sticky{
Cookie: &dynamic.Cookie{ Cookie: &dynamic.Cookie{
Name: "foobar", Name: "foobar",
@ -157,6 +165,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -191,6 +202,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -225,6 +239,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -259,6 +276,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -289,6 +309,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -319,6 +342,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-example-com-80": { "testing-example-com-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.11.0.1:80", URL: "http://10.11.0.1:80",
@ -350,6 +376,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -384,6 +413,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -418,6 +450,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -431,6 +466,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service2-8082": { "testing-service2-8082": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.2:8080", URL: "http://10.10.0.2:8080",
@ -462,6 +500,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -496,6 +537,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"default-backend": { "default-backend": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -526,6 +570,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8089", URL: "http://10.10.0.1:8089",
@ -556,6 +603,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-tchouk": { "testing-service1-tchouk": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8089", URL: "http://10.10.0.1:8089",
@ -586,6 +636,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-tchouk": { "testing-service1-tchouk": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8089", URL: "http://10.10.0.1:8089",
@ -620,6 +673,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-tchouk": { "testing-service1-tchouk": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8089", URL: "http://10.10.0.1:8089",
@ -633,6 +689,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-carotte": { "testing-service1-carotte": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8090", URL: "http://10.10.0.1:8090",
@ -663,6 +722,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-tchouk": { "testing-service1-tchouk": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8089", URL: "http://10.10.0.1:8089",
@ -697,6 +759,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-tchouk": { "testing-service1-tchouk": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8089", URL: "http://10.10.0.1:8089",
@ -710,6 +775,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"toto-service1-tchouk": { "toto-service1-tchouk": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.11.0.1:8089", URL: "http://10.11.0.1:8089",
@ -762,6 +830,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-8080": { "testing-service1-8080": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.0.0.1:8080", URL: "http://10.0.0.1:8080",
@ -790,6 +861,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-example-com-80": { "testing-example-com-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.11.0.1:80", URL: "http://10.11.0.1:80",
@ -827,6 +901,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-443": { "testing-service1-443": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "https://10.10.0.1:8443", URL: "https://10.10.0.1:8443",
@ -857,6 +934,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-8443": { "testing-service1-8443": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "https://10.10.0.1:8443", URL: "https://10.10.0.1:8443",
@ -888,6 +968,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-8443": { "testing-service1-8443": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "https://10.10.0.1:8443", URL: "https://10.10.0.1:8443",
@ -919,6 +1002,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"default-backend": { "default-backend": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.30.0.1:8080", URL: "http://10.30.0.1:8080",
@ -949,6 +1035,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -1023,6 +1112,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -1053,6 +1145,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -1085,6 +1180,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -1113,6 +1211,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -1141,6 +1242,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -1169,6 +1273,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -1197,6 +1304,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -1225,6 +1335,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -1265,6 +1378,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -1294,6 +1410,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -1322,6 +1441,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -1350,6 +1472,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -1378,6 +1503,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -1406,6 +1534,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -1434,6 +1565,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -1462,6 +1596,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -1490,6 +1627,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -1518,6 +1658,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-80": { "testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -1546,6 +1689,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"testing-service1-foobar": { "testing-service1-foobar": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:4711", URL: "http://10.10.0.1:4711",
@ -1587,6 +1733,9 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
"default-backend": { "default-backend": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://10.10.0.1:8080", URL: "http://10.10.0.1:8080",
@ -1678,6 +1827,9 @@ func TestLoadConfigurationFromIngressesWithExternalNameServices(t *testing.T) {
"testing-service1-8080": { "testing-service1-8080": {
LoadBalancer: &dynamic.ServersLoadBalancer{ LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: "http://traefik.wtf:8080", URL: "http://traefik.wtf:8080",
@ -1710,6 +1862,9 @@ func TestLoadConfigurationFromIngressesWithExternalNameServices(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -1738,6 +1893,9 @@ func TestLoadConfigurationFromIngressesWithExternalNameServices(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },

View file

@ -42,15 +42,15 @@ func Test_buildConfiguration(t *testing.T) {
"traefik/http/routers/Router1/service": "foobar", "traefik/http/routers/Router1/service": "foobar",
"traefik/http/services/Service01/loadBalancer/healthCheck/path": "foobar", "traefik/http/services/Service01/loadBalancer/healthCheck/path": "foobar",
"traefik/http/services/Service01/loadBalancer/healthCheck/port": "42", "traefik/http/services/Service01/loadBalancer/healthCheck/port": "42",
"traefik/http/services/Service01/loadBalancer/healthCheck/interval": "foobar", "traefik/http/services/Service01/loadBalancer/healthCheck/interval": "1s",
"traefik/http/services/Service01/loadBalancer/healthCheck/timeout": "foobar", "traefik/http/services/Service01/loadBalancer/healthCheck/timeout": "1s",
"traefik/http/services/Service01/loadBalancer/healthCheck/hostname": "foobar", "traefik/http/services/Service01/loadBalancer/healthCheck/hostname": "foobar",
"traefik/http/services/Service01/loadBalancer/healthCheck/headers/name0": "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/headers/name1": "foobar",
"traefik/http/services/Service01/loadBalancer/healthCheck/scheme": "foobar", "traefik/http/services/Service01/loadBalancer/healthCheck/scheme": "foobar",
"traefik/http/services/Service01/loadBalancer/healthCheck/mode": "foobar", "traefik/http/services/Service01/loadBalancer/healthCheck/mode": "foobar",
"traefik/http/services/Service01/loadBalancer/healthCheck/followredirects": "true", "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/passHostHeader": "true",
"traefik/http/services/Service01/loadBalancer/sticky/cookie/name": "foobar", "traefik/http/services/Service01/loadBalancer/sticky/cookie/name": "foobar",
"traefik/http/services/Service01/loadBalancer/sticky/cookie/secure": "true", "traefik/http/services/Service01/loadBalancer/sticky/cookie/secure": "true",
@ -646,8 +646,8 @@ func Test_buildConfiguration(t *testing.T) {
Mode: "foobar", Mode: "foobar",
Path: "foobar", Path: "foobar",
Port: 42, Port: 42,
Interval: "foobar", Interval: ptypes.Duration(time.Second),
Timeout: "foobar", Timeout: ptypes.Duration(time.Second),
Hostname: "foobar", Hostname: "foobar",
FollowRedirects: func(v bool) *bool { return &v }(true), FollowRedirects: func(v bool) *bool { return &v }(true),
Headers: map[string]string{ Headers: map[string]string{
@ -657,7 +657,7 @@ func Test_buildConfiguration(t *testing.T) {
}, },
PassHostHeader: func(v bool) *bool { return &v }(true), PassHostHeader: func(v bool) *bool { return &v }(true),
ResponseForwarding: &dynamic.ResponseForwarding{ ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: "foobar", FlushInterval: ptypes.Duration(time.Second),
}, },
}, },
}, },

View file

@ -4,10 +4,12 @@ import (
"context" "context"
"math" "math"
"testing" "testing"
"time"
"github.com/gambol99/go-marathon" "github.com/gambol99/go-marathon"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "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/dynamic"
) )
@ -71,6 +73,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}}, }},
}, },
ServersTransports: map[string]*dynamic.ServersTransport{}, ServersTransports: map[string]*dynamic.ServersTransport{},
@ -137,6 +142,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}}, }},
}, },
ServersTransports: map[string]*dynamic.ServersTransport{}, ServersTransports: map[string]*dynamic.ServersTransport{},
@ -189,6 +197,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}}, }},
}, },
ServersTransports: map[string]*dynamic.ServersTransport{}, ServersTransports: map[string]*dynamic.ServersTransport{},
@ -295,6 +306,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}}, }},
}, },
ServersTransports: map[string]*dynamic.ServersTransport{}, ServersTransports: map[string]*dynamic.ServersTransport{},
@ -356,6 +370,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}}, }},
}, },
ServersTransports: map[string]*dynamic.ServersTransport{}, ServersTransports: map[string]*dynamic.ServersTransport{},
@ -404,6 +421,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}}, }},
"bar": {LoadBalancer: &dynamic.ServersLoadBalancer{ "bar": {LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{ Servers: []dynamic.Server{
@ -412,6 +432,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}}, }},
}, },
ServersTransports: map[string]*dynamic.ServersTransport{}, ServersTransports: map[string]*dynamic.ServersTransport{},
@ -456,6 +479,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -498,6 +524,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}}, }},
}, },
ServersTransports: map[string]*dynamic.ServersTransport{}, ServersTransports: map[string]*dynamic.ServersTransport{},
@ -542,6 +571,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -579,6 +611,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -629,6 +664,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -669,6 +707,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"Service2": { "Service2": {
@ -679,6 +720,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -780,6 +824,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"app2": { "app2": {
@ -790,6 +837,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -843,6 +893,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"app2": { "app2": {
@ -853,6 +906,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -897,6 +953,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"app2": { "app2": {
@ -907,6 +966,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -958,6 +1020,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -1002,6 +1067,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"app2": { "app2": {
@ -1012,6 +1080,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -1055,6 +1126,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -1099,6 +1173,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -1138,6 +1215,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"Service2": { "Service2": {
@ -1148,6 +1228,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -1355,6 +1438,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -1399,6 +1485,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -1442,6 +1531,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -1766,6 +1858,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },
@ -1826,6 +1921,9 @@ func TestBuildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },

View file

@ -3,9 +3,11 @@ package nomad
import ( import (
"context" "context"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "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/dynamic"
) )
@ -56,6 +58,9 @@ func Test_defaultRule(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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), 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), 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), 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), 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), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"Test2": { "Test2": {
@ -333,6 +353,9 @@ func Test_buildConfig(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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), 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), 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), 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), 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), 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), 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), 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), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"Service2": { "Service2": {
@ -757,6 +804,9 @@ func Test_buildConfig(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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), 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), 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), 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), 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), 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), 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), 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), 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), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"Service2": { "Service2": {
@ -1372,6 +1449,9 @@ func Test_buildConfig(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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), 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), 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), 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), 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), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"Test-1234154071633021619": { "Test-1234154071633021619": {
@ -2282,6 +2377,9 @@ func Test_buildConfig(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },

View file

@ -3,9 +3,11 @@ package rancher
import ( import (
"context" "context"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "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/dynamic"
) )
@ -58,6 +60,9 @@ func Test_buildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"Test2": { "Test2": {
@ -126,6 +134,9 @@ func Test_buildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
"Test2": { "Test2": {
@ -197,6 +211,9 @@ func Test_buildConfiguration(t *testing.T) {
}, },
}, },
PassHostHeader: Bool(true), 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), 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), 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), 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), 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), 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), 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), PassHostHeader: Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
}, },
}, },
}, },

View file

@ -82,8 +82,8 @@ func init() {
Scheme: "foo", Scheme: "foo",
Path: "foo", Path: "foo",
Port: 42, Port: 42,
Interval: "foo", Interval: ptypes.Duration(111 * time.Second),
Timeout: "foo", Timeout: ptypes.Duration(111 * time.Second),
Hostname: "foo", Hostname: "foo",
FollowRedirects: boolPtr(true), FollowRedirects: boolPtr(true),
Headers: map[string]string{ Headers: map[string]string{
@ -92,7 +92,7 @@ func init() {
}, },
PassHostHeader: boolPtr(true), PassHostHeader: boolPtr(true),
ResponseForwarding: &dynamic.ResponseForwarding{ ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: "foo", FlushInterval: ptypes.Duration(111 * time.Second),
}, },
ServersTransport: "foo", ServersTransport: "foo",
Servers: []dynamic.Server{ Servers: []dynamic.Server{

View file

@ -75,8 +75,8 @@
"scheme": "foo", "scheme": "foo",
"path": "foo", "path": "foo",
"port": 42, "port": 42,
"interval": "foo", "interval": "1m51s",
"timeout": "foo", "timeout": "1m51s",
"hostname": "xxxx", "hostname": "xxxx",
"followRedirects": true, "followRedirects": true,
"headers": { "headers": {
@ -85,7 +85,7 @@
}, },
"passHostHeader": true, "passHostHeader": true,
"responseForwarding": { "responseForwarding": {
"flushInterval": "foo" "flushInterval": "1m51s"
}, },
"serversTransport": "foo" "serversTransport": "foo"
} }

View file

@ -75,8 +75,8 @@
"scheme": "foo", "scheme": "foo",
"path": "foo", "path": "foo",
"port": 42, "port": 42,
"interval": "foo", "interval": "1m51s",
"timeout": "foo", "timeout": "1m51s",
"hostname": "foo", "hostname": "foo",
"followRedirects": true, "followRedirects": true,
"headers": { "headers": {
@ -85,7 +85,7 @@
}, },
"passHostHeader": true, "passHostHeader": true,
"responseForwarding": { "responseForwarding": {
"flushInterval": "foo" "flushInterval": "1m51s"
}, },
"serversTransport": "foo" "serversTransport": "foo"
} }

View file

@ -24,7 +24,7 @@ type middlewareBuilder interface {
type serviceManager interface { type serviceManager interface {
BuildHTTP(rootCtx context.Context, serviceName string) (http.Handler, error) BuildHTTP(rootCtx context.Context, serviceName string) (http.Handler, error)
LaunchHealthCheck() LaunchHealthCheck(ctx context.Context)
} }
// Manager A route/router manager. // Manager A route/router manager.

View file

@ -7,10 +7,12 @@ import (
"net/http/httptest" "net/http/httptest"
"strings" "strings"
"testing" "testing"
"time"
"github.com/containous/alice" "github.com/containous/alice"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "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/dynamic"
"github.com/traefik/traefik/v2/pkg/config/runtime" "github.com/traefik/traefik/v2/pkg/config/runtime"
"github.com/traefik/traefik/v2/pkg/metrics" "github.com/traefik/traefik/v2/pkg/metrics"
@ -478,7 +480,7 @@ func TestRuntimeConfiguration(t *testing.T) {
}, },
}, },
HealthCheck: &dynamic.ServerHealthCheck{ HealthCheck: &dynamic.ServerHealthCheck{
Interval: "500ms", Interval: ptypes.Duration(500 * time.Millisecond),
Path: "/health", Path: "/health",
}, },
}, },

View file

@ -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
}

View file

@ -108,6 +108,17 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) {
// TODO -- Check if ProxyProtocol changes the first bytes of the request // TODO -- Check if ProxyProtocol changes the first bytes of the request
br := bufio.NewReader(conn) 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) hello, err := clientHelloInfo(br)
if err != nil { if err != nil {
conn.Close() conn.Close()
@ -277,7 +288,7 @@ func (r *Router) SetHTTPSHandler(handler http.Handler, config *tls.Config) {
type Conn struct { type Conn struct {
// Peeked are the bytes that have been read from Conn for the // Peeked are the bytes that have been read from Conn for the
// purposes of route matching, but have not yet been consumed // 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 Peeked []byte
// Conn is the underlying connection. // Conn is the underlying connection.

View file

@ -922,3 +922,89 @@ func checkHTTPSTLS10(addr string, timeout time.Duration) error {
func checkHTTPSTLS12(addr string, timeout time.Duration) error { func checkHTTPSTLS12(addr string, timeout time.Duration) error {
return checkHTTPS(addr, timeout, tls.VersionTLS12) 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
}

View file

@ -31,6 +31,8 @@ type RouterFactory struct {
chainBuilder *middleware.ChainBuilder chainBuilder *middleware.ChainBuilder
tlsManager *tls.Manager tlsManager *tls.Manager
cancelPrevState func()
} }
// NewRouterFactory creates a new RouterFactory. // NewRouterFactory creates a new RouterFactory.
@ -65,7 +67,12 @@ func NewRouterFactory(staticConfiguration static.Configuration, managerFactory *
// CreateRouters creates new TCPRouters and UDPRouters. // CreateRouters creates new TCPRouters and UDPRouters.
func (f *RouterFactory) CreateRouters(rtConf *runtime.Configuration) (map[string]*tcprouter.Router, map[string]udptypes.Handler) { 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 // HTTP
serviceManager := f.managerFactory.Build(rtConf) 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) handlersNonTLS := routerManager.BuildHandlers(ctx, f.entryPointsTCP, false)
handlersTLS := routerManager.BuildHandlers(ctx, f.entryPointsTCP, true) handlersTLS := routerManager.BuildHandlers(ctx, f.entryPointsTCP, true)
serviceManager.LaunchHealthCheck() serviceManager.LaunchHealthCheck(ctx)
// TCP // TCP
svcTCPManager := tcp.NewManager(rtConf) svcTCPManager := tcp.NewManager(rtConf)

View file

@ -10,7 +10,7 @@ import (
type serviceManager interface { type serviceManager interface {
BuildHTTP(rootCtx context.Context, serviceName string) (http.Handler, error) BuildHTTP(rootCtx context.Context, serviceName string) (http.Handler, error)
LaunchHealthCheck() LaunchHealthCheck(ctx context.Context)
} }
// InternalHandlers is the internal HTTP handlers builder. // InternalHandlers is the internal HTTP handlers builder.

View file

@ -4,7 +4,6 @@ import (
"container/heap" "container/heap"
"context" "context"
"errors" "errors"
"fmt"
"net/http" "net/http"
"sync" "sync"
@ -39,7 +38,7 @@ type Balancer struct {
curDeadline float64 curDeadline float64
// status is a record of which child services of the Balancer are healthy, keyed // 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 // 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. // through the SetStatus method.
status map[string]struct{} status map[string]struct{}
// updaters is the list of hooks that are run (to update the Balancer // 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. // 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{ balancer := &Balancer{
status: make(map[string]struct{}), status: make(map[string]struct{}),
wantsHealthCheck: hc != nil, wantsHealthCheck: wantHealthCheck,
} }
if sticky != nil && sticky.Cookie != nil { if sticky != nil && sticky.Cookie != nil {
balancer.stickyCookie = &stickyCookie{ balancer.stickyCookie = &stickyCookie{
@ -150,10 +149,7 @@ func (b *Balancer) nextServer() (*namedHandler, error) {
b.mutex.Lock() b.mutex.Lock()
defer b.mutex.Unlock() defer b.mutex.Unlock()
if len(b.handlers) == 0 { if len(b.handlers) == 0 || len(b.status) == 0 {
return nil, fmt.Errorf("no servers in the pool")
}
if len(b.status) == 0 {
return nil, errNoAvailableServer return nil, errNoAvailableServer
} }
@ -223,9 +219,9 @@ func (b *Balancer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
server.ServeHTTP(w, req) server.ServeHTTP(w, req)
} }
// AddService adds a handler. // Add adds a handler.
// A handler with a non-positive weight is ignored. // 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 w := 1
if weight != nil { if weight != nil {
w = *weight w = *weight

View file

@ -10,31 +10,15 @@ import (
"github.com/traefik/traefik/v2/pkg/config/dynamic" "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) { 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.Header().Set("server", "first")
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
}), Int(3)) }), 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.Header().Set("server", "second")
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
}), Int(1)) }), Int(1))
@ -49,23 +33,23 @@ func TestBalancer(t *testing.T) {
} }
func TestBalancerNoService(t *testing.T) { func TestBalancerNoService(t *testing.T) {
balancer := New(nil, nil) balancer := New(nil, false)
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
balancer.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/", nil)) 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) { 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.Header().Set("server", "first")
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
}), Int(1)) }), 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{}} recorder := &responseRecorder{ResponseRecorder: httptest.NewRecorder(), save: map[string]int{}}
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
@ -80,13 +64,13 @@ type key string
const serviceName key = "serviceName" const serviceName key = "serviceName"
func TestBalancerNoServiceUp(t *testing.T) { 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) rw.WriteHeader(http.StatusInternalServerError)
}), Int(1)) }), 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) rw.WriteHeader(http.StatusInternalServerError)
}), Int(1)) }), Int(1))
@ -100,14 +84,14 @@ func TestBalancerNoServiceUp(t *testing.T) {
} }
func TestBalancerOneServerDown(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.Header().Set("server", "first")
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
}), Int(1)) }), 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) rw.WriteHeader(http.StatusInternalServerError)
}), Int(1)) }), Int(1))
balancer.SetStatus(context.WithValue(context.Background(), serviceName, "parent"), "second", false) balancer.SetStatus(context.WithValue(context.Background(), serviceName, "parent"), "second", false)
@ -121,14 +105,14 @@ func TestBalancerOneServerDown(t *testing.T) {
} }
func TestBalancerDownThenUp(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.Header().Set("server", "first")
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
}), Int(1)) }), 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.Header().Set("server", "second")
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
}), Int(1)) }), Int(1))
@ -150,35 +134,35 @@ func TestBalancerDownThenUp(t *testing.T) {
} }
func TestBalancerPropagate(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.Header().Set("server", "first")
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
}), Int(1)) }), 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.Header().Set("server", "second")
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
}), Int(1)) }), Int(1))
balancer2 := New(nil, &dynamic.HealthCheck{}) balancer2 := New(nil, true)
balancer2.AddService("third", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { balancer2.Add("third", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("server", "third") rw.Header().Set("server", "third")
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
}), Int(1)) }), 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.Header().Set("server", "fourth")
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
}), Int(1)) }), Int(1))
topBalancer := New(nil, &dynamic.HealthCheck{}) topBalancer := New(nil, true)
topBalancer.AddService("balancer1", balancer1, Int(1)) topBalancer.Add("balancer1", balancer1, Int(1))
_ = balancer1.RegisterStatusUpdater(func(up bool) { _ = balancer1.RegisterStatusUpdater(func(up bool) {
topBalancer.SetStatus(context.WithValue(context.Background(), serviceName, "top"), "balancer1", up) topBalancer.SetStatus(context.WithValue(context.Background(), serviceName, "top"), "balancer1", up)
// TODO(mpl): if test gets flaky, add channel or something here to signal that // TODO(mpl): if test gets flaky, add channel or something here to signal that
// propagation is done, and wait on it before sending request. // 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) { _ = balancer2.RegisterStatusUpdater(func(up bool) {
topBalancer.SetStatus(context.WithValue(context.Background(), serviceName, "top"), "balancer2", up) topBalancer.SetStatus(context.WithValue(context.Background(), serviceName, "top"), "balancer2", up)
}) })
@ -223,28 +207,28 @@ func TestBalancerPropagate(t *testing.T) {
} }
func TestBalancerAllServersZeroWeight(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.Add("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("test2", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}), Int(0))
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
balancer.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/", nil)) 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) { func TestSticky(t *testing.T) {
balancer := New(&dynamic.Sticky{ balancer := New(&dynamic.Sticky{
Cookie: &dynamic.Cookie{Name: "test"}, 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.Header().Set("server", "first")
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
}), Int(1)) }), 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.Header().Set("server", "second")
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
}), Int(2)) }), 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, // 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. // and that it does not "over-favor" the high-weighted ones with a biased start-up regime.
func TestBalancerBias(t *testing.T) { 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.Header().Set("server", "A")
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
}), Int(11)) }), 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.Header().Set("server", "B")
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
}), Int(3)) }), Int(3))
@ -290,3 +274,19 @@ func TestBalancerBias(t *testing.T) {
assert.Equal(t, wantSequence, recorder.sequence) 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)
}

View file

@ -3,7 +3,6 @@ package service
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"io" "io"
"net" "net"
"net/http" "net/http"
@ -12,8 +11,6 @@ import (
"strings" "strings"
"time" "time"
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/log" "github.com/traefik/traefik/v2/pkg/log"
"golang.org/x/net/http/httpguts" "golang.org/x/net/http/httpguts"
) )
@ -24,103 +21,107 @@ const StatusClientClosedRequest = 499
// StatusClientClosedRequestText non-standard HTTP status for client disconnection. // StatusClientClosedRequestText non-standard HTTP status for client disconnection.
const StatusClientClosedRequestText = "Client Closed Request" const StatusClientClosedRequestText = "Client Closed Request"
func buildProxy(passHostHeader *bool, responseForwarding *dynamic.ResponseForwarding, roundTripper http.RoundTripper, bufferPool httputil.BufferPool) (http.Handler, error) { func buildSingleHostProxy(target *url.URL, passHostHeader bool, flushInterval time.Duration, roundTripper http.RoundTripper, bufferPool httputil.BufferPool) http.Handler {
var flushInterval ptypes.Duration return &httputil.ReverseProxy{
if responseForwarding != nil { Director: directorBuilder(target, passHostHeader),
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")
}
},
Transport: roundTripper, Transport: roundTripper,
FlushInterval: time.Duration(flushInterval), FlushInterval: flushInterval,
BufferPool: bufferPool, BufferPool: bufferPool,
ErrorHandler: func(w http.ResponseWriter, request *http.Request, err error) { ErrorHandler: errorHandler,
statusCode := http.StatusInternalServerError }
}
switch { func directorBuilder(target *url.URL, passHostHeader bool) func(req *http.Request) {
case errors.Is(err, io.EOF): return func(outReq *http.Request) {
statusCode = http.StatusBadGateway outReq.URL.Scheme = target.Scheme
case errors.Is(err, context.Canceled): outReq.URL.Host = target.Host
statusCode = StatusClientClosedRequest
default:
var netErr net.Error
if errors.As(err, &netErr) {
if netErr.Timeout() {
statusCode = http.StatusGatewayTimeout
} else {
statusCode = http.StatusBadGateway
}
}
}
log.Debugf("'%d %s' caused by: %v", statusCode, statusText(statusCode), err) u := outReq.URL
w.WriteHeader(statusCode) if outReq.RequestURI != "" {
_, werr := w.Write([]byte(statusText(statusCode))) parsedURL, err := url.ParseRequestURI(outReq.RequestURI)
if werr != nil { if err == nil {
log.Debugf("Error while writing status code", werr) 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 { func isWebSocketUpgrade(req *http.Request) bool {
if !httpguts.HeaderValuesContainsToken(req.Header["Connection"], "Upgrade") { return httpguts.HeaderValuesContainsToken(req.Header["Connection"], "Upgrade") &&
return false 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 { func statusText(statusCode int) string {

View file

@ -28,7 +28,7 @@ func BenchmarkProxy(b *testing.B) {
req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil) req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)
pool := newBufferPool() pool := newBufferPool()
handler, _ := buildProxy(Bool(false), nil, &staticTransport{res}, pool) handler := buildSingleHostProxy(req.URL, false, 0, &staticTransport{res}, pool)
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {

View file

@ -18,12 +18,7 @@ import (
"golang.org/x/net/websocket" "golang.org/x/net/websocket"
) )
func Bool(v bool) *bool { return &v }
func TestWebSocketTCPClose(t *testing.T) { func TestWebSocketTCPClose(t *testing.T) {
f, err := buildProxy(Bool(true), nil, http.DefaultTransport, nil)
require.NoError(t, err)
errChan := make(chan error, 1) errChan := make(chan error, 1)
upgrader := gorillawebsocket.Upgrader{} upgrader := gorillawebsocket.Upgrader{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -42,7 +37,7 @@ func TestWebSocketTCPClose(t *testing.T) {
})) }))
defer srv.Close() defer srv.Close()
proxy := createProxyWithForwarder(t, f, srv.URL) proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport)
proxyAddr := proxy.Listener.Addr().String() proxyAddr := proxy.Listener.Addr().String()
_, conn, err := newWebsocketRequest( _, conn, err := newWebsocketRequest(
@ -61,10 +56,6 @@ func TestWebSocketTCPClose(t *testing.T) {
} }
func TestWebSocketPingPong(t *testing.T) { func TestWebSocketPingPong(t *testing.T) {
f, err := buildProxy(Bool(true), nil, http.DefaultTransport, nil)
require.NoError(t, err)
upgrader := gorillawebsocket.Upgrader{ upgrader := gorillawebsocket.Upgrader{
HandshakeTimeout: 10 * time.Second, HandshakeTimeout: 10 * time.Second,
CheckOrigin: func(*http.Request) bool { CheckOrigin: func(*http.Request) bool {
@ -86,17 +77,10 @@ func TestWebSocketPingPong(t *testing.T) {
_, _, _ = ws.ReadMessage() _, _, _ = ws.ReadMessage()
}) })
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { srv := httptest.NewServer(mux)
mux.ServeHTTP(w, req)
}))
defer srv.Close() defer srv.Close()
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport)
req.URL = parseURI(t, srv.URL)
f.ServeHTTP(w, req)
}))
defer proxy.Close()
serverAddr := proxy.Listener.Addr().String() serverAddr := proxy.Listener.Addr().String()
headers := http.Header{} headers := http.Header{}
@ -127,9 +111,6 @@ func TestWebSocketPingPong(t *testing.T) {
} }
func TestWebSocketEcho(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 := http.NewServeMux()
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) { mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
msg := make([]byte, 4) msg := make([]byte, 4)
@ -145,17 +126,10 @@ func TestWebSocketEcho(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
})) }))
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { srv := httptest.NewServer(mux)
mux.ServeHTTP(w, req)
}))
defer srv.Close() defer srv.Close()
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport)
req.URL = parseURI(t, srv.URL)
f.ServeHTTP(w, req)
}))
defer proxy.Close()
serverAddr := proxy.Listener.Addr().String() serverAddr := proxy.Listener.Addr().String()
headers := http.Header{} headers := http.Header{}
@ -193,10 +167,6 @@ func TestWebSocketPassHost(t *testing.T) {
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { 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 := http.NewServeMux()
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) { mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
req := conn.Request() req := conn.Request()
@ -208,7 +178,7 @@ func TestWebSocketPassHost(t *testing.T) {
} }
msg := make([]byte, 4) msg := make([]byte, 4)
_, err = conn.Read(msg) _, err := conn.Read(msg)
require.NoError(t, err) require.NoError(t, err)
fmt.Println(string(msg)) fmt.Println(string(msg))
@ -219,16 +189,10 @@ func TestWebSocketPassHost(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
})) }))
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { srv := httptest.NewServer(mux)
mux.ServeHTTP(w, req)
}))
defer srv.Close() defer srv.Close()
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport)
req.URL = parseURI(t, srv.URL)
f.ServeHTTP(w, req)
}))
defer proxy.Close()
serverAddr := proxy.Listener.Addr().String() serverAddr := proxy.Listener.Addr().String()
@ -252,9 +216,6 @@ func TestWebSocketPassHost(t *testing.T) {
} }
func TestWebSocketServerWithoutCheckOrigin(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 { upgrader := gorillawebsocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
return true return true
}} }}
@ -277,7 +238,7 @@ func TestWebSocketServerWithoutCheckOrigin(t *testing.T) {
})) }))
defer srv.Close() defer srv.Close()
proxy := createProxyWithForwarder(t, f, srv.URL) proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport)
defer proxy.Close() defer proxy.Close()
proxyAddr := proxy.Listener.Addr().String() proxyAddr := proxy.Listener.Addr().String()
@ -293,9 +254,6 @@ func TestWebSocketServerWithoutCheckOrigin(t *testing.T) {
} }
func TestWebSocketRequestWithOrigin(t *testing.T) { func TestWebSocketRequestWithOrigin(t *testing.T) {
f, err := buildProxy(Bool(true), nil, http.DefaultTransport, nil)
require.NoError(t, err)
upgrader := gorillawebsocket.Upgrader{} upgrader := gorillawebsocket.Upgrader{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil) c, err := upgrader.Upgrade(w, r, nil)
@ -316,11 +274,11 @@ func TestWebSocketRequestWithOrigin(t *testing.T) {
})) }))
defer srv.Close() defer srv.Close()
proxy := createProxyWithForwarder(t, f, srv.URL) proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport)
defer proxy.Close() defer proxy.Close()
proxyAddr := proxy.Listener.Addr().String() proxyAddr := proxy.Listener.Addr().String()
_, err = newWebsocketRequest( _, err := newWebsocketRequest(
withServer(proxyAddr), withServer(proxyAddr),
withPath("/ws"), withPath("/ws"),
withData("echo"), withData("echo"),
@ -339,9 +297,6 @@ func TestWebSocketRequestWithOrigin(t *testing.T) {
} }
func TestWebSocketRequestWithQueryParams(t *testing.T) { func TestWebSocketRequestWithQueryParams(t *testing.T) {
f, err := buildProxy(Bool(true), nil, http.DefaultTransport, nil)
require.NoError(t, err)
upgrader := gorillawebsocket.Upgrader{} upgrader := gorillawebsocket.Upgrader{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil) conn, err := upgrader.Upgrade(w, r, nil)
@ -363,7 +318,7 @@ func TestWebSocketRequestWithQueryParams(t *testing.T) {
})) }))
defer srv.Close() defer srv.Close()
proxy := createProxyWithForwarder(t, f, srv.URL) proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport)
defer proxy.Close() defer proxy.Close()
proxyAddr := proxy.Listener.Addr().String() proxyAddr := proxy.Listener.Addr().String()
@ -379,18 +334,14 @@ func TestWebSocketRequestWithQueryParams(t *testing.T) {
} }
func TestWebSocketRequestWithHeadersInResponseWriter(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 := http.NewServeMux()
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) { mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
conn.Close() conn.Close()
})) }))
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { srv := httptest.NewServer(mux)
mux.ServeHTTP(w, req)
}))
defer srv.Close() 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) { proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = parseURI(t, srv.URL) req.URL = parseURI(t, srv.URL)
w.Header().Set("HEADER-KEY", "HEADER-VALUE") w.Header().Set("HEADER-KEY", "HEADER-VALUE")
@ -403,6 +354,7 @@ func TestWebSocketRequestWithHeadersInResponseWriter(t *testing.T) {
headers := http.Header{} headers := http.Header{}
webSocketURL := "ws://" + serverAddr + "/ws" webSocketURL := "ws://" + serverAddr + "/ws"
headers.Add("Origin", webSocketURL) headers.Add("Origin", webSocketURL)
conn, resp, err := gorillawebsocket.DefaultDialer.Dial(webSocketURL, headers) conn, resp, err := gorillawebsocket.DefaultDialer.Dial(webSocketURL, headers)
require.NoError(t, err, "Error during Dial with response: %+v", err, resp) require.NoError(t, err, "Error during Dial with response: %+v", err, resp)
defer conn.Close() defer conn.Close()
@ -411,9 +363,6 @@ func TestWebSocketRequestWithHeadersInResponseWriter(t *testing.T) {
} }
func TestWebSocketRequestWithEncodedChar(t *testing.T) { func TestWebSocketRequestWithEncodedChar(t *testing.T) {
f, err := buildProxy(Bool(true), nil, http.DefaultTransport, nil)
require.NoError(t, err)
upgrader := gorillawebsocket.Upgrader{} upgrader := gorillawebsocket.Upgrader{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil) conn, err := upgrader.Upgrade(w, r, nil)
@ -435,7 +384,7 @@ func TestWebSocketRequestWithEncodedChar(t *testing.T) {
})) }))
defer srv.Close() defer srv.Close()
proxy := createProxyWithForwarder(t, f, srv.URL) proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport)
defer proxy.Close() defer proxy.Close()
proxyAddr := proxy.Listener.Addr().String() proxyAddr := proxy.Listener.Addr().String()
@ -451,18 +400,14 @@ func TestWebSocketRequestWithEncodedChar(t *testing.T) {
} }
func TestWebSocketUpgradeFailed(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 := http.NewServeMux()
mux.HandleFunc("/ws", func(w http.ResponseWriter, req *http.Request) { mux.HandleFunc("/ws", func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
}) })
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { srv := httptest.NewServer(mux)
mux.ServeHTTP(w, req)
}))
defer srv.Close() 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) { proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
path := req.URL.Path // keep the original path path := req.URL.Path // keep the original path
@ -501,9 +446,6 @@ func TestWebSocketUpgradeFailed(t *testing.T) {
} }
func TestForwardsWebsocketTraffic(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 := http.NewServeMux()
mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) { mux.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) {
_, err := conn.Write([]byte("ok")) _, err := conn.Write([]byte("ok"))
@ -512,12 +454,10 @@ func TestForwardsWebsocketTraffic(t *testing.T) {
err = conn.Close() err = conn.Close()
require.NoError(t, err) require.NoError(t, err)
})) }))
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { srv := httptest.NewServer(mux)
mux.ServeHTTP(w, req)
}))
defer srv.Close() defer srv.Close()
proxy := createProxyWithForwarder(t, f, srv.URL) proxy := createProxyWithForwarder(t, srv.URL, http.DefaultTransport)
defer proxy.Close() defer proxy.Close()
proxyAddr := proxy.Listener.Addr().String() proxyAddr := proxy.Listener.Addr().String()
@ -557,15 +497,12 @@ func TestWebSocketTransferTLSConfig(t *testing.T) {
srv := createTLSWebsocketServer() srv := createTLSWebsocketServer()
defer srv.Close() defer srv.Close()
forwarderWithoutTLSConfig, err := buildProxy(Bool(true), nil, http.DefaultTransport, nil) proxyWithoutTLSConfig := createProxyWithForwarder(t, srv.URL, http.DefaultTransport)
require.NoError(t, err)
proxyWithoutTLSConfig := createProxyWithForwarder(t, forwarderWithoutTLSConfig, srv.URL)
defer proxyWithoutTLSConfig.Close() defer proxyWithoutTLSConfig.Close()
proxyAddr := proxyWithoutTLSConfig.Listener.Addr().String() proxyAddr := proxyWithoutTLSConfig.Listener.Addr().String()
_, err = newWebsocketRequest( _, err := newWebsocketRequest(
withServer(proxyAddr), withServer(proxyAddr),
withPath("/ws"), withPath("/ws"),
withData("ok"), withData("ok"),
@ -576,10 +513,8 @@ func TestWebSocketTransferTLSConfig(t *testing.T) {
transport := &http.Transport{ transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 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() defer proxyWithTLSConfig.Close()
proxyAddr = proxyWithTLSConfig.Listener.Addr().String() proxyAddr = proxyWithTLSConfig.Listener.Addr().String()
@ -597,10 +532,7 @@ func TestWebSocketTransferTLSConfig(t *testing.T) {
defaultTransport := http.DefaultTransport.(*http.Transport).Clone() defaultTransport := http.DefaultTransport.(*http.Transport).Clone()
defaultTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} defaultTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
forwarderWithTLSConfigFromDefaultTransport, err := buildProxy(Bool(true), nil, defaultTransport, nil) proxyWithTLSConfigFromDefaultTransport := createProxyWithForwarder(t, srv.URL, defaultTransport)
require.NoError(t, err)
proxyWithTLSConfigFromDefaultTransport := createProxyWithForwarder(t, forwarderWithTLSConfigFromDefaultTransport, srv.URL)
defer proxyWithTLSConfig.Close() defer proxyWithTLSConfig.Close()
proxyAddr = proxyWithTLSConfigFromDefaultTransport.Listener.Addr().String() proxyAddr = proxyWithTLSConfigFromDefaultTransport.Listener.Addr().String()
@ -705,15 +637,19 @@ func parseURI(t *testing.T, uri string) *url.URL {
return out 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() 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 path := req.URL.Path // keep the original path
// Set new backend URL // Set new backend URL
req.URL = parseURI(t, url) req.URL = u
req.URL.Path = path req.URL.Path = path
proxy.ServeHTTP(w, req) proxy.ServeHTTP(w, req)
})) }))
t.Cleanup(srv.Close)
return srv
} }

View file

@ -4,36 +4,28 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"hash/fnv"
"math/rand" "math/rand"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
"reflect" "reflect"
"strings"
"time" "time"
"github.com/containous/alice"
"github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/config/runtime" "github.com/traefik/traefik/v2/pkg/config/runtime"
"github.com/traefik/traefik/v2/pkg/healthcheck" "github.com/traefik/traefik/v2/pkg/healthcheck"
"github.com/traefik/traefik/v2/pkg/log" "github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/metrics" "github.com/traefik/traefik/v2/pkg/metrics"
"github.com/traefik/traefik/v2/pkg/middlewares/accesslog" "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" 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/safe"
"github.com/traefik/traefik/v2/pkg/server/cookie" "github.com/traefik/traefik/v2/pkg/server/cookie"
"github.com/traefik/traefik/v2/pkg/server/provider" "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/failover"
"github.com/traefik/traefik/v2/pkg/server/service/loadbalancer/mirror" "github.com/traefik/traefik/v2/pkg/server/service/loadbalancer/mirror"
"github.com/traefik/traefik/v2/pkg/server/service/loadbalancer/wrr" "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 const defaultMaxBodySize int64 = -1
@ -43,6 +35,19 @@ type RoundTripperGetter interface {
Get(name string) (http.RoundTripper, error) 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. // NewManager creates a new Manager.
func NewManager(configs map[string]*runtime.ServiceInfo, metricsRegistry metrics.Registry, routinePool *safe.Pool, roundTripperManager RoundTripperGetter) *Manager { func NewManager(configs map[string]*runtime.ServiceInfo, metricsRegistry metrics.Registry, routinePool *safe.Pool, roundTripperManager RoundTripperGetter) *Manager {
return &Manager{ return &Manager{
@ -50,27 +55,13 @@ func NewManager(configs map[string]*runtime.ServiceInfo, metricsRegistry metrics
metricsRegistry: metricsRegistry, metricsRegistry: metricsRegistry,
bufferPool: newBufferPool(), bufferPool: newBufferPool(),
roundTripperManager: roundTripperManager, roundTripperManager: roundTripperManager,
balancers: make(map[string]healthcheck.Balancers), services: make(map[string]http.Handler),
configs: configs, configs: configs,
healthCheckers: make(map[string]*healthcheck.ServiceHealthChecker),
rand: rand.New(rand.NewSource(time.Now().UnixNano())), 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. // BuildHTTP Creates a http.Handler for a service configuration.
func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string) (http.Handler, error) { func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string) (http.Handler, error) {
ctx := log.With(rootCtx, log.Str(log.ServiceName, serviceName)) 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) serviceName = provider.GetQualifiedName(ctx, serviceName)
ctx = provider.AddInContext(ctx, serviceName) ctx = provider.AddInContext(ctx, serviceName)
handler, ok := m.services[serviceName]
if ok {
return handler, nil
}
conf, ok := m.configs[serviceName] conf, ok := m.configs[serviceName]
if !ok { if !ok {
return nil, fmt.Errorf("the service %q does not exist", serviceName) 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) value := reflect.ValueOf(*conf.Service)
var count int var count int
for i := 0; i < value.NumField(); i++ { for i := 0; i < value.NumField(); i++ {
@ -101,7 +101,7 @@ func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string) (http.H
switch { switch {
case conf.LoadBalancer != nil: case conf.LoadBalancer != nil:
var err error var err error
lb, err = m.getLoadBalancerServiceHandler(ctx, serviceName, conf.LoadBalancer) lb, err = m.getLoadBalancerServiceHandler(ctx, serviceName, conf)
if err != nil { if err != nil {
conf.AddError(err, true) conf.AddError(err, true)
return nil, err return nil, err
@ -133,6 +133,8 @@ func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string) (http.H
return nil, sErr return nil, sErr
} }
m.services[serviceName] = lb
return lb, nil 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) 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) { for _, service := range shuffle(config.Services, m.rand) {
serviceHandler, err := m.BuildHTTP(ctx, service.Name) serviceHandler, err := m.BuildHTTP(ctx, service.Name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
balancer.AddService(service.Name, serviceHandler, service.Weight) balancer.Add(service.Name, serviceHandler, service.Weight)
if config.HealthCheck == nil { if config.HealthCheck == nil {
continue continue
@ -245,216 +247,91 @@ func (m *Manager) getWRRServiceHandler(ctx context.Context, serviceName string,
return balancer, nil return balancer, nil
} }
func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName string, service *dynamic.ServersLoadBalancer) (http.Handler, error) { func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName string, info *runtime.ServiceInfo) (http.Handler, error) {
if service.PassHostHeader == nil { service := info.LoadBalancer
defaultPassHostHeader := true
service.PassHostHeader = &defaultPassHostHeader 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 { if len(service.ServersTransport) > 0 {
service.ServersTransport = provider.GetQualifiedName(ctx, service.ServersTransport) 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) roundTripper, err := m.roundTripperManager.Get(service.ServersTransport)
if err != nil { if err != nil {
return nil, err return nil, err
} }
fwd, err := buildProxy(service.PassHostHeader, service.ResponseForwarding, roundTripper, m.bufferPool) lb := wrr.New(service.Sticky, service.HealthCheck != nil)
if err != nil { healthCheckTargets := make(map[string]*url.URL)
return nil, err
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) { if service.HealthCheck != nil {
return accesslog.NewFieldHandler(next, accesslog.ServiceName, serviceName, accesslog.AddServiceFields), nil m.healthCheckers[serviceName] = healthcheck.NewServiceHealthChecker(
} ctx,
chain := alice.New() m.metricsRegistry,
if m.metricsRegistry != nil && m.metricsRegistry.IsSvcEnabled() { service.HealthCheck,
chain = chain.Append(metricsMiddle.WrapServiceHandler(ctx, m.metricsRegistry, serviceName)) lb,
info,
roundTripper,
healthCheckTargets,
)
} }
handler, err := chain.Append(alHandler).Then(pipelining.New(ctx, fwd, "pipelining")) return lb, nil
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
} }
// LaunchHealthCheck launches the health checks. // LaunchHealthCheck launches the health checks.
func (m *Manager) LaunchHealthCheck() { func (m *Manager) LaunchHealthCheck(ctx context.Context) {
backendConfigs := make(map[string]*healthcheck.BackendConfig) for serviceName, hc := range m.healthCheckers {
ctx = log.With(ctx, log.Str(log.ServiceName, serviceName))
for serviceName, balancers := range m.balancers { go hc.Launch(ctx)
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
} }
} }

View file

@ -15,14 +15,10 @@ import (
"github.com/traefik/traefik/v2/pkg/testhelpers" "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) { func TestGetLoadBalancer(t *testing.T) {
sm := Manager{} sm := Manager{
roundTripperManager: newRtMock(),
}
testCases := []struct { testCases := []struct {
desc string desc string
@ -67,7 +63,8 @@ func TestGetLoadBalancer(t *testing.T) {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() 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 { if test.expectError {
require.Error(t, err) require.Error(t, err)
assert.Nil(t, handler) assert.Nil(t, handler)
@ -129,6 +126,7 @@ func TestGetLoadBalancerServiceHandler(t *testing.T) {
desc: "Load balances between the two servers", desc: "Load balances between the two servers",
serviceName: "test", serviceName: "test",
service: &dynamic.ServersLoadBalancer{ service: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true),
Servers: []dynamic.Server{ Servers: []dynamic.Server{
{ {
URL: server1.URL, 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 { for _, test := range testCases {
test := test test := test
t.Run(test.desc, func(t *testing.T) { 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.NoError(t, err)
assert.NotNil(t, handler) assert.NotNil(t, handler)
@ -419,3 +390,21 @@ func TestMultipleTypeOnBuildHTTP(t *testing.T) {
_, err := manager.BuildHTTP(context.Background(), "test@file") _, 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") 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
}