Merge tag 'v1.4.0-rc5' into master

This commit is contained in:
Fernandez Ludovic 2017-10-10 17:17:44 +02:00
commit 9faae7387e
52 changed files with 930 additions and 416 deletions

View file

@ -1,5 +1,26 @@
# Change Log # Change Log
## [v1.4.0-rc5](https://github.com/containous/traefik/tree/v1.4.0-rc5) (2017-10-10)
[All Commits](https://github.com/containous/traefik/compare/v1.4.0-rc4...v1.4.0-rc5)
**Enhancements:**
- **[middleware]** Add trusted whitelist proxy protocol ([#2234](https://github.com/containous/traefik/pull/2234) by [emilevauge](https://github.com/emilevauge))
**Bug fixes:**
- **[consul,docker,ecs,k8s,marathon,rancher,sticky-session]** Stickiness cookie name ([#2232](https://github.com/containous/traefik/pull/2232) by [ldez](https://github.com/ldez))
- **[logs]** Fix flakiness in log rotation test ([#2213](https://github.com/containous/traefik/pull/2213) by [marco-jantke](https://github.com/marco-jantke))
- **[middleware]** Enable prefix matching within slash boundaries ([#2214](https://github.com/containous/traefik/pull/2214) by [marco-jantke](https://github.com/marco-jantke))
- **[sticky-session]** Sanitize cookie names. ([#2216](https://github.com/containous/traefik/pull/2216) by [timoreimann](https://github.com/timoreimann))
- Move http2 configure transport ([#2231](https://github.com/containous/traefik/pull/2231) by [Juliens](https://github.com/Juliens))
- Delay first version check ([#2215](https://github.com/containous/traefik/pull/2215) by [emilevauge](https://github.com/emilevauge))
**Documentation:**
- **[acme]** Fix grammar ([#2208](https://github.com/containous/traefik/pull/2208) by [mvasin](https://github.com/mvasin))
- **[docker,ecs,k8s,marathon,rancher]** Stickiness documentation ([#2238](https://github.com/containous/traefik/pull/2238) by [ldez](https://github.com/ldez))
- **[k8s]** Quote priority values in annotation examples. ([#2230](https://github.com/containous/traefik/pull/2230) by [timoreimann](https://github.com/timoreimann))
- **[k8s]** Remove pod from RBAC rules. ([#2229](https://github.com/containous/traefik/pull/2229) by [timoreimann](https://github.com/timoreimann))
- **[k8s]** Document ways to partition Ingresses in the k8s guide. ([#2223](https://github.com/containous/traefik/pull/2223) by [timoreimann](https://github.com/timoreimann))
## [v1.4.0-rc4](https://github.com/containous/traefik/tree/v1.4.0-rc4) (2017-10-02) ## [v1.4.0-rc4](https://github.com/containous/traefik/tree/v1.4.0-rc4) (2017-10-02)
[All Commits](https://github.com/containous/traefik/compare/v1.4.0-rc3...v1.4.0-rc4) [All Commits](https://github.com/containous/traefik/compare/v1.4.0-rc3...v1.4.0-rc4)

View file

@ -97,7 +97,7 @@ dist:
run-dev: run-dev:
go generate go generate
go build go build ./cmd/traefik
./traefik ./traefik
generate-webui: build-webui generate-webui: build-webui

View file

@ -84,7 +84,9 @@ func TestDo_globalConfiguration(t *testing.T) {
}, },
WhitelistSourceRange: []string{"foo WhitelistSourceRange 1", "foo WhitelistSourceRange 2", "foo WhitelistSourceRange 3"}, WhitelistSourceRange: []string{"foo WhitelistSourceRange 1", "foo WhitelistSourceRange 2", "foo WhitelistSourceRange 3"},
Compress: true, Compress: true,
ProxyProtocol: true, ProxyProtocol: &configuration.ProxyProtocol{
TrustedIPs: []string{"127.0.0.1/32", "192.168.0.1"},
},
}, },
"fii": { "fii": {
Network: "fii Network", Network: "fii Network",
@ -125,7 +127,9 @@ func TestDo_globalConfiguration(t *testing.T) {
}, },
WhitelistSourceRange: []string{"fii WhitelistSourceRange 1", "fii WhitelistSourceRange 2", "fii WhitelistSourceRange 3"}, WhitelistSourceRange: []string{"fii WhitelistSourceRange 1", "fii WhitelistSourceRange 2", "fii WhitelistSourceRange 3"},
Compress: true, Compress: true,
ProxyProtocol: true, ProxyProtocol: &configuration.ProxyProtocol{
TrustedIPs: []string{"127.0.0.1/32", "192.168.0.1"},
},
}, },
} }
config.Cluster = &types.Cluster{ config.Cluster = &types.Cluster{

View file

@ -349,6 +349,7 @@ func CreateKvSource(traefikConfiguration *TraefikConfiguration) (*staert.KvSourc
func checkNewVersion() { func checkNewVersion() {
ticker := time.NewTicker(24 * time.Hour) ticker := time.NewTicker(24 * time.Hour)
safe.Go(func() { safe.Go(func() {
time.Sleep(10 * time.Minute)
version.CheckNewVersion() version.CheckNewVersion()
for { for {
select { select {

View file

@ -284,7 +284,14 @@ func (ep *EntryPoints) Set(value string) error {
} }
compress := toBool(result, "Compress") compress := toBool(result, "Compress")
proxyProtocol := toBool(result, "ProxyProtocol")
var proxyProtocol *ProxyProtocol
if len(result["ProxyProtocol"]) > 0 {
trustedIPs := strings.Split(result["ProxyProtocol"], ",")
proxyProtocol = &ProxyProtocol{
TrustedIPs: trustedIPs,
}
}
(*ep)[result["Name"]] = &EntryPoint{ (*ep)[result["Name"]] = &EntryPoint{
Address: result["Address"], Address: result["Address"],
@ -299,7 +306,7 @@ func (ep *EntryPoints) Set(value string) error {
} }
func parseEntryPointsConfiguration(value string) (map[string]string, error) { func parseEntryPointsConfiguration(value string) (map[string]string, error) {
regex := regexp.MustCompile(`(?:Name:(?P<Name>\S*))\s*(?:Address:(?P<Address>\S*))?\s*(?:TLS:(?P<TLS>\S*))?\s*(?P<TLSACME>TLS)?\s*(?:CA:(?P<CA>\S*))?\s*(?:Redirect\.EntryPoint:(?P<RedirectEntryPoint>\S*))?\s*(?:Redirect\.Regex:(?P<RedirectRegex>\S*))?\s*(?:Redirect\.Replacement:(?P<RedirectReplacement>\S*))?\s*(?:Compress:(?P<Compress>\S*))?\s*(?:WhiteListSourceRange:(?P<WhiteListSourceRange>\S*))?\s*(?:ProxyProtocol:(?P<ProxyProtocol>\S*))?`) regex := regexp.MustCompile(`(?:Name:(?P<Name>\S*))\s*(?:Address:(?P<Address>\S*))?\s*(?:TLS:(?P<TLS>\S*))?\s*(?P<TLSACME>TLS)?\s*(?:CA:(?P<CA>\S*))?\s*(?:Redirect\.EntryPoint:(?P<RedirectEntryPoint>\S*))?\s*(?:Redirect\.Regex:(?P<RedirectRegex>\S*))?\s*(?:Redirect\.Replacement:(?P<RedirectReplacement>\S*))?\s*(?:Compress:(?P<Compress>\S*))?\s*(?:WhiteListSourceRange:(?P<WhiteListSourceRange>\S*))?\s*(?:ProxyProtocol\.TrustedIPs:(?P<ProxyProtocol>\S*))?`)
match := regex.FindAllStringSubmatch(value, -1) match := regex.FindAllStringSubmatch(value, -1)
if match == nil { if match == nil {
return nil, fmt.Errorf("bad EntryPoints format: %s", value) return nil, fmt.Errorf("bad EntryPoints format: %s", value)
@ -346,8 +353,8 @@ type EntryPoint struct {
Redirect *Redirect `export:"true"` Redirect *Redirect `export:"true"`
Auth *types.Auth `export:"true"` Auth *types.Auth `export:"true"`
WhitelistSourceRange []string WhitelistSourceRange []string
Compress bool `export:"true"` Compress bool `export:"true"`
ProxyProtocol bool `export:"true"` ProxyProtocol *ProxyProtocol `export:"true"`
} }
// Redirect configures a redirection of an entry point to another, or to an URL // Redirect configures a redirection of an entry point to another, or to an URL
@ -497,6 +504,11 @@ type ForwardingTimeouts struct {
ResponseHeaderTimeout flaeg.Duration `description:"The amount of time to wait for a server's response headers after fully writing the request (including its body, if any). If zero, no timeout exists" export:"true"` ResponseHeaderTimeout flaeg.Duration `description:"The amount of time to wait for a server's response headers after fully writing the request (including its body, if any). If zero, no timeout exists" export:"true"`
} }
// ProxyProtocol contains Proxy-Protocol configuration
type ProxyProtocol struct {
TrustedIPs []string
}
// LifeCycle contains configurations relevant to the lifecycle (such as the // LifeCycle contains configurations relevant to the lifecycle (such as the
// shutdown phase) of Traefik. // shutdown phase) of Traefik.
type LifeCycle struct { type LifeCycle struct {

View file

@ -18,7 +18,7 @@ func Test_parseEntryPointsConfiguration(t *testing.T) {
}{ }{
{ {
name: "all parameters", name: "all parameters",
value: "Name:foo Address:bar TLS:goo TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:WhiteListSourceRange ProxyProtocol:true", value: "Name:foo Address:bar TLS:goo TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:WhiteListSourceRange ProxyProtocol.TrustedIPs:192.168.0.1",
expectedResult: map[string]string{ expectedResult: map[string]string{
"Name": "foo", "Name": "foo",
"Address": "bar", "Address": "bar",
@ -29,18 +29,10 @@ func Test_parseEntryPointsConfiguration(t *testing.T) {
"RedirectRegex": "RedirectRegex", "RedirectRegex": "RedirectRegex",
"RedirectReplacement": "RedirectReplacement", "RedirectReplacement": "RedirectReplacement",
"WhiteListSourceRange": "WhiteListSourceRange", "WhiteListSourceRange": "WhiteListSourceRange",
"ProxyProtocol": "true", "ProxyProtocol": "192.168.0.1",
"Compress": "true", "Compress": "true",
}, },
}, },
{
name: "proxy protocol on",
value: "Name:foo ProxyProtocol:on",
expectedResult: map[string]string{
"Name": "foo",
"ProxyProtocol": "on",
},
},
{ {
name: "compress on", name: "compress on",
value: "Name:foo Compress:on", value: "Name:foo Compress:on",
@ -144,7 +136,7 @@ func TestEntryPoints_Set(t *testing.T) {
}{ }{
{ {
name: "all parameters", name: "all parameters",
expression: "Name:foo Address:bar TLS:goo,gii TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:Range ProxyProtocol:true", expression: "Name:foo Address:bar TLS:goo,gii TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:Range ProxyProtocol.TrustedIPs:192.168.0.1",
expectedEntryPointName: "foo", expectedEntryPointName: "foo",
expectedEntryPoint: &EntryPoint{ expectedEntryPoint: &EntryPoint{
Address: "bar", Address: "bar",
@ -153,8 +145,10 @@ func TestEntryPoints_Set(t *testing.T) {
Regex: "RedirectRegex", Regex: "RedirectRegex",
Replacement: "RedirectReplacement", Replacement: "RedirectReplacement",
}, },
Compress: true, Compress: true,
ProxyProtocol: true, ProxyProtocol: &ProxyProtocol{
TrustedIPs: []string{"192.168.0.1"},
},
WhitelistSourceRange: []string{"Range"}, WhitelistSourceRange: []string{"Range"},
TLS: &TLS{ TLS: &TLS{
ClientCAFiles: []string{"car"}, ClientCAFiles: []string{"car"},

View file

@ -375,12 +375,33 @@ For example:
- Another possible value for `extractorfunc` is `client.ip` which will categorize requests based on client source ip. - Another possible value for `extractorfunc` is `client.ip` which will categorize requests based on client source ip.
- Lastly `extractorfunc` can take the value of `request.header.ANY_HEADER` which will categorize requests based on `ANY_HEADER` that you provide. - Lastly `extractorfunc` can take the value of `request.header.ANY_HEADER` which will categorize requests based on `ANY_HEADER` that you provide.
### Sticky sessions
Sticky sessions are supported with both load balancers. Sticky sessions are supported with both load balancers.
When sticky sessions are enabled, a cookie called `_TRAEFIK_BACKEND` is set on the initial request. When sticky sessions are enabled, a cookie is set on the initial request.
The default cookie name is an abbreviation of a sha1 (ex: `_1d52e`).
On subsequent requests, the client will be directed to the backend stored in the cookie if it is still healthy. On subsequent requests, the client will be directed to the backend stored in the cookie if it is still healthy.
If not, a new backend will be assigned. If not, a new backend will be assigned.
For example: To activate sticky session:
```toml
[backends]
[backends.backend1]
[backends.backend1.loadbalancer.stickiness]
```
To customize the cookie name:
```toml
[backends]
[backends.backend1]
[backends.backend1.loadbalancer.stickiness]
cookieName = "my_cookie"
```
The deprecated way:
```toml ```toml
[backends] [backends]
[backends.backend1] [backends.backend1]
@ -388,6 +409,8 @@ For example:
sticky = true sticky = true
``` ```
### Health Check
A health check can be configured in order to remove a backend from LB rotation as long as it keeps returning HTTP status codes other than `200 OK` to HTTP GET requests periodically carried out by Traefik. A health check can be configured in order to remove a backend from LB rotation as long as it keeps returning HTTP status codes other than `200 OK` to HTTP GET requests periodically carried out by Traefik.
The check is defined by a pathappended to the backend URL and an interval (given in a format understood by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration)) specifying how often the health check should be executed (the default being 30 seconds). The check is defined by a pathappended to the backend URL and an interval (given in a format understood by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration)) specifying how often the health check should be executed (the default being 30 seconds).
Each backend must respond to the health check within 5 seconds. Each backend must respond to the health check within 5 seconds.

View file

@ -177,7 +177,7 @@ Enable on demand certificate.
This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate.
!!! warning !!! warning
TLS handshakes will be slow when requesting a hostname certificate for the first time, this can leads to DoS attacks. TLS handshakes will be slow when requesting a hostname certificate for the first time, this can lead to DoS attacks.
!!! warning !!! warning
Take note that Let's Encrypt have [rate limiting](https://letsencrypt.org/docs/rate-limits) Take note that Let's Encrypt have [rate limiting](https://letsencrypt.org/docs/rate-limits)

View file

@ -117,17 +117,20 @@ To enable constraints see [backend-specific constraints section](/configuration/
Additional settings can be defined using Consul Catalog tags. Additional settings can be defined using Consul Catalog tags.
| Tag | Description | | Tag | Description |
|---------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |-----------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `traefik.enable=false` | Disable this container in Træfik | | `traefik.enable=false` | Disable this container in Træfik |
| `traefik.protocol=https` | Override the default `http` protocol | | `traefik.protocol=https` | Override the default `http` protocol |
| `traefik.backend.weight=10` | Assign this weight to the container | | `traefik.backend.weight=10` | Assign this weight to the container |
| `traefik.backend.circuitbreaker=EXPR` | Create a [circuit breaker](/basics/#backends) to be used against the backend, ex: `NetworkErrorRatio() > 0.` | | `traefik.backend.circuitbreaker=EXPR` | Create a [circuit breaker](/basics/#backends) to be used against the backend, ex: `NetworkErrorRatio() > 0.` |
| `traefik.backend.loadbalancer=drr` | Override the default load balancing mode | | `traefik.backend.maxconn.amount=10` | Set a maximum number of connections to the backend. Must be used in conjunction with the below label to take effect. |
| `traefik.backend.maxconn.amount=10` | Set a maximum number of connections to the backend. Must be used in conjunction with the below label to take effect. | | `traefik.backend.maxconn.extractorfunc=client.ip` | Set the function to be used against the request to determine what to limit maximum connections to the backend by. Must be used in conjunction with the above label to take effect. |
| `traefik.backend.maxconn.extractorfunc=client.ip` | Set the function to be used against the request to determine what to limit maximum connections to the backend by. Must be used in conjunction with the above label to take effect. | | `traefik.frontend.rule=Host:test.traefik.io` | Override the default frontend rule (Default: `Host:{{.ServiceName}}.{{.Domain}}`). |
| `traefik.frontend.rule=Host:test.traefik.io` | Override the default frontend rule (Default: `Host:{{.ServiceName}}.{{.Domain}}`). | | `traefik.frontend.passHostHeader=true` | Forward client `Host` header to the backend. |
| `traefik.frontend.passHostHeader=true` | Forward client `Host` header to the backend. | | `traefik.frontend.priority=10` | Override default frontend priority |
| `traefik.frontend.priority=10` | Override default frontend priority | | `traefik.frontend.entryPoints=http,https` | Assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. |
| `traefik.frontend.entryPoints=http,https` | Assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. | | `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash` |
| `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash` | | `traefik.backend.loadbalancer=drr` | override the default `wrr` load balancer algorithm |
| `traefik.backend.loadbalancer.stickiness=true` | enable backend sticky sessions |
| `traefik.backend.loadbalancer.stickiness.cookieName=NAME` | Manually set the cookie name for sticky sessions |
| `traefik.backend.loadbalancer.sticky=true` | enable backend sticky sessions (DEPRECATED) |

View file

@ -148,26 +148,28 @@ To enable constraints see [backend-specific constraints section](/configuration/
Labels can be used on containers to override default behaviour. Labels can be used on containers to override default behaviour.
| Label | Description | | Label | Description |
|---------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |-----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `traefik.backend=foo` | Give the name `foo` to the generated backend for this container. | | `traefik.backend=foo` | Give the name `foo` to the generated backend for this container. |
| `traefik.backend.maxconn.amount=10` | Set a maximum number of connections to the backend. Must be used in conjunction with the below label to take effect. | | `traefik.backend.maxconn.amount=10` | Set a maximum number of connections to the backend. Must be used in conjunction with the below label to take effect. |
| `traefik.backend.maxconn.extractorfunc=client.ip` | Set the function to be used against the request to determine what to limit maximum connections to the backend by. Must be used in conjunction with the above label to take effect. | | `traefik.backend.maxconn.extractorfunc=client.ip` | Set the function to be used against the request to determine what to limit maximum connections to the backend by. Must be used in conjunction with the above label to take effect. |
| `traefik.backend.loadbalancer.method=drr` | Override the default `wrr` load balancer algorithm | | `traefik.backend.loadbalancer.method=drr` | Override the default `wrr` load balancer algorithm |
| `traefik.backend.loadbalancer.sticky=true` | Enable backend sticky sessions | | `traefik.backend.loadbalancer.stickiness=true` | Enable backend sticky sessions |
| `traefik.backend.loadbalancer.swarm=true` | Use Swarm's inbuilt load balancer (only relevant under Swarm Mode). | | `traefik.backend.loadbalancer.stickiness.cookieName=NAME` | Manually set the cookie name for sticky sessions |
| `traefik.backend.circuitbreaker.expression=EXPR` | Create a [circuit breaker](/basics/#backends) to be used against the backend | | `traefik.backend.loadbalancer.sticky=true` | Enable backend sticky sessions (DEPRECATED) |
| `traefik.port=80` | Register this port. Useful when the container exposes multiples ports. | | `traefik.backend.loadbalancer.swarm=true` | Use Swarm's inbuilt load balancer (only relevant under Swarm Mode). |
| `traefik.protocol=https` | Override the default `http` protocol | | `traefik.backend.circuitbreaker.expression=EXPR` | Create a [circuit breaker](/basics/#backends) to be used against the backend |
| `traefik.weight=10` | Assign this weight to the container | | `traefik.port=80` | Register this port. Useful when the container exposes multiples ports. |
| `traefik.enable=false` | Disable this container in Træfik | | `traefik.protocol=https` | Override the default `http` protocol |
| `traefik.frontend.rule=EXPR` | Override the default frontend rule. Default: `Host:{containerName}.{domain}` or `Host:{service}.{project_name}.{domain}` if you are using `docker-compose`. | | `traefik.weight=10` | Assign this weight to the container |
| `traefik.frontend.passHostHeader=true` | Forward client `Host` header to the backend. | | `traefik.enable=false` | Disable this container in Træfik |
| `traefik.frontend.priority=10` | Override default frontend priority | | `traefik.frontend.rule=EXPR` | Override the default frontend rule. Default: `Host:{containerName}.{domain}` or `Host:{service}.{project_name}.{domain}` if you are using `docker-compose`. |
| `traefik.frontend.entryPoints=http,https` | Assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints` | | `traefik.frontend.passHostHeader=true` | Forward client `Host` header to the backend. |
| `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash` | | `traefik.frontend.priority=10` | Override default frontend priority |
| `traefik.frontend.whitelistSourceRange:RANGE` | List of IP-Ranges which are allowed to access. An unset or empty list allows all Source-IPs to access. If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. | | `traefik.frontend.entryPoints=http,https` | Assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints` |
| `traefik.docker.network` | Set the docker network to use for connections to this container. If a container is linked to several networks, be sure to set the proper network name (you can check with `docker inspect <container_id>`) otherwise it will randomly pick one (depending on how docker is returning them). For instance when deploying docker `stack` from compose files, the compose defined networks will be prefixed with the `stack` name. | | `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash` |
| `traefik.frontend.whitelistSourceRange:RANGE` | List of IP-Ranges which are allowed to access. An unset or empty list allows all Source-IPs to access. If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. |
| `traefik.docker.network` | Set the docker network to use for connections to this container. If a container is linked to several networks, be sure to set the proper network name (you can check with `docker inspect <container_id>`) otherwise it will randomly pick one (depending on how docker is returning them). For instance when deploying docker `stack` from compose files, the compose defined networks will be prefixed with the `stack` name. |
### On Service ### On Service

View file

@ -124,15 +124,17 @@ Træfik needs the following policy to read ECS information:
Labels can be used on task containers to override default behaviour: Labels can be used on task containers to override default behaviour:
| Label | Description | | Label | Description |
|---------------------------------------------------|------------------------------------------------------------------------------------------| |-----------------------------------------------------------|------------------------------------------------------------------------------------------|
| `traefik.protocol=https` | override the default `http` protocol | | `traefik.protocol=https` | override the default `http` protocol |
| `traefik.weight=10` | assign this weight to the container | | `traefik.weight=10` | assign this weight to the container |
| `traefik.enable=false` | disable this container in Træfik | | `traefik.enable=false` | disable this container in Træfik |
| `traefik.backend.loadbalancer.method=drr` | override the default `wrr` load balancer algorithm | | `traefik.backend.loadbalancer.method=drr` | override the default `wrr` load balancer algorithm |
| `traefik.backend.loadbalancer.sticky=true` | enable backend sticky sessions | | `traefik.backend.loadbalancer.stickiness=true` | enable backend sticky sessions |
| `traefik.frontend.rule=Host:test.traefik.io` | override the default frontend rule (Default: `Host:{containerName}.{domain}`). | | `traefik.backend.loadbalancer.stickiness.cookieName=NAME` | Manually set the cookie name for sticky sessions |
| `traefik.frontend.passHostHeader=true` | forward client `Host` header to the backend. | | `traefik.backend.loadbalancer.sticky=true` | enable backend sticky sessions (DEPRECATED) |
| `traefik.frontend.priority=10` | override default frontend priority | | `traefik.frontend.rule=Host:test.traefik.io` | override the default frontend rule (Default: `Host:{containerName}.{domain}`). |
| `traefik.frontend.entryPoints=http,https` | assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. | | `traefik.frontend.passHostHeader=true` | forward client `Host` header to the backend. |
| `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash` | | `traefik.frontend.priority=10` | override default frontend priority |
| `traefik.frontend.entryPoints=http,https` | assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. |
| `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash` |

View file

@ -86,15 +86,19 @@ Annotations can be used on containers to override default behaviour for the whol
- `traefik.frontend.rule.type: PathPrefixStrip` - `traefik.frontend.rule.type: PathPrefixStrip`
Override the default frontend rule type. Default: `PathPrefix`. Override the default frontend rule type. Default: `PathPrefix`.
- `traefik.frontend.priority: 3` - `traefik.frontend.priority: "3"`
Override the default frontend rule priority. Override the default frontend rule priority.
Annotations can be used on the Kubernetes service to override default behaviour: Annotations can be used on the Kubernetes service to override default behaviour:
- `traefik.backend.loadbalancer.method=drr` - `traefik.backend.loadbalancer.method=drr`
Override the default `wrr` load balancer algorithm Override the default `wrr` load balancer algorithm
- `traefik.backend.loadbalancer.sticky=true` - `traefik.backend.loadbalancer.stickiness=true`
Enable backend sticky sessions Enable backend sticky sessions
- `traefik.backend.loadbalancer.stickiness.cookieName=NAME`
Manually set the cookie name for sticky sessions
- `traefik.backend.loadbalancer.sticky=true`
Enable backend sticky sessions (DEPRECATED)
You can find here an example [ingress](https://raw.githubusercontent.com/containous/traefik/master/examples/k8s/cheese-ingress.yaml) and [replication controller](https://raw.githubusercontent.com/containous/traefik/master/examples/k8s/traefik.yaml). You can find here an example [ingress](https://raw.githubusercontent.com/containous/traefik/master/examples/k8s/cheese-ingress.yaml) and [replication controller](https://raw.githubusercontent.com/containous/traefik/master/examples/k8s/traefik.yaml).

View file

@ -153,7 +153,9 @@ Labels can be used on containers to override default behaviour:
| `traefik.backend.maxconn.amount=10` | set a maximum number of connections to the backend. Must be used in conjunction with the below label to take effect. | | `traefik.backend.maxconn.amount=10` | set a maximum number of connections to the backend. Must be used in conjunction with the below label to take effect. |
| `traefik.backend.maxconn.extractorfunc=client.ip` | set the function to be used against the request to determine what to limit maximum connections to the backend by. Must be used in conjunction with the above label to take effect. | | `traefik.backend.maxconn.extractorfunc=client.ip` | set the function to be used against the request to determine what to limit maximum connections to the backend by. Must be used in conjunction with the above label to take effect. |
| `traefik.backend.loadbalancer.method=drr` | override the default `wrr` load balancer algorithm | | `traefik.backend.loadbalancer.method=drr` | override the default `wrr` load balancer algorithm |
| `traefik.backend.loadbalancer.sticky=true` | enable backend sticky sessions | | `traefik.backend.loadbalancer.sticky=true` | enable backend sticky sessions (DEPRECATED) |
| `traefik.backend.loadbalancer.stickiness=true` | enable backend sticky sessions |
| `traefik.backend.loadbalancer.stickiness.cookieName=NAME` | Manually set the cookie name for sticky sessions |
| `traefik.backend.circuitbreaker.expression=NetworkErrorRatio() > 0.5` | create a [circuit breaker](/basics/#backends) to be used against the backend | | `traefik.backend.circuitbreaker.expression=NetworkErrorRatio() > 0.5` | create a [circuit breaker](/basics/#backends) to be used against the backend |
| `traefik.backend.healthcheck.path=/health` | set the Traefik health check path [default: no health checks] | | `traefik.backend.healthcheck.path=/health` | set the Traefik health check path [default: no health checks] |
| `traefik.backend.healthcheck.interval=5s` | sets a custom health check interval in Go-parseable (`time.ParseDuration`) format [default: 30s] | | `traefik.backend.healthcheck.interval=5s` | sets a custom health check interval in Go-parseable (`time.ParseDuration`) format [default: 30s] |

View file

@ -114,13 +114,18 @@ secretKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Labels can be used on task containers to override default behaviour: Labels can be used on task containers to override default behaviour:
| Label | Description | | Label | Description |
|----------------------------------------------|------------------------------------------------------------------------------------------| |-----------------------------------------------------------------------|------------------------------------------------------------------------------------------|
| `traefik.protocol=https` | override the default `http` protocol | | `traefik.protocol=https` | Override the default `http` protocol |
| `traefik.weight=10` | assign this weight to the container | | `traefik.weight=10` | Assign this weight to the container |
| `traefik.enable=false` | disable this container in Træfik | | `traefik.enable=false` | Disable this container in Træfik |
| `traefik.frontend.rule=Host:test.traefik.io` | override the default frontend rule (Default: `Host:{containerName}.{domain}`). | | `traefik.frontend.rule=Host:test.traefik.io` | Override the default frontend rule (Default: `Host:{containerName}.{domain}`). |
| `traefik.frontend.passHostHeader=true` | forward client `Host` header to the backend. | | `traefik.frontend.passHostHeader=true` | Forward client `Host` header to the backend. |
| `traefik.frontend.priority=10` | override default frontend priority | | `traefik.frontend.priority=10` | Override default frontend priority |
| `traefik.frontend.entryPoints=http,https` | assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. | | `traefik.frontend.entryPoints=http,https` | Assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. |
| `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash`. | | `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash`. |
| `traefik.backend.circuitbreaker.expression=NetworkErrorRatio() > 0.5` | Create a [circuit breaker](/basics/#backends) to be used against the backend |
| `traefik.backend.loadbalancer.method=drr` | Override the default `wrr` load balancer algorithm |
| `traefik.backend.loadbalancer.stickiness=true` | Enable backend sticky sessions |
| `traefik.backend.loadbalancer.stickiness.cookieName=NAME` | Manually set the cookie name for sticky sessions |
| `traefik.backend.loadbalancer.sticky=true` | Enable backend sticky sessions (DEPRECATED) |

View file

@ -185,16 +185,20 @@ To enable IP whitelisting at the entrypoint level.
[entryPoints] [entryPoints]
[entryPoints.http] [entryPoints.http]
address = ":80" address = ":80"
whiteListSourceRange = ["127.0.0.1/32"] whiteListSourceRange = ["127.0.0.1/32", "192.168.1.7"]
``` ```
## ProxyProtocol Support ## ProxyProtocol Support
To enable [ProxyProtocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) support. To enable [ProxyProtocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) support.
Only IPs in `trustedIPs` will lead to remote client address replacement: you should declare your load-balancer IP or CIDR range here.
```toml ```toml
[entryPoints] [entryPoints]
[entryPoints.http] [entryPoints.http]
address = ":80" address = ":80"
proxyprotocol = true [entryPoints.http.proxyProtocol]
trustedIPs = ["127.0.0.1/32", "192.168.1.7"]
``` ```
²

View file

@ -74,7 +74,7 @@ Also, we're making sure the container is automatically restarted by the Docker e
We're publishing the default HTTP ports `80` and `443` on the host, and making sure the container is placed within the `web` network we've created earlier on. We're publishing the default HTTP ports `80` and `443` on the host, and making sure the container is placed within the `web` network we've created earlier on.
Finally, we're giving this container a static name called `traefik`. Finally, we're giving this container a static name called `traefik`.
Let's take a look at a simply `traefik.toml` configuration as well before we'll create the Traefik container: Let's take a look at a simple `traefik.toml` configuration as well before we'll create the Traefik container:
```toml ```toml
debug = false debug = false

View file

@ -3,7 +3,7 @@
This section explains how to use Traefik as reverse proxy for gRPC application with self-signed certificates. This section explains how to use Traefik as reverse proxy for gRPC application with self-signed certificates.
!!! warning !!! warning
As gRPC needs HTTP2, we need valid HTTPS certificates on both gRPC Server and Træfik. As gRPC needs HTTP2, we need HTTPS certificates on both gRPC Server and Træfik.
<p align="center"> <p align="center">
<img src="/img/grpc.svg" alt="gRPC architecture" title="gRPC architecture" /> <img src="/img/grpc.svg" alt="gRPC architecture" title="gRPC architecture" />
@ -76,9 +76,12 @@ RootCAs = [ "./backend.cert" ]
rule = "Host:frontend.local" rule = "Host:frontend.local"
``` ```
!!! warning
With some backends, the server URLs use the IP, so you may need to configure `InsecureSkipVerify` instead of the `RootCAS` to activate HTTPS without hostname verification.
## Conclusion ## Conclusion
We don't need specific configuration to use gRPC in Træfik, we just need to be careful that all the exchanges (between client and Træfik, and between Træfik and backend) are valid HTTPS communications (without `InsecureSkipVerify` enabled) because gRPC use HTTP2. We don't need specific configuration to use gRPC in Træfik, we just need to be careful that all the exchanges (between client and Træfik, and between Træfik and backend) are HTTPS communications because gRPC uses HTTP2.
## A gRPC example in go ## A gRPC example in go

View file

@ -33,7 +33,6 @@ rules:
- apiGroups: - apiGroups:
- "" - ""
resources: resources:
- pods
- services - services
- endpoints - endpoints
- secrets - secrets
@ -594,7 +593,7 @@ kind: Ingress
metadata: metadata:
name: wildcard-cheeses name: wildcard-cheeses
annotations: annotations:
traefik.frontend.priority: 1 traefik.frontend.priority: "1"
spec: spec:
rules: rules:
- host: *.minikube - host: *.minikube
@ -609,7 +608,7 @@ kind: Ingress
metadata: metadata:
name: specific-cheeses name: specific-cheeses
annotations: annotations:
traefik.frontend.priority: 2 traefik.frontend.priority: "2"
spec: spec:
rules: rules:
- host: specific.minikube - host: specific.minikube
@ -621,6 +620,7 @@ spec:
servicePort: http servicePort: http
``` ```
Note that priority values must be quoted to avoid them being interpreted as numbers (which are illegal for annotations).
## Forwarding to ExternalNames ## Forwarding to ExternalNames
@ -690,13 +690,23 @@ If you were to visit `example.com/static` the request would then be passed onto
So you could set `disablePassHostHeaders` to `true` in your toml file and then enable passing So you could set `disablePassHostHeaders` to `true` in your toml file and then enable passing
the host header per ingress if you wanted. the host header per ingress if you wanted.
## Excluding an ingress from Træfik ## Partitioning the Ingress object space
You can control which ingress Træfik cares about by using the `kubernetes.io/ingress.class` annotation. By default, Træfik processes every Ingress objects it observes. At times, however, it may be desirable to ignore certain objects. The following sub-sections describe common use cases and how they can be handled with Træfik.
By default if the annotation is not set at all Træfik will include the ingress. ### Between Træfik and other Ingress controller implementations
If the annotation is set to anything other than traefik or a blank string Træfik will ignore it.
Sometimes Træfik runs along other Ingress controller implementations. One such example is when both Træfik and a cloud provider Ingress controller are active.
The `kubernetes.io/ingress.class` annotation can be attached to any Ingress object in order to control whether Træfik should handle it.
If the annotation is missing, contains an empty value, or the value `traefik`, then the Træfik controller will take responsibility and process the associated Ingress object. If the annotation contains any other value (usually the name of a different Ingress controller), Træfik will ignore the object.
### Between multiple Træfik Deployments
Sometimes multiple Træfik Deployments are supposed to run concurrently. For instance, it is conceivable to have one Deployment deal with internal and another one with external traffic.
For such cases, it is advisable to classify Ingress objects through a label and configure the `labelSelector` option per each Træfik Deployment accordingly. To stick with the internal/external example above, all Ingress objects meant for internal traffic could receive a `traffic-type: internal` label while objects designated for external traffic receive a `traffic-type: external` label. The label selectors on the Træfik Deployments would then be `traffic-type=internal` and `traffic-type=external`, respectively.
## Production advice ## Production advice

View file

@ -126,7 +126,7 @@ docker-machine ssh manager "docker service create \
``` ```
!!! note !!! note
We set `whoami1` to use sticky sessions (`--label traefik.backend.loadbalancer.sticky=true`). We set `whoami1` to use sticky sessions (`--label traefik.backend.loadbalancer.stickiness=true`).
We'll demonstrate that later. We'll demonstrate that later.
!!! note !!! note
@ -307,7 +307,7 @@ cat ./cookies.txt
whoami1.traefik FALSE / FALSE 0 _TRAEFIK_BACKEND http://10.0.0.15:80 whoami1.traefik FALSE / FALSE 0 _TRAEFIK_BACKEND http://10.0.0.15:80
``` ```
If you load the cookies file (`-b cookies.txt`) for the next request, you will see that stickyness is maintained: If you load the cookies file (`-b cookies.txt`) for the next request, you will see that stickiness is maintained:
```shell ```shell
curl -b cookies.txt -H Host:whoami1.traefik http://$(docker-machine ip manager) curl -b cookies.txt -H Host:whoami1.traefik http://$(docker-machine ip manager)

View file

@ -7,7 +7,6 @@ rules:
- apiGroups: - apiGroups:
- "" - ""
resources: resources:
- pods
- services - services
- endpoints - endpoints
- secrets - secrets

View file

@ -0,0 +1,29 @@
defaultEntryPoints = ["https"]
InsecureSkipVerify = true
[entryPoints]
[entryPoints.https]
address = ":4443"
[entryPoints.https.tls]
[[entryPoints.https.tls.certificates]]
CertFile = """{{ .CertContent }}"""
KeyFile = """{{ .KeyContent }}"""
[web]
address = ":8080"
[file]
[backends]
[backends.backend1]
[backends.backend1.servers.server1]
url = "https://127.0.0.1:{{ .GRPCServerPort }}"
[frontends]
[frontends.frontend1]
backend = "backend1"
[frontends.frontend1.routes.test_1]
rule = "Host:127.0.0.1"

View file

@ -113,3 +113,45 @@ func (s *GRPCSuite) TestGRPC(c *check.C) {
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(response, check.Equals, "Hello World") c.Assert(response, check.Equals, "Hello World")
} }
func (s *GRPCSuite) TestGRPCInsecure(c *check.C) {
lis, err := net.Listen("tcp", ":0")
_, port, err := net.SplitHostPort(lis.Addr().String())
c.Assert(err, check.IsNil)
go func() {
err := startGRPCServer(lis)
c.Log(err)
c.Assert(err, check.IsNil)
}()
file := s.adaptFile(c, "fixtures/grpc/config_insecure.toml", struct {
CertContent string
KeyContent string
GRPCServerPort string
}{
CertContent: string(LocalhostCert),
KeyContent: string(LocalhostKey),
GRPCServerPort: port,
})
defer os.Remove(file)
cmd, display := s.traefikCmd(withConfigFile(file))
defer display(c)
err = cmd.Start()
c.Assert(err, check.IsNil)
defer cmd.Process.Kill()
// wait for Traefik
err = try.GetRequest("http://127.0.0.1:8080/api/providers", 1*time.Second, try.BodyContains("Host:127.0.0.1"))
c.Assert(err, check.IsNil)
var response string
err = try.Do(1*time.Second, func() error {
response, err = callHelloClientGRPC()
return err
})
c.Assert(err, check.IsNil)
c.Assert(response, check.Equals, "Hello World")
}

View file

@ -57,10 +57,19 @@ func (s *LogRotationSuite) TestAccessLogRotation(c *check.C) {
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
// Verify access.log.rotated output as expected // Verify access.log.rotated output as expected
logAccessLogFile(c, traefikTestAccessLogFile+".rotated")
lineCount := verifyLogLines(c, traefikTestAccessLogFile+".rotated", 0, true) lineCount := verifyLogLines(c, traefikTestAccessLogFile+".rotated", 0, true)
c.Assert(lineCount, checker.GreaterOrEqualThan, 1) c.Assert(lineCount, checker.GreaterOrEqualThan, 1)
// make sure that the access log file is at least created before we do assertions on it
err = try.Do(1*time.Second, func() error {
_, err := os.Stat(traefikTestAccessLogFile)
return err
})
c.Assert(err, checker.IsNil, check.Commentf("access log file was not created in time"))
// Verify access.log output as expected // Verify access.log output as expected
logAccessLogFile(c, traefikTestAccessLogFile)
lineCount = verifyLogLines(c, traefikTestAccessLogFile, lineCount, true) lineCount = verifyLogLines(c, traefikTestAccessLogFile, lineCount, true)
c.Assert(lineCount, checker.Equals, 3) c.Assert(lineCount, checker.Equals, 3)
@ -111,6 +120,12 @@ func (s *LogRotationSuite) TestTraefikLogRotation(c *check.C) {
c.Assert(lineCount, checker.GreaterOrEqualThan, 7) c.Assert(lineCount, checker.GreaterOrEqualThan, 7)
} }
func logAccessLogFile(c *check.C, fileName string) {
output, err := ioutil.ReadFile(fileName)
c.Assert(err, checker.IsNil)
c.Logf("Contents of file %s\n%s", fileName, string(output))
}
func verifyEmptyErrorLog(c *check.C, name string) { func verifyEmptyErrorLog(c *check.C, name string) {
err := try.Do(5*time.Second, func() error { err := try.Do(5*time.Second, func() error {
traefikLog, e2 := ioutil.ReadFile(name) traefikLog, e2 := ioutil.ReadFile(name)
@ -130,7 +145,6 @@ func verifyLogLines(c *check.C, fileName string, countInit int, accessLog bool)
count := countInit count := countInit
for rotatedLog.Scan() { for rotatedLog.Scan() {
line := rotatedLog.Text() line := rotatedLog.Text()
c.Log(line)
if accessLog { if accessLog {
if len(line) > 0 { if len(line) > 0 {
CheckAccessLogFormat(c, line, count) CheckAccessLogFormat(c, line, count)

View file

@ -6,80 +6,70 @@ import (
"net/http" "net/http"
"github.com/containous/traefik/log" "github.com/containous/traefik/log"
"github.com/containous/traefik/whitelist"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/urfave/negroni" "github.com/urfave/negroni"
) )
// IPWhitelister is a middleware that provides Checks of the Requesting IP against a set of Whitelists // IPWhiteLister is a middleware that provides Checks of the Requesting IP against a set of Whitelists
type IPWhitelister struct { type IPWhiteLister struct {
handler negroni.Handler handler negroni.Handler
whitelists []*net.IPNet whiteLister *whitelist.IP
} }
// NewIPWhitelister builds a new IPWhitelister given a list of CIDR-Strings to whitelist // NewIPWhitelister builds a new IPWhiteLister given a list of CIDR-Strings to whitelist
func NewIPWhitelister(whitelistStrings []string) (*IPWhitelister, error) { func NewIPWhitelister(whitelistStrings []string) (*IPWhiteLister, error) {
if len(whitelistStrings) == 0 { if len(whitelistStrings) == 0 {
return nil, errors.New("no whitelists provided") return nil, errors.New("no whitelists provided")
} }
whitelister := IPWhitelister{} whiteLister := IPWhiteLister{}
for _, whitelistString := range whitelistStrings { ip, err := whitelist.NewIP(whitelistStrings)
_, whitelist, err := net.ParseCIDR(whitelistString) if err != nil {
if err != nil { return nil, fmt.Errorf("parsing CIDR whitelist %s: %v", whitelistStrings, err)
return nil, fmt.Errorf("parsing CIDR whitelist %s: %v", whitelist, err)
}
whitelister.whitelists = append(whitelister.whitelists, whitelist)
} }
whiteLister.whiteLister = ip
whitelister.handler = negroni.HandlerFunc(whitelister.handle) whiteLister.handler = negroni.HandlerFunc(whiteLister.handle)
log.Debugf("configured %u IP whitelists: %s", len(whitelister.whitelists), whitelister.whitelists) log.Debugf("configured %u IP whitelists: %s", len(whitelistStrings), whitelistStrings)
return &whitelister, nil return &whiteLister, nil
} }
func (whitelister *IPWhitelister) handle(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { func (wl *IPWhiteLister) handle(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
remoteIP, err := ipFromRemoteAddr(r.RemoteAddr) ipAddress, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil { if err != nil {
log.Warnf("unable to parse remote-address from header: %s - rejecting", r.RemoteAddr) log.Warnf("unable to parse remote-address from header: %s - rejecting", r.RemoteAddr)
reject(w) reject(w)
return return
} }
for _, whitelist := range whitelister.whitelists { allowed, ip, err := wl.whiteLister.Contains(ipAddress)
if whitelist.Contains(*remoteIP) { if err != nil {
log.Debugf("source-IP %s matched whitelist %s - passing", remoteIP, whitelist) log.Debugf("source-IP %s matched none of the whitelists - rejecting", ipAddress)
next.ServeHTTP(w, r) reject(w)
return return
}
} }
log.Debugf("source-IP %s matched none of the whitelists - rejecting", remoteIP) if allowed {
log.Debugf("source-IP %s matched whitelist %s - passing", ipAddress, wl.whiteLister)
next.ServeHTTP(w, r)
return
}
log.Debugf("source-IP %s matched none of the whitelists - rejecting", ip)
reject(w) reject(w)
} }
func (wl *IPWhiteLister) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
wl.handler.ServeHTTP(rw, r, next)
}
func reject(w http.ResponseWriter) { func reject(w http.ResponseWriter) {
statusCode := http.StatusForbidden statusCode := http.StatusForbidden
w.WriteHeader(statusCode) w.WriteHeader(statusCode)
w.Write([]byte(http.StatusText(statusCode))) w.Write([]byte(http.StatusText(statusCode)))
} }
func ipFromRemoteAddr(addr string) (*net.IP, error) {
ip, _, err := net.SplitHostPort(addr)
if err != nil {
return nil, fmt.Errorf("can't extract IP/Port from address %s: %s", addr, err)
}
userIP := net.ParseIP(ip)
if userIP == nil {
return nil, fmt.Errorf("can't parse IP from address %s", ip)
}
return &userIP, nil
}
func (whitelister *IPWhitelister) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
whitelister.handler.ServeHTTP(rw, r, next)
}

View file

@ -16,17 +16,9 @@ type StripPrefix struct {
func (s *StripPrefix) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *StripPrefix) ServeHTTP(w http.ResponseWriter, r *http.Request) {
for _, prefix := range s.Prefixes { for _, prefix := range s.Prefixes {
origPrefix := strings.TrimSpace(prefix)
if origPrefix == r.URL.Path {
r.URL.Path = "/"
s.serveRequest(w, r, origPrefix)
return
}
prefix = strings.TrimSuffix(origPrefix, "/") + "/"
if p := strings.TrimPrefix(r.URL.Path, prefix); len(p) < len(r.URL.Path) { if p := strings.TrimPrefix(r.URL.Path, prefix); len(p) < len(r.URL.Path) {
r.URL.Path = "/" + strings.TrimPrefix(p, "/") r.URL.Path = "/" + strings.TrimPrefix(p, "/")
s.serveRequest(w, r, origPrefix) s.serveRequest(w, r, strings.TrimSpace(prefix))
return return
} }
} }

View file

@ -86,6 +86,14 @@ func TestStripPrefix(t *testing.T) {
expectedPath: "/", expectedPath: "/",
expectedHeader: "/stat", expectedHeader: "/stat",
}, },
{
desc: "prefix matching within slash boundaries",
prefixes: []string{"/stat"},
path: "/status",
expectedStatusCode: http.StatusOK,
expectedPath: "/us",
expectedHeader: "/stat",
},
} }
for _, test := range tests { for _, test := range tests {

View file

@ -385,10 +385,6 @@ func (p *CatalogProvider) getBackendName(node *api.ServiceEntry, index int) stri
return serviceName return serviceName
} }
func (p *CatalogProvider) getAttribute(name string, tags []string, defaultValue string) string {
return p.getTag(p.getPrefixedName(name), tags, defaultValue)
}
func (p *CatalogProvider) getBasicAuth(tags []string) []string { func (p *CatalogProvider) getBasicAuth(tags []string) []string {
list := p.getAttribute("frontend.auth.basic", tags, "") list := p.getAttribute("frontend.auth.basic", tags, "")
if list != "" { if list != "" {
@ -397,6 +393,27 @@ func (p *CatalogProvider) getBasicAuth(tags []string) []string {
return []string{} return []string{}
} }
func (p *CatalogProvider) hasStickinessLabel(tags []string) bool {
stickinessTag := p.getTag(types.LabelBackendLoadbalancerStickiness, tags, "")
stickyTag := p.getTag(types.LabelBackendLoadbalancerSticky, tags, "")
if len(stickyTag) > 0 {
log.Warn("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness)
}
stickiness := len(stickinessTag) > 0 && strings.EqualFold(strings.TrimSpace(stickinessTag), "true")
sticky := len(stickyTag) > 0 && strings.EqualFold(strings.TrimSpace(stickyTag), "true")
return stickiness || sticky
}
func (p *CatalogProvider) getStickinessCookieName(tags []string) string {
return p.getTag(types.LabelBackendLoadbalancerStickinessCookieName, tags, "")
}
func (p *CatalogProvider) getAttribute(name string, tags []string, defaultValue string) string {
return p.getTag(p.getPrefixedName(name), tags, defaultValue)
}
func (p *CatalogProvider) hasTag(name string, tags []string) bool { func (p *CatalogProvider) hasTag(name string, tags []string) bool {
// Very-very unlikely that a Consul tag would ever start with '=!=' // Very-very unlikely that a Consul tag would ever start with '=!='
tag := p.getTag(name, tags, "=!=") tag := p.getTag(name, tags, "=!=")
@ -439,16 +456,18 @@ func (p *CatalogProvider) getConstraintTags(tags []string) []string {
func (p *CatalogProvider) buildConfig(catalog []catalogUpdate) *types.Configuration { func (p *CatalogProvider) buildConfig(catalog []catalogUpdate) *types.Configuration {
var FuncMap = template.FuncMap{ var FuncMap = template.FuncMap{
"getBackend": p.getBackend, "getBackend": p.getBackend,
"getFrontendRule": p.getFrontendRule, "getFrontendRule": p.getFrontendRule,
"getBackendName": p.getBackendName, "getBackendName": p.getBackendName,
"getBackendAddress": p.getBackendAddress, "getBackendAddress": p.getBackendAddress,
"getAttribute": p.getAttribute, "getBasicAuth": p.getBasicAuth,
"getBasicAuth": p.getBasicAuth, "hasStickinessLabel": p.hasStickinessLabel,
"getTag": p.getTag, "getStickinessCookieName": p.getStickinessCookieName,
"hasTag": p.hasTag, "getAttribute": p.getAttribute,
"getEntryPoints": p.getEntryPoints, "getTag": p.getTag,
"hasMaxconnAttributes": p.hasMaxconnAttributes, "hasTag": p.hasTag,
"getEntryPoints": p.getEntryPoints,
"hasMaxconnAttributes": p.hasMaxconnAttributes,
} }
allNodes := []*api.ServiceEntry{} allNodes := []*api.ServiceEntry{}

View file

@ -275,7 +275,8 @@ func (p *Provider) loadDockerConfig(containersInspected []dockerData) *types.Con
"hasMaxConnLabels": p.hasMaxConnLabels, "hasMaxConnLabels": p.hasMaxConnLabels,
"getMaxConnAmount": p.getMaxConnAmount, "getMaxConnAmount": p.getMaxConnAmount,
"getMaxConnExtractorFunc": p.getMaxConnExtractorFunc, "getMaxConnExtractorFunc": p.getMaxConnExtractorFunc,
"getSticky": p.getSticky, "getStickinessCookieName": p.getStickinessCookieName,
"hasStickinessLabel": p.hasStickinessLabel,
"getIsBackendLBSwarm": p.getIsBackendLBSwarm, "getIsBackendLBSwarm": p.getIsBackendLBSwarm,
"hasServices": p.hasServices, "hasServices": p.hasServices,
"getServiceNames": p.getServiceNames, "getServiceNames": p.getServiceNames,
@ -328,10 +329,8 @@ func (p *Provider) loadDockerConfig(containersInspected []dockerData) *types.Con
} }
func (p *Provider) hasCircuitBreakerLabel(container dockerData) bool { func (p *Provider) hasCircuitBreakerLabel(container dockerData) bool {
if _, err := getLabel(container, types.LabelBackendCircuitbreakerExpression); err != nil { _, err := getLabel(container, types.LabelBackendCircuitbreakerExpression)
return false return err == nil
}
return true
} }
// Regexp used to extract the name of the service and the name of the property for this service // Regexp used to extract the name of the service and the name of the property for this service
@ -645,11 +644,22 @@ func (p *Provider) getWeight(container dockerData) string {
return "0" return "0"
} }
func (p *Provider) getSticky(container dockerData) string { func (p *Provider) hasStickinessLabel(container dockerData) bool {
if label, err := getLabel(container, types.LabelBackendLoadbalancerSticky); err == nil { _, errStickiness := getLabel(container, types.LabelBackendLoadbalancerStickiness)
label, errSticky := getLabel(container, types.LabelBackendLoadbalancerSticky)
if len(label) > 0 {
log.Warn("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness)
}
return errStickiness == nil || (errSticky == nil && strings.EqualFold(strings.TrimSpace(label), "true"))
}
func (p *Provider) getStickinessCookieName(container dockerData) string {
if label, err := getLabel(container, types.LabelBackendLoadbalancerStickinessCookieName); err == nil {
return label return label
} }
return "false" return ""
} }
func (p *Provider) getIsBackendLBSwarm(container dockerData) string { func (p *Provider) getIsBackendLBSwarm(container dockerData) string {

View file

@ -162,12 +162,12 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
}) })
operation := func() error { operation := func() error {
aws, err := p.createClient() awsClient, err := p.createClient()
if err != nil { if err != nil {
return handleCanceled(ctx, err) return handleCanceled(ctx, err)
} }
configuration, err := p.loadDynamoConfig(aws) configuration, err := p.loadDynamoConfig(awsClient)
if err != nil { if err != nil {
return handleCanceled(ctx, err) return handleCanceled(ctx, err)
} }
@ -184,7 +184,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
log.Debug("Watching Provider...") log.Debug("Watching Provider...")
select { select {
case <-reload.C: case <-reload.C:
configuration, err := p.loadDynamoConfig(aws) configuration, err := p.loadDynamoConfig(awsClient)
if err != nil { if err != nil {
return handleCanceled(ctx, err) return handleCanceled(ctx, err)
} }

View file

@ -122,12 +122,12 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
}) })
operation := func() error { operation := func() error {
aws, err := p.createClient() awsClient, err := p.createClient()
if err != nil { if err != nil {
return err return err
} }
configuration, err := p.loadECSConfig(ctx, aws) configuration, err := p.loadECSConfig(ctx, awsClient)
if err != nil { if err != nil {
return handleCanceled(ctx, err) return handleCanceled(ctx, err)
} }
@ -143,7 +143,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
for { for {
select { select {
case <-reload.C: case <-reload.C:
configuration, err := p.loadECSConfig(ctx, aws) configuration, err := p.loadECSConfig(ctx, awsClient)
if err != nil { if err != nil {
return handleCanceled(ctx, err) return handleCanceled(ctx, err)
} }
@ -180,11 +180,12 @@ func wrapAws(ctx context.Context, req *request.Request) error {
func (p *Provider) loadECSConfig(ctx context.Context, client *awsClient) (*types.Configuration, error) { func (p *Provider) loadECSConfig(ctx context.Context, client *awsClient) (*types.Configuration, error) {
var ecsFuncMap = template.FuncMap{ var ecsFuncMap = template.FuncMap{
"filterFrontends": p.filterFrontends, "filterFrontends": p.filterFrontends,
"getFrontendRule": p.getFrontendRule, "getFrontendRule": p.getFrontendRule,
"getBasicAuth": p.getBasicAuth, "getBasicAuth": p.getBasicAuth,
"getLoadBalancerSticky": p.getLoadBalancerSticky, "getLoadBalancerMethod": p.getLoadBalancerMethod,
"getLoadBalancerMethod": p.getLoadBalancerMethod, "hasStickinessLabel": p.hasStickinessLabel,
"getStickinessCookieName": p.getStickinessCookieName,
} }
instances, err := p.listInstances(ctx, client) instances, err := p.listInstances(ctx, client)
@ -477,14 +478,27 @@ func (p *Provider) getBasicAuth(i ecsInstance) []string {
return []string{} return []string{}
} }
func (p *Provider) getLoadBalancerSticky(instances []ecsInstance) string { func (p *Provider) getFirstInstanceLabel(instances []ecsInstance, labelName string) string {
if len(instances) > 0 { if len(instances) > 0 {
label := p.label(instances[0], types.LabelBackendLoadbalancerSticky) return p.label(instances[0], labelName)
if label != "" {
return label
}
} }
return "false" return ""
}
func (p *Provider) hasStickinessLabel(instances []ecsInstance) bool {
stickinessLabel := p.getFirstInstanceLabel(instances, types.LabelBackendLoadbalancerStickiness)
stickyLabel := p.getFirstInstanceLabel(instances, types.LabelBackendLoadbalancerSticky)
if len(stickyLabel) > 0 {
log.Warn("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness)
}
stickiness := len(stickinessLabel) > 0 && strings.EqualFold(strings.TrimSpace(stickinessLabel), "true")
sticky := len(stickyLabel) > 0 && strings.EqualFold(strings.TrimSpace(stickyLabel), "true")
return stickiness || sticky
}
func (p *Provider) getStickinessCookieName(instances []ecsInstance) string {
return p.getFirstInstanceLabel(instances, types.LabelBackendLoadbalancerStickinessCookieName)
} }
func (p *Provider) getLoadBalancerMethod(instances []ecsInstance) string { func (p *Provider) getLoadBalancerMethod(instances []ecsInstance) string {

View file

@ -18,6 +18,7 @@ import (
"github.com/containous/traefik/log" "github.com/containous/traefik/log"
"github.com/containous/traefik/provider" "github.com/containous/traefik/provider"
"github.com/containous/traefik/safe" "github.com/containous/traefik/safe"
"github.com/containous/traefik/server/cookie"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
"k8s.io/client-go/pkg/api/v1" "k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/pkg/apis/extensions/v1beta1" "k8s.io/client-go/pkg/apis/extensions/v1beta1"
@ -160,7 +161,6 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
templateObjects.Backends[r.Host+pa.Path] = &types.Backend{ templateObjects.Backends[r.Host+pa.Path] = &types.Backend{
Servers: make(map[string]types.Server), Servers: make(map[string]types.Server),
LoadBalancer: &types.LoadBalancer{ LoadBalancer: &types.LoadBalancer{
Sticky: false,
Method: "wrr", Method: "wrr",
}, },
} }
@ -247,8 +247,14 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
templateObjects.Backends[r.Host+pa.Path].LoadBalancer.Method = "drr" templateObjects.Backends[r.Host+pa.Path].LoadBalancer.Method = "drr"
} }
if service.Annotations[types.LabelBackendLoadbalancerSticky] == "true" { if len(service.Annotations[types.LabelBackendLoadbalancerSticky]) > 0 {
templateObjects.Backends[r.Host+pa.Path].LoadBalancer.Sticky = true log.Warn("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness)
}
if service.Annotations[types.LabelBackendLoadbalancerSticky] == "true" || service.Annotations[types.LabelBackendLoadbalancerStickiness] == "true" {
templateObjects.Backends[r.Host+pa.Path].LoadBalancer.Stickiness = &types.Stickiness{
CookieName: cookie.GenerateName(r.Host + pa.Path),
}
} }
protocol := "http" protocol := "http"

View file

@ -243,7 +243,6 @@ func TestLoadIngresses(t *testing.T) {
}, },
CircuitBreaker: nil, CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{ LoadBalancer: &types.LoadBalancer{
Sticky: false,
Method: "wrr", Method: "wrr",
}, },
}, },
@ -256,7 +255,6 @@ func TestLoadIngresses(t *testing.T) {
}, },
CircuitBreaker: nil, CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{ LoadBalancer: &types.LoadBalancer{
Sticky: false,
Method: "wrr", Method: "wrr",
}, },
}, },
@ -273,7 +271,6 @@ func TestLoadIngresses(t *testing.T) {
}, },
CircuitBreaker: nil, CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{ LoadBalancer: &types.LoadBalancer{
Sticky: false,
Method: "wrr", Method: "wrr",
}, },
}, },
@ -489,7 +486,6 @@ func TestGetPassHostHeader(t *testing.T) {
Servers: map[string]types.Server{}, Servers: map[string]types.Server{},
CircuitBreaker: nil, CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{ LoadBalancer: &types.LoadBalancer{
Sticky: false,
Method: "wrr", Method: "wrr",
}, },
}, },
@ -591,7 +587,6 @@ func TestOnlyReferencesServicesFromOwnNamespace(t *testing.T) {
Servers: map[string]types.Server{}, Servers: map[string]types.Server{},
CircuitBreaker: nil, CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{ LoadBalancer: &types.LoadBalancer{
Sticky: false,
Method: "wrr", Method: "wrr",
}, },
}, },
@ -673,7 +668,6 @@ func TestHostlessIngress(t *testing.T) {
Servers: map[string]types.Server{}, Servers: map[string]types.Server{},
CircuitBreaker: nil, CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{ LoadBalancer: &types.LoadBalancer{
Sticky: false,
Method: "wrr", Method: "wrr",
}, },
}, },
@ -875,7 +869,6 @@ func TestServiceAnnotations(t *testing.T) {
}, },
LoadBalancer: &types.LoadBalancer{ LoadBalancer: &types.LoadBalancer{
Method: "drr", Method: "drr",
Sticky: false,
}, },
}, },
"bar": { "bar": {
@ -892,7 +885,9 @@ func TestServiceAnnotations(t *testing.T) {
CircuitBreaker: nil, CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{ LoadBalancer: &types.LoadBalancer{
Method: "wrr", Method: "wrr",
Sticky: true, Stickiness: &types.Stickiness{
CookieName: "_4155f",
},
}, },
}, },
}, },
@ -1156,7 +1151,6 @@ func TestIngressAnnotations(t *testing.T) {
}, },
CircuitBreaker: nil, CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{ LoadBalancer: &types.LoadBalancer{
Sticky: false,
Method: "wrr", Method: "wrr",
}, },
}, },
@ -1169,7 +1163,6 @@ func TestIngressAnnotations(t *testing.T) {
}, },
CircuitBreaker: nil, CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{ LoadBalancer: &types.LoadBalancer{
Sticky: false,
Method: "wrr", Method: "wrr",
}, },
}, },
@ -1182,7 +1175,6 @@ func TestIngressAnnotations(t *testing.T) {
}, },
CircuitBreaker: nil, CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{ LoadBalancer: &types.LoadBalancer{
Sticky: false,
Method: "wrr", Method: "wrr",
}, },
}, },
@ -1195,7 +1187,6 @@ func TestIngressAnnotations(t *testing.T) {
}, },
CircuitBreaker: nil, CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{ LoadBalancer: &types.LoadBalancer{
Sticky: false,
Method: "wrr", Method: "wrr",
}, },
}, },
@ -1208,7 +1199,6 @@ func TestIngressAnnotations(t *testing.T) {
}, },
CircuitBreaker: nil, CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{ LoadBalancer: &types.LoadBalancer{
Sticky: false,
Method: "wrr", Method: "wrr",
}, },
}, },
@ -1362,7 +1352,6 @@ func TestPriorityHeaderValue(t *testing.T) {
}, },
CircuitBreaker: nil, CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{ LoadBalancer: &types.LoadBalancer{
Sticky: false,
Method: "wrr", Method: "wrr",
}, },
}, },
@ -1464,7 +1453,6 @@ func TestInvalidPassHostHeaderValue(t *testing.T) {
}, },
CircuitBreaker: nil, CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{ LoadBalancer: &types.LoadBalancer{
Sticky: false,
Method: "wrr", Method: "wrr",
}, },
}, },
@ -1747,14 +1735,12 @@ func TestMissingResources(t *testing.T) {
CircuitBreaker: nil, CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{ LoadBalancer: &types.LoadBalancer{
Method: "wrr", Method: "wrr",
Sticky: false,
}, },
}, },
"missing_service": { "missing_service": {
Servers: map[string]types.Server{}, Servers: map[string]types.Server{},
LoadBalancer: &types.LoadBalancer{ LoadBalancer: &types.LoadBalancer{
Method: "wrr", Method: "wrr",
Sticky: false,
}, },
}, },
"missing_endpoints": { "missing_endpoints": {
@ -1762,7 +1748,6 @@ func TestMissingResources(t *testing.T) {
CircuitBreaker: nil, CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{ LoadBalancer: &types.LoadBalancer{
Method: "wrr", Method: "wrr",
Sticky: false,
}, },
}, },
"missing_endpoint_subsets": { "missing_endpoint_subsets": {
@ -1770,7 +1755,6 @@ func TestMissingResources(t *testing.T) {
CircuitBreaker: nil, CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{ LoadBalancer: &types.LoadBalancer{
Method: "wrr", Method: "wrr",
Sticky: false,
}, },
}, },
}, },

View file

@ -139,11 +139,13 @@ func (p *Provider) loadConfig() *types.Configuration {
} }
var KvFuncMap = template.FuncMap{ var KvFuncMap = template.FuncMap{
"List": p.list, "List": p.list,
"ListServers": p.listServers, "ListServers": p.listServers,
"Get": p.get, "Get": p.get,
"SplitGet": p.splitGet, "SplitGet": p.splitGet,
"Last": p.last, "Last": p.last,
"hasStickinessLabel": p.hasStickinessLabel,
"getStickinessCookieName": p.getStickinessCookieName,
} }
configuration, err := p.GetConfiguration("templates/kv.tmpl", KvFuncMap, templateObjects) configuration, err := p.GetConfiguration("templates/kv.tmpl", KvFuncMap, templateObjects)
@ -239,3 +241,17 @@ func (p *Provider) checkConstraints(keys ...string) bool {
} }
return true return true
} }
func (p *Provider) hasStickinessLabel(rootPath string) bool {
stickiness, err := p.kvclient.Exists(rootPath + "/loadbalancer/stickiness")
if err != nil {
log.Debugf("Error occurs when check stickiness: %v", err)
}
sticky := p.get("false", rootPath, "/loadbalancer", "/sticky")
return stickiness || (len(sticky) != 0 && strings.EqualFold(strings.TrimSpace(sticky), "true"))
}
func (p *Provider) getStickinessCookieName(rootPath string) string {
return p.get("", rootPath, "/loadbalancer", "/stickiness", "/cookiename")
}

View file

@ -188,7 +188,8 @@ func (p *Provider) loadMarathonConfig() *types.Configuration {
"getMaxConnAmount": p.getMaxConnAmount, "getMaxConnAmount": p.getMaxConnAmount,
"getLoadBalancerMethod": p.getLoadBalancerMethod, "getLoadBalancerMethod": p.getLoadBalancerMethod,
"getCircuitBreakerExpression": p.getCircuitBreakerExpression, "getCircuitBreakerExpression": p.getCircuitBreakerExpression,
"getSticky": p.getSticky, "getStickinessCookieName": p.getStickinessCookieName,
"hasStickinessLabel": p.hasStickinessLabel,
"hasHealthCheckLabels": p.hasHealthCheckLabels, "hasHealthCheckLabels": p.hasHealthCheckLabels,
"getHealthCheckPath": p.getHealthCheckPath, "getHealthCheckPath": p.getHealthCheckPath,
"getHealthCheckInterval": p.getHealthCheckInterval, "getHealthCheckInterval": p.getHealthCheckInterval,
@ -428,11 +429,22 @@ func (p *Provider) getProtocol(application marathon.Application, serviceName str
return "http" return "http"
} }
func (p *Provider) getSticky(application marathon.Application) string { func (p *Provider) hasStickinessLabel(application marathon.Application) bool {
if sticky, ok := p.getAppLabel(application, types.LabelBackendLoadbalancerSticky); ok { _, okStickiness := p.getAppLabel(application, types.LabelBackendLoadbalancerStickiness)
return sticky
label, okSticky := p.getAppLabel(application, types.LabelBackendLoadbalancerSticky)
if len(label) > 0 {
log.Warn("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness)
} }
return "false"
return okStickiness || (okSticky && strings.EqualFold(strings.TrimSpace(label), "true"))
}
func (p *Provider) getStickinessCookieName(application marathon.Application) string {
if label, ok := p.getAppLabel(application, types.LabelBackendLoadbalancerStickinessCookieName); ok {
return label
}
return ""
} }
func (p *Provider) getPassHostHeader(application marathon.Application, serviceName string) string { func (p *Provider) getPassHostHeader(application marathon.Application, serviceName string) string {

View file

@ -855,21 +855,36 @@ func TestMarathonGetProtocol(t *testing.T) {
} }
} }
func TestMarathonGetSticky(t *testing.T) { func TestMarathonHasStickinessLabel(t *testing.T) {
cases := []struct { cases := []struct {
desc string desc string
application marathon.Application application marathon.Application
expected string expected bool
}{ }{
{ {
desc: "label missing", desc: "label missing",
application: application(), application: application(),
expected: "false", expected: false,
}, },
{ {
desc: "label existing", desc: "label existing and value equals true (deprecated)",
application: application(label(types.LabelBackendLoadbalancerSticky, "true")), application: application(label(types.LabelBackendLoadbalancerSticky, "true")),
expected: "true", expected: true,
},
{
desc: "label existing and value equals false (deprecated)",
application: application(label(types.LabelBackendLoadbalancerSticky, "false")),
expected: false,
},
{
desc: "label existing and value equals true",
application: application(label(types.LabelBackendLoadbalancerStickiness, "true")),
expected: true,
},
{
desc: "label existing and value equals false ",
application: application(label(types.LabelBackendLoadbalancerStickiness, "true")),
expected: true,
}, },
} }
@ -878,7 +893,7 @@ func TestMarathonGetSticky(t *testing.T) {
t.Run(c.desc, func(t *testing.T) { t.Run(c.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
provider := &Provider{} provider := &Provider{}
actual := provider.getSticky(c.application) actual := provider.hasStickinessLabel(c.application)
if actual != c.expected { if actual != c.expected {
t.Errorf("actual %q, expected %q", actual, c.expected) t.Errorf("actual %q, expected %q", actual, c.expected)
} }

View file

@ -34,7 +34,7 @@ type BaseProvider struct {
// MatchConstraints must match with EVERY single contraint // MatchConstraints must match with EVERY single contraint
// returns first constraint that do not match or nil // returns first constraint that do not match or nil
func (p *BaseProvider) MatchConstraints(tags []string) (bool, *types.Constraint) { func (p *BaseProvider) MatchConstraints(tags []string) (bool, *types.Constraint) {
// if there is no tags and no contraints, filtering is disabled // if there is no tags and no constraints, filtering is disabled
if len(tags) == 0 && len(p.Constraints) == 0 { if len(tags) == 0 && len(p.Constraints) == 0 {
return true, nil return true, nil
} }

View file

@ -112,11 +112,22 @@ func (p *Provider) getCircuitBreakerExpression(service rancherData) string {
return "NetworkErrorRatio() > 1" return "NetworkErrorRatio() > 1"
} }
func (p *Provider) getSticky(service rancherData) string { func (p *Provider) hasStickinessLabel(service rancherData) bool {
if _, err := getServiceLabel(service, types.LabelBackendLoadbalancerSticky); err == nil { _, errStickiness := getServiceLabel(service, types.LabelBackendLoadbalancerStickiness)
return "true"
label, errSticky := getServiceLabel(service, types.LabelBackendLoadbalancerSticky)
if len(label) > 0 {
log.Warn("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness)
} }
return "false"
return errStickiness == nil || (errSticky == nil && strings.EqualFold(strings.TrimSpace(label), "true"))
}
func (p *Provider) getStickinessCookieName(service rancherData, backendName string) string {
if label, err := getServiceLabel(service, types.LabelBackendLoadbalancerStickinessCookieName); err == nil {
return label
}
return ""
} }
func (p *Provider) getBackend(service rancherData) string { func (p *Provider) getBackend(service rancherData) string {
@ -222,7 +233,8 @@ func (p *Provider) loadRancherConfig(services []rancherData) *types.Configuratio
"hasMaxConnLabels": p.hasMaxConnLabels, "hasMaxConnLabels": p.hasMaxConnLabels,
"getMaxConnAmount": p.getMaxConnAmount, "getMaxConnAmount": p.getMaxConnAmount,
"getMaxConnExtractorFunc": p.getMaxConnExtractorFunc, "getMaxConnExtractorFunc": p.getMaxConnExtractorFunc,
"getSticky": p.getSticky, "hasStickinessLabel": p.hasStickinessLabel,
"getStickinessCookieName": p.getStickinessCookieName,
} }
// filter services // filter services

57
server/cookie/cookie.go Normal file
View file

@ -0,0 +1,57 @@
package cookie
import (
"crypto/sha1"
"fmt"
"strings"
"github.com/containous/traefik/log"
)
const cookieNameLength = 6
// GetName of a cookie
func GetName(cookieName string, backendName string) string {
if len(cookieName) != 0 {
return sanitizeName(cookieName)
}
return GenerateName(backendName)
}
// GenerateName Generate a hashed name
func GenerateName(backendName string) string {
data := []byte("_TRAEFIK_BACKEND_" + backendName)
hash := sha1.New()
_, err := hash.Write(data)
if err != nil {
// Impossible case
log.Errorf("Fail to create cookie name: %v", err)
}
return fmt.Sprintf("_%x", hash.Sum(nil))[:cookieNameLength]
}
// sanitizeName According to [RFC 2616](https://www.ietf.org/rfc/rfc2616.txt) section 2.2
func sanitizeName(backend string) string {
sanitizer := func(r rune) rune {
switch r {
case '!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '`', '|', '~':
return r
}
switch {
case 'a' <= r && r <= 'z':
fallthrough
case 'A' <= r && r <= 'Z':
fallthrough
case '0' <= r && r <= '9':
return r
default:
return '_'
}
}
return strings.Map(sanitizer, backend)
}

View file

@ -0,0 +1,83 @@
package cookie
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetName(t *testing.T) {
testCases := []struct {
desc string
cookieName string
backendName string
expectedCookieName string
}{
{
desc: "with backend name, without cookie name",
cookieName: "",
backendName: "/my/BACKEND-v1.0~rc1",
expectedCookieName: "_5f7bc",
},
{
desc: "without backend name, with cookie name",
cookieName: "/my/BACKEND-v1.0~rc1",
backendName: "",
expectedCookieName: "_my_BACKEND-v1.0~rc1",
},
{
desc: "with backend name, with cookie name",
cookieName: "containous",
backendName: "treafik",
expectedCookieName: "containous",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
cookieName := GetName(test.cookieName, test.backendName)
assert.Equal(t, test.expectedCookieName, cookieName)
})
}
}
func Test_sanitizeName(t *testing.T) {
testCases := []struct {
desc string
srcName string
expectedName string
}{
{
desc: "with /",
srcName: "/my/BACKEND-v1.0~rc1",
expectedName: "_my_BACKEND-v1.0~rc1",
},
{
desc: "some chars",
srcName: "!#$%&'()*+-./:<=>?@[]^_`{|}~",
expectedName: "!#$%&'__*+-._________^_`_|_~",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
cookieName := sanitizeName(test.srcName)
assert.Equal(t, test.expectedName, cookieName, "Cookie name")
})
}
}
func TestGenerateName(t *testing.T) {
cookieName := GenerateName("containous")
assert.Len(t, "_8a7bc", 6)
assert.Equal(t, "_8a7bc", cookieName)
}

View file

@ -6,6 +6,7 @@ import (
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io/ioutil" "io/ioutil"
"net" "net"
"net/http" "net/http"
@ -30,7 +31,9 @@ import (
mauth "github.com/containous/traefik/middlewares/auth" mauth "github.com/containous/traefik/middlewares/auth"
"github.com/containous/traefik/provider" "github.com/containous/traefik/provider"
"github.com/containous/traefik/safe" "github.com/containous/traefik/safe"
"github.com/containous/traefik/server/cookie"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
"github.com/containous/traefik/whitelist"
"github.com/streamrail/concurrent-map" "github.com/streamrail/concurrent-map"
thoas_stats "github.com/thoas/stats" thoas_stats "github.com/thoas/stats"
"github.com/urfave/negroni" "github.com/urfave/negroni"
@ -154,8 +157,8 @@ func createHTTPTransport(globalConfiguration configuration.GlobalConfiguration)
transport.TLSClientConfig = &tls.Config{ transport.TLSClientConfig = &tls.Config{
RootCAs: createRootCACertPool(globalConfiguration.RootCAs), RootCAs: createRootCACertPool(globalConfiguration.RootCAs),
} }
http2.ConfigureTransport(transport)
} }
http2.ConfigureTransport(transport)
return transport return transport
} }
@ -335,11 +338,11 @@ func (server *Server) listenProviders(stop chan bool) {
lastReceivedConfigurationValue := lastReceivedConfiguration.Get().(time.Time) lastReceivedConfigurationValue := lastReceivedConfiguration.Get().(time.Time)
providersThrottleDuration := time.Duration(server.globalConfiguration.ProvidersThrottleDuration) providersThrottleDuration := time.Duration(server.globalConfiguration.ProvidersThrottleDuration)
if time.Now().After(lastReceivedConfigurationValue.Add(providersThrottleDuration)) { if time.Now().After(lastReceivedConfigurationValue.Add(providersThrottleDuration)) {
log.Debugf("Last %s config received more than %s, OK", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration.String()) log.Debugf("Last %s config received more than %s, OK", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration)
// last config received more than n s ago // last config received more than n s ago
server.configurationValidatedChan <- configMsg server.configurationValidatedChan <- configMsg
} else { } else {
log.Debugf("Last %s config received less than %s, waiting...", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration.String()) log.Debugf("Last %s config received less than %s, waiting...", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration)
safe.Go(func() { safe.Go(func() {
<-time.After(providersThrottleDuration) <-time.After(providersThrottleDuration)
lastReceivedConfigurationValue := lastReceivedConfiguration.Get().(time.Time) lastReceivedConfigurationValue := lastReceivedConfiguration.Get().(time.Time)
@ -652,8 +655,22 @@ func (server *Server) prepareServer(entryPointName string, entryPoint *configura
return nil, nil, err return nil, nil, err
} }
if entryPoint.ProxyProtocol { if entryPoint.ProxyProtocol != nil {
listener = &proxyproto.Listener{Listener: listener} IPs, err := whitelist.NewIP(entryPoint.ProxyProtocol.TrustedIPs)
if err != nil {
return nil, nil, fmt.Errorf("Error creating whitelist: %s", err)
}
log.Infof("Enabling ProxyProtocol for trusted IPs %v", entryPoint.ProxyProtocol.TrustedIPs)
listener = &proxyproto.Listener{
Listener: listener,
SourceCheck: func(addr net.Addr) (bool, error) {
ip, ok := addr.(*net.TCPAddr)
if !ok {
return false, fmt.Errorf("Type error %v", addr)
}
return IPs.ContainsIP(ip.IP)
},
}
} }
return &http.Server{ return &http.Server{
@ -826,11 +843,10 @@ func (server *Server) loadConfig(configurations types.Configurations, globalConf
continue frontend continue frontend
} }
stickySession := config.Backends[frontend.Backend].LoadBalancer.Sticky
cookieName := "_TRAEFIK_BACKEND_" + frontend.Backend
var sticky *roundrobin.StickySession var sticky *roundrobin.StickySession
var cookieName string
if stickySession { if stickiness := config.Backends[frontend.Backend].LoadBalancer.Stickiness; stickiness != nil {
cookieName = cookie.GetName(stickiness.CookieName, frontend.Backend)
sticky = roundrobin.NewStickySession(cookieName) sticky = roundrobin.NewStickySession(cookieName)
} }
@ -839,7 +855,7 @@ func (server *Server) loadConfig(configurations types.Configurations, globalConf
case types.Drr: case types.Drr:
log.Debugf("Creating load-balancer drr") log.Debugf("Creating load-balancer drr")
rebalancer, _ := roundrobin.NewRebalancer(rr, roundrobin.RebalancerLogger(oxyLogger)) rebalancer, _ := roundrobin.NewRebalancer(rr, roundrobin.RebalancerLogger(oxyLogger))
if stickySession { if sticky != nil {
log.Debugf("Sticky session with cookie %v", cookieName) log.Debugf("Sticky session with cookie %v", cookieName)
rebalancer, _ = roundrobin.NewRebalancer(rr, roundrobin.RebalancerLogger(oxyLogger), roundrobin.RebalancerStickySession(sticky)) rebalancer, _ = roundrobin.NewRebalancer(rr, roundrobin.RebalancerLogger(oxyLogger), roundrobin.RebalancerStickySession(sticky))
} }
@ -856,7 +872,7 @@ func (server *Server) loadConfig(configurations types.Configurations, globalConf
lb = middlewares.NewEmptyBackendHandler(rebalancer, lb) lb = middlewares.NewEmptyBackendHandler(rebalancer, lb)
case types.Wrr: case types.Wrr:
log.Debugf("Creating load-balancer wrr") log.Debugf("Creating load-balancer wrr")
if stickySession { if sticky != nil {
log.Debugf("Sticky session with cookie %v", cookieName) log.Debugf("Sticky session with cookie %v", cookieName)
if server.accessLoggerMiddleware != nil { if server.accessLoggerMiddleware != nil {
rr, _ = roundrobin.New(saveFrontend, roundrobin.EnableStickySession(sticky)) rr, _ = roundrobin.New(saveFrontend, roundrobin.EnableStickySession(sticky))
@ -1159,16 +1175,31 @@ func (server *Server) configureFrontends(frontends map[string]*types.Frontend) {
func (*Server) configureBackends(backends map[string]*types.Backend) { func (*Server) configureBackends(backends map[string]*types.Backend) {
for backendName, backend := range backends { for backendName, backend := range backends {
if backend.LoadBalancer != nil && backend.LoadBalancer.Sticky {
log.Warn("Deprecated configuration found: %s. Please use %s.", "backend.LoadBalancer.Sticky", "backend.LoadBalancer.Stickiness")
}
_, err := types.NewLoadBalancerMethod(backend.LoadBalancer) _, err := types.NewLoadBalancerMethod(backend.LoadBalancer)
if err != nil { if err == nil {
if backend.LoadBalancer != nil && backend.LoadBalancer.Stickiness == nil && backend.LoadBalancer.Sticky {
backend.LoadBalancer.Stickiness = &types.Stickiness{}
}
} else {
log.Debugf("Validation of load balancer method for backend %s failed: %s. Using default method wrr.", backendName, err) log.Debugf("Validation of load balancer method for backend %s failed: %s. Using default method wrr.", backendName, err)
var sticky bool
var stickiness *types.Stickiness
if backend.LoadBalancer != nil { if backend.LoadBalancer != nil {
sticky = backend.LoadBalancer.Sticky if backend.LoadBalancer.Stickiness != nil {
stickiness = backend.LoadBalancer.Stickiness
} else if backend.LoadBalancer.Sticky {
if backend.LoadBalancer.Stickiness == nil {
stickiness = &types.Stickiness{}
}
}
} }
backend.LoadBalancer = &types.LoadBalancer{ backend.LoadBalancer = &types.LoadBalancer{
Method: "wrr", Method: "wrr",
Sticky: sticky, Stickiness: stickiness,
} }
} }
} }

View file

@ -355,7 +355,7 @@ func TestNewServerWithWhitelistSourceRange(t *testing.T) {
"foo", "foo",
}, },
middlewareConfigured: false, middlewareConfigured: false,
errMessage: "parsing CIDR whitelist <nil>: invalid CIDR address: foo", errMessage: "parsing CIDR whitelist [foo]: parsing CIDR whitelist <nil>: invalid CIDR address: foo",
}, },
} }
@ -422,52 +422,49 @@ func TestConfigureBackends(t *testing.T) {
defaultMethod := "wrr" defaultMethod := "wrr"
tests := []struct { tests := []struct {
desc string desc string
lb *types.LoadBalancer lb *types.LoadBalancer
wantMethod string wantMethod string
wantSticky bool wantStickiness *types.Stickiness
}{ }{
{ {
desc: "valid load balancer method with sticky enabled", desc: "valid load balancer method with sticky enabled",
lb: &types.LoadBalancer{ lb: &types.LoadBalancer{
Method: validMethod, Method: validMethod,
Sticky: true, Stickiness: &types.Stickiness{},
}, },
wantMethod: validMethod, wantMethod: validMethod,
wantSticky: true, wantStickiness: &types.Stickiness{},
}, },
{ {
desc: "valid load balancer method with sticky disabled", desc: "valid load balancer method with sticky disabled",
lb: &types.LoadBalancer{ lb: &types.LoadBalancer{
Method: validMethod, Method: validMethod,
Sticky: false, Stickiness: nil,
}, },
wantMethod: validMethod, wantMethod: validMethod,
wantSticky: false,
}, },
{ {
desc: "invalid load balancer method with sticky enabled", desc: "invalid load balancer method with sticky enabled",
lb: &types.LoadBalancer{ lb: &types.LoadBalancer{
Method: "Invalid", Method: "Invalid",
Sticky: true, Stickiness: &types.Stickiness{},
}, },
wantMethod: defaultMethod, wantMethod: defaultMethod,
wantSticky: true, wantStickiness: &types.Stickiness{},
}, },
{ {
desc: "invalid load balancer method with sticky disabled", desc: "invalid load balancer method with sticky disabled",
lb: &types.LoadBalancer{ lb: &types.LoadBalancer{
Method: "Invalid", Method: "Invalid",
Sticky: false, Stickiness: nil,
}, },
wantMethod: defaultMethod, wantMethod: defaultMethod,
wantSticky: false,
}, },
{ {
desc: "missing load balancer", desc: "missing load balancer",
lb: nil, lb: nil,
wantMethod: defaultMethod, wantMethod: defaultMethod,
wantSticky: false,
}, },
} }
@ -485,8 +482,8 @@ func TestConfigureBackends(t *testing.T) {
}) })
wantLB := types.LoadBalancer{ wantLB := types.LoadBalancer{
Method: test.wantMethod, Method: test.wantMethod,
Sticky: test.wantSticky, Stickiness: test.wantStickiness,
} }
if !reflect.DeepEqual(*backend.LoadBalancer, wantLB) { if !reflect.DeepEqual(*backend.LoadBalancer, wantLB) {
t.Errorf("got backend load-balancer\n%v\nwant\n%v\n", spew.Sdump(backend.LoadBalancer), spew.Sdump(wantLB)) t.Errorf("got backend load-balancer\n%v\nwant\n%v\n", spew.Sdump(backend.LoadBalancer), spew.Sdump(wantLB))
@ -539,7 +536,7 @@ func TestServerEntrypointWhitelistConfig(t *testing.T) {
handler := srvEntryPoint.httpServer.Handler.(*negroni.Negroni) handler := srvEntryPoint.httpServer.Handler.(*negroni.Negroni)
found := false found := false
for _, handler := range handler.Handlers() { for _, handler := range handler.Handlers() {
if reflect.TypeOf(handler) == reflect.TypeOf((*middlewares.IPWhitelister)(nil)) { if reflect.TypeOf(handler) == reflect.TypeOf((*middlewares.IPWhiteLister)(nil)) {
found = true found = true
} }
} }
@ -719,6 +716,10 @@ func withServer(name, url string) func(backend *types.Backend) {
func withLoadBalancer(method string, sticky bool) func(*types.Backend) { func withLoadBalancer(method string, sticky bool) func(*types.Backend) {
return func(be *types.Backend) { return func(be *types.Backend) {
be.LoadBalancer = &types.LoadBalancer{Method: method, Sticky: sticky} if sticky {
be.LoadBalancer = &types.LoadBalancer{Method: method, Stickiness: &types.Stickiness{CookieName: "test"}}
} else {
be.LoadBalancer = &types.LoadBalancer{Method: method}
}
} }
} }

View file

@ -17,8 +17,12 @@
{{end}} {{end}}
[backends."backend-{{$service}}".loadbalancer] [backends."backend-{{$service}}".loadbalancer]
sticky = {{getAttribute "backend.loadbalancer.sticky" .Attributes "false"}}
method = "{{getAttribute "backend.loadbalancer" .Attributes "wrr"}}" method = "{{getAttribute "backend.loadbalancer" .Attributes "wrr"}}"
sticky = {{getAttribute "backend.loadbalancer.sticky" .Attributes "false"}}
{{if hasStickinessLabel .Attributes}}
[Backends."backend-{{$service}}".LoadBalancer.Stickiness]
cookieName = {{getStickinessCookieName .Attributes}}
{{end}}
{{if hasMaxconnAttributes .Attributes}} {{if hasMaxconnAttributes .Attributes}}
[backends."backend-{{$service}}".maxconn] [backends."backend-{{$service}}".maxconn]

View file

@ -8,7 +8,10 @@
{{if hasLoadBalancerLabel $backend}} {{if hasLoadBalancerLabel $backend}}
[backends.backend-{{$backendName}}.loadbalancer] [backends.backend-{{$backendName}}.loadbalancer]
method = "{{getLoadBalancerMethod $backend}}" method = "{{getLoadBalancerMethod $backend}}"
sticky = {{getSticky $backend}} {{if hasStickinessLabel $backend}}
[Backends."{{$backendName}}".LoadBalancer.Stickiness]
cookieName = {{getStickinessCookieName $backend}}
{{end}}
{{end}} {{end}}
{{if hasMaxConnLabels $backend}} {{if hasMaxConnLabels $backend}}

View file

@ -1,7 +1,10 @@
[backends]{{range $serviceName, $instances := .Services}} [backends]{{range $serviceName, $instances := .Services}}
[backends.backend-{{ $serviceName }}.loadbalancer] [backends.backend-{{ $serviceName }}.loadbalancer]
sticky = {{ getLoadBalancerSticky $instances}}
method = "{{ getLoadBalancerMethod $instances}}" method = "{{ getLoadBalancerMethod $instances}}"
{{if hasStickinessLabel $instances}}
[Backends.backend-{{ $serviceName }}.LoadBalancer.Stickiness]
cookieName = {{getStickinessCookieName $instances}}
{{end}}
{{range $index, $i := $instances}} {{range $index, $i := $instances}}
[backends.backend-{{ $i.Name }}.servers.server-{{ $i.Name }}{{ $i.ID }}] [backends.backend-{{ $i.Name }}.servers.server-{{ $i.Name }}{{ $i.ID }}]

View file

@ -6,8 +6,9 @@
{{end}} {{end}}
[backends."{{$backendName}}".loadbalancer] [backends."{{$backendName}}".loadbalancer]
method = "{{$backend.LoadBalancer.Method}}" method = "{{$backend.LoadBalancer.Method}}"
{{if $backend.LoadBalancer.Sticky}} {{if $backend.LoadBalancer.Stickiness}}
sticky = true [Backends."{{$backendName}}".LoadBalancer.Stickiness]
cookieName = {{$backend.LoadBalancer.Stickiness.CookieName}}
{{end}} {{end}}
{{range $serverName, $server := $backend.Servers}} {{range $serverName, $server := $backend.Servers}}
[backends."{{$backendName}}".servers."{{$serverName}}"] [backends."{{$backendName}}".servers."{{$serverName}}"]

View file

@ -3,25 +3,29 @@
[backends]{{range $backends}} [backends]{{range $backends}}
{{$backend := .}} {{$backend := .}}
{{$backendName := Last $backend}}
{{$servers := ListServers $backend }} {{$servers := ListServers $backend }}
{{$circuitBreaker := Get "" . "/circuitbreaker/" "expression"}} {{$circuitBreaker := Get "" . "/circuitbreaker/" "expression"}}
{{with $circuitBreaker}} {{with $circuitBreaker}}
[backends."{{Last $backend}}".circuitBreaker] [backends."{{$backendName}}".circuitBreaker]
expression = "{{$circuitBreaker}}" expression = "{{$circuitBreaker}}"
{{end}} {{end}}
{{$loadBalancer := Get "" . "/loadbalancer/" "method"}} {{$loadBalancer := Get "" . "/loadbalancer/" "method"}}
{{$sticky := Get "false" . "/loadbalancer/" "sticky"}}
{{with $loadBalancer}} {{with $loadBalancer}}
[backends."{{Last $backend}}".loadBalancer] [backends."{{$backendName}}".loadBalancer]
method = "{{$loadBalancer}}" method = "{{$loadBalancer}}"
sticky = {{$sticky}} sticky = {{ Get "false" . "/loadbalancer/" "sticky" }}
{{if hasStickinessLabel $backend}}
[Backends."{{$backendName}}".LoadBalancer.Stickiness]
cookieName = {{getStickinessCookieName $backend}}
{{end}}
{{end}} {{end}}
{{$healthCheck := Get "" . "/healthcheck/" "path"}} {{$healthCheck := Get "" . "/healthcheck/" "path"}}
{{with $healthCheck}} {{with $healthCheck}}
[backends."{{Last $backend}}".healthCheck] [backends."{{$backendName}}".healthCheck]
path = "{{$healthCheck}}" path = "{{$healthCheck}}"
interval = "{{ Get "30s" $backend "/healthcheck/" "interval" }}" interval = "{{ Get "30s" $backend "/healthcheck/" "interval" }}"
{{end}} {{end}}
@ -30,14 +34,14 @@
{{$maxConnExtractorFunc := Get "" . "/maxconn/" "extractorfunc"}} {{$maxConnExtractorFunc := Get "" . "/maxconn/" "extractorfunc"}}
{{with $maxConnAmt}} {{with $maxConnAmt}}
{{with $maxConnExtractorFunc}} {{with $maxConnExtractorFunc}}
[backends."{{Last $backend}}".maxConn] [backends."{{$backendName}}".maxConn]
amount = {{$maxConnAmt}} amount = {{$maxConnAmt}}
extractorFunc = "{{$maxConnExtractorFunc}}" extractorFunc = "{{$maxConnExtractorFunc}}"
{{end}} {{end}}
{{end}} {{end}}
{{range $servers}} {{range $servers}}
[backends."{{Last $backend}}".servers."{{Last .}}"] [backends."{{$backendName}}".servers."{{Last .}}"]
url = "{{Get "" . "/url"}}" url = "{{Get "" . "/url"}}"
weight = {{Get "0" . "/weight"}} weight = {{Get "0" . "/weight"}}
{{end}} {{end}}

View file

@ -20,7 +20,10 @@
{{ if hasLoadBalancerLabels $app }} {{ if hasLoadBalancerLabels $app }}
[backends."backend{{getBackend $app $serviceName }}".loadbalancer] [backends."backend{{getBackend $app $serviceName }}".loadbalancer]
method = "{{getLoadBalancerMethod $app }}" method = "{{getLoadBalancerMethod $app }}"
sticky = {{getSticky $app}} {{if hasStickinessLabel $app}}
[Backends."backend{{getBackend $app $serviceName }}".LoadBalancer.Stickiness]
cookieName = {{getStickinessCookieName $app}}
{{end}}
{{end}} {{end}}
{{ if hasCircuitBreakerLabels $app }} {{ if hasCircuitBreakerLabels $app }}
[backends."backend{{getBackend $app $serviceName }}".circuitbreaker] [backends."backend{{getBackend $app $serviceName }}".circuitbreaker]

View file

@ -8,7 +8,10 @@
{{if hasLoadBalancerLabel $backend}} {{if hasLoadBalancerLabel $backend}}
[backends.backend-{{$backendName}}.loadbalancer] [backends.backend-{{$backendName}}.loadbalancer]
method = "{{getLoadBalancerMethod $backend}}" method = "{{getLoadBalancerMethod $backend}}"
sticky = {{getSticky $backend}} {{if hasStickinessLabel $backend}}
[Backends."{{$backendName}}".LoadBalancer.Stickiness]
cookieName = {{getStickinessCookieName $backend}}
{{end}}
{{end}} {{end}}
{{if hasMaxConnLabels $backend}} {{if hasMaxConnLabels $backend}}

View file

@ -2,59 +2,36 @@ package types
import "strings" import "strings"
// Traefik labels
const ( const (
// LabelPrefix Traefik label LabelPrefix = "traefik."
LabelPrefix = "traefik." LabelDomain = LabelPrefix + "domain"
// LabelDomain Traefik label LabelEnable = LabelPrefix + "enable"
LabelDomain = LabelPrefix + "domain" LabelPort = LabelPrefix + "port"
// LabelEnable Traefik label LabelPortIndex = LabelPrefix + "portIndex"
LabelEnable = LabelPrefix + "enable" LabelProtocol = LabelPrefix + "protocol"
// LabelPort Traefik label LabelTags = LabelPrefix + "tags"
LabelPort = LabelPrefix + "port" LabelWeight = LabelPrefix + "weight"
// LabelPortIndex Traefik label LabelFrontendAuthBasic = LabelPrefix + "frontend.auth.basic"
LabelPortIndex = LabelPrefix + "portIndex" LabelFrontendEntryPoints = LabelPrefix + "frontend.entryPoints"
// LabelProtocol Traefik label LabelFrontendPassHostHeader = LabelPrefix + "frontend.passHostHeader"
LabelProtocol = LabelPrefix + "protocol" LabelFrontendPriority = LabelPrefix + "frontend.priority"
// LabelTags Traefik label LabelFrontendRule = LabelPrefix + "frontend.rule"
LabelTags = LabelPrefix + "tags" LabelFrontendRuleType = LabelPrefix + "frontend.rule.type"
// LabelWeight Traefik label LabelTraefikFrontendValue = LabelPrefix + "frontend.value"
LabelWeight = LabelPrefix + "weight" LabelTraefikFrontendWhitelistSourceRange = LabelPrefix + "frontend.whitelistSourceRange"
// LabelFrontendAuthBasic Traefik label LabelBackend = LabelPrefix + "backend"
LabelFrontendAuthBasic = LabelPrefix + "frontend.auth.basic" LabelBackendID = LabelPrefix + "backend.id"
// LabelFrontendEntryPoints Traefik label LabelTraefikBackendCircuitbreaker = LabelPrefix + "backend.circuitbreaker"
LabelFrontendEntryPoints = LabelPrefix + "frontend.entryPoints" LabelBackendCircuitbreakerExpression = LabelPrefix + "backend.circuitbreaker.expression"
// LabelFrontendPassHostHeader Traefik label LabelBackendHealthcheckPath = LabelPrefix + "backend.healthcheck.path"
LabelFrontendPassHostHeader = LabelPrefix + "frontend.passHostHeader" LabelBackendHealthcheckInterval = LabelPrefix + "backend.healthcheck.interval"
// LabelFrontendPriority Traefik label LabelBackendLoadbalancerMethod = LabelPrefix + "backend.loadbalancer.method"
LabelFrontendPriority = LabelPrefix + "frontend.priority" LabelBackendLoadbalancerSticky = LabelPrefix + "backend.loadbalancer.sticky"
// LabelFrontendRule Traefik label LabelBackendLoadbalancerStickiness = LabelPrefix + "backend.loadbalancer.stickiness"
LabelFrontendRule = LabelPrefix + "frontend.rule" LabelBackendLoadbalancerStickinessCookieName = LabelPrefix + "backend.loadbalancer.stickiness.cookieName"
// LabelFrontendRuleType Traefik label LabelBackendMaxconnAmount = LabelPrefix + "backend.maxconn.amount"
LabelFrontendRuleType = LabelPrefix + "frontend.rule.type" LabelBackendMaxconnExtractorfunc = LabelPrefix + "backend.maxconn.extractorfunc"
// LabelTraefikFrontendValue Traefik label
LabelTraefikFrontendValue = LabelPrefix + "frontend.value"
// LabelTraefikFrontendWhitelistSourceRange Traefik label
LabelTraefikFrontendWhitelistSourceRange = LabelPrefix + "frontend.whitelistSourceRange"
// LabelBackend Traefik label
LabelBackend = LabelPrefix + "backend"
// LabelBackendID Traefik label
LabelBackendID = LabelPrefix + "backend.id"
// LabelTraefikBackendCircuitbreaker Traefik label
LabelTraefikBackendCircuitbreaker = LabelPrefix + "backend.circuitbreaker"
// LabelBackendCircuitbreakerExpression Traefik label
LabelBackendCircuitbreakerExpression = LabelPrefix + "backend.circuitbreaker.expression"
// LabelBackendHealthcheckPath Traefik label
LabelBackendHealthcheckPath = LabelPrefix + "backend.healthcheck.path"
// LabelBackendHealthcheckInterval Traefik label
LabelBackendHealthcheckInterval = LabelPrefix + "backend.healthcheck.interval"
// LabelBackendLoadbalancerMethod Traefik label
LabelBackendLoadbalancerMethod = LabelPrefix + "backend.loadbalancer.method"
// LabelBackendLoadbalancerSticky Traefik label
LabelBackendLoadbalancerSticky = LabelPrefix + "backend.loadbalancer.sticky"
// LabelBackendMaxconnAmount Traefik label
LabelBackendMaxconnAmount = LabelPrefix + "backend.maxconn.amount"
// LabelBackendMaxconnExtractorfunc Traefik label
LabelBackendMaxconnExtractorfunc = LabelPrefix + "backend.maxconn.extractorfunc"
) )
//ServiceLabel converts a key value of Label*, given a serviceName, into a pattern <LabelPrefix>.<serviceName>.<property> //ServiceLabel converts a key value of Label*, given a serviceName, into a pattern <LabelPrefix>.<serviceName>.<property>

View file

@ -34,8 +34,14 @@ type MaxConn struct {
// LoadBalancer holds load balancing configuration. // LoadBalancer holds load balancing configuration.
type LoadBalancer struct { type LoadBalancer struct {
Method string `json:"method,omitempty"` Method string `json:"method,omitempty"`
Sticky bool `json:"sticky,omitempty"` Sticky bool `json:"sticky,omitempty"` // Deprecated: use Stickiness instead
Stickiness *Stickiness `json:"stickiness,omitempty"`
}
// Stickiness holds sticky session configuration.
type Stickiness struct {
CookieName string `json:"cookieName,omitempty"`
} }
// CircuitBreaker holds circuit breaker configuration. // CircuitBreaker holds circuit breaker configuration.

75
whitelist/ip.go Normal file
View file

@ -0,0 +1,75 @@
package whitelist
import (
"fmt"
"net"
"github.com/pkg/errors"
)
// IP allows to check that addresses are in a white list
type IP struct {
whiteListsIPs []*net.IP
whiteListsNet []*net.IPNet
}
// NewIP builds a new IP given a list of CIDR-Strings to whitelist
func NewIP(whitelistStrings []string) (*IP, error) {
if len(whitelistStrings) == 0 {
return nil, errors.New("no whiteListsNet provided")
}
ip := IP{}
for _, whitelistString := range whitelistStrings {
ipAddr := net.ParseIP(whitelistString)
if ipAddr != nil {
ip.whiteListsIPs = append(ip.whiteListsIPs, &ipAddr)
} else {
_, whitelist, err := net.ParseCIDR(whitelistString)
if err != nil {
return nil, fmt.Errorf("parsing CIDR whitelist %s: %v", whitelist, err)
}
ip.whiteListsNet = append(ip.whiteListsNet, whitelist)
}
}
return &ip, nil
}
// Contains checks if provided address is in the white list
func (ip *IP) Contains(addr string) (bool, net.IP, error) {
ipAddr, err := ipFromRemoteAddr(addr)
if err != nil {
return false, nil, fmt.Errorf("unable to parse address: %s: %s", addr, err)
}
contains, err := ip.ContainsIP(ipAddr)
return contains, ipAddr, err
}
// ContainsIP checks if provided address is in the white list
func (ip *IP) ContainsIP(addr net.IP) (bool, error) {
for _, whiteListIP := range ip.whiteListsIPs {
if whiteListIP.Equal(addr) {
return true, nil
}
}
for _, whiteListNet := range ip.whiteListsNet {
if whiteListNet.Contains(addr) {
return true, nil
}
}
return false, nil
}
func ipFromRemoteAddr(addr string) (net.IP, error) {
userIP := net.ParseIP(addr)
if userIP == nil {
return nil, fmt.Errorf("can't parse IP from address %s", addr)
}
return userIP, nil
}

View file

@ -1,19 +1,14 @@
package middlewares package whitelist
import ( import (
"fmt"
"net" "net"
"net/http"
"net/http/httptest"
"testing" "testing"
"github.com/containous/traefik/testhelpers"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/urfave/negroni"
) )
func TestNewIPWhitelister(t *testing.T) { func TestNew(t *testing.T) {
cases := []struct { cases := []struct {
desc string desc string
whitelistStrings []string whitelistStrings []string
@ -24,12 +19,12 @@ func TestNewIPWhitelister(t *testing.T) {
desc: "nil whitelist", desc: "nil whitelist",
whitelistStrings: nil, whitelistStrings: nil,
expectedWhitelists: nil, expectedWhitelists: nil,
errMessage: "no whitelists provided", errMessage: "no whiteListsNet provided",
}, { }, {
desc: "empty whitelist", desc: "empty whitelist",
whitelistStrings: []string{}, whitelistStrings: []string{},
expectedWhitelists: nil, expectedWhitelists: nil,
errMessage: "no whitelists provided", errMessage: "no whiteListsNet provided",
}, { }, {
desc: "whitelist containing empty string", desc: "whitelist containing empty string",
whitelistStrings: []string{ whitelistStrings: []string{
@ -80,12 +75,12 @@ func TestNewIPWhitelister(t *testing.T) {
test := test test := test
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
whitelister, err := NewIPWhitelister(test.whitelistStrings) whitelister, err := NewIP(test.whitelistStrings)
if test.errMessage != "" { if test.errMessage != "" {
require.EqualError(t, err, test.errMessage) require.EqualError(t, err, test.errMessage)
} else { } else {
require.NoError(t, err) require.NoError(t, err)
for index, actual := range whitelister.whitelists { for index, actual := range whitelister.whiteListsNet {
expected := test.expectedWhitelists[index] expected := test.expectedWhitelists[index]
assert.Equal(t, expected.IP, actual.IP) assert.Equal(t, expected.IP, actual.IP)
assert.Equal(t, expected.Mask.String(), actual.Mask.String()) assert.Equal(t, expected.Mask.String(), actual.Mask.String())
@ -95,7 +90,7 @@ func TestNewIPWhitelister(t *testing.T) {
} }
} }
func TestIPWhitelisterHandle(t *testing.T) { func TestIsAllowed(t *testing.T) {
cases := []struct { cases := []struct {
desc string desc string
whitelistStrings []string whitelistStrings []string
@ -122,6 +117,23 @@ func TestIPWhitelisterHandle(t *testing.T) {
}, },
{ {
desc: "IPv4 single IP", desc: "IPv4 single IP",
whitelistStrings: []string{
"8.8.8.8",
},
passIPs: []string{
"8.8.8.8",
},
rejectIPs: []string{
"8.8.8.7",
"8.8.8.9",
"8.8.8.0",
"8.8.8.255",
"4.4.4.4",
"127.0.0.1",
},
},
{
desc: "IPv4 Net single IP",
whitelistStrings: []string{ whitelistStrings: []string{
"8.8.8.8/32", "8.8.8.8/32",
}, },
@ -167,16 +179,16 @@ func TestIPWhitelisterHandle(t *testing.T) {
"2a03:4000:6:d080::/64", "2a03:4000:6:d080::/64",
}, },
passIPs: []string{ passIPs: []string{
"[2a03:4000:6:d080::]", "2a03:4000:6:d080::",
"[2a03:4000:6:d080::1]", "2a03:4000:6:d080::1",
"[2a03:4000:6:d080:dead:beef:ffff:ffff]", "2a03:4000:6:d080:dead:beef:ffff:ffff",
"[2a03:4000:6:d080::42]", "2a03:4000:6:d080::42",
}, },
rejectIPs: []string{ rejectIPs: []string{
"[2a03:4000:7:d080::]", "2a03:4000:7:d080::",
"[2a03:4000:7:d080::1]", "2a03:4000:7:d080::1",
"[fe80::]", "fe80::",
"[4242::1]", "4242::1",
}, },
}, },
{ {
@ -185,12 +197,12 @@ func TestIPWhitelisterHandle(t *testing.T) {
"2a03:4000:6:d080::42/128", "2a03:4000:6:d080::42/128",
}, },
passIPs: []string{ passIPs: []string{
"[2a03:4000:6:d080::42]", "2a03:4000:6:d080::42",
}, },
rejectIPs: []string{ rejectIPs: []string{
"[2a03:4000:6:d080::1]", "2a03:4000:6:d080::1",
"[2a03:4000:6:d080:dead:beef:ffff:ffff]", "2a03:4000:6:d080:dead:beef:ffff:ffff",
"[2a03:4000:6:d080::43]", "2a03:4000:6:d080::43",
}, },
}, },
{ {
@ -200,18 +212,18 @@ func TestIPWhitelisterHandle(t *testing.T) {
"fe80::/16", "fe80::/16",
}, },
passIPs: []string{ passIPs: []string{
"[2a03:4000:6:d080::]", "2a03:4000:6:d080::",
"[2a03:4000:6:d080::1]", "2a03:4000:6:d080::1",
"[2a03:4000:6:d080:dead:beef:ffff:ffff]", "2a03:4000:6:d080:dead:beef:ffff:ffff",
"[2a03:4000:6:d080::42]", "2a03:4000:6:d080::42",
"[fe80::1]", "fe80::1",
"[fe80:aa00:00bb:4232:ff00:eeee:00ff:1111]", "fe80:aa00:00bb:4232:ff00:eeee:00ff:1111",
"[fe80::fe80]", "fe80::fe80",
}, },
rejectIPs: []string{ rejectIPs: []string{
"[2a03:4000:7:d080::]", "2a03:4000:7:d080::",
"[2a03:4000:7:d080::1]", "2a03:4000:7:d080::1",
"[4242::1]", "4242::1",
}, },
}, },
{ {
@ -223,13 +235,13 @@ func TestIPWhitelisterHandle(t *testing.T) {
"8.8.8.8/8", "8.8.8.8/8",
}, },
passIPs: []string{ passIPs: []string{
"[2a03:4000:6:d080::]", "2a03:4000:6:d080::",
"[2a03:4000:6:d080::1]", "2a03:4000:6:d080::1",
"[2a03:4000:6:d080:dead:beef:ffff:ffff]", "2a03:4000:6:d080:dead:beef:ffff:ffff",
"[2a03:4000:6:d080::42]", "2a03:4000:6:d080::42",
"[fe80::1]", "fe80::1",
"[fe80:aa00:00bb:4232:ff00:eeee:00ff:1111]", "fe80:aa00:00bb:4232:ff00:eeee:00ff:1111",
"[fe80::fe80]", "fe80::fe80",
"1.2.3.1", "1.2.3.1",
"1.2.3.32", "1.2.3.32",
"1.2.3.156", "1.2.3.156",
@ -240,9 +252,9 @@ func TestIPWhitelisterHandle(t *testing.T) {
"8.255.255.255", "8.255.255.255",
}, },
rejectIPs: []string{ rejectIPs: []string{
"[2a03:4000:7:d080::]", "2a03:4000:7:d080::",
"[2a03:4000:7:d080::1]", "2a03:4000:7:d080::1",
"[4242::1]", "4242::1",
"1.2.16.1", "1.2.16.1",
"1.2.32.1", "1.2.32.1",
"127.0.0.1", "127.0.0.1",
@ -256,13 +268,6 @@ func TestIPWhitelisterHandle(t *testing.T) {
"127.0.0.1/32", "127.0.0.1/32",
}, },
passIPs: nil, passIPs: nil,
rejectIPs: []string{
"foo",
"10.0.0.350",
"fe:::80",
"",
"\\&$§&/(",
},
}, },
} }
@ -270,38 +275,44 @@ func TestIPWhitelisterHandle(t *testing.T) {
test := test test := test
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
whitelister, err := NewIPWhitelister(test.whitelistStrings) whiteLister, err := NewIP(test.whitelistStrings)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, whitelister) require.NotNil(t, whiteLister)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "traefik")
})
n := negroni.New(whitelister)
n.UseHandler(handler)
for _, testIP := range test.passIPs { for _, testIP := range test.passIPs {
req := testhelpers.MustNewRequest(http.MethodGet, "/", nil) allowed, ip, err := whiteLister.Contains(testIP)
require.NoError(t, err)
req.RemoteAddr = testIP + ":2342" require.NotNil(t, ip, err)
recorder := httptest.NewRecorder() assert.True(t, allowed, testIP+" should have passed "+test.desc)
n.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusOK, recorder.Code, testIP+" should have passed "+test.desc)
assert.Contains(t, recorder.Body.String(), "traefik")
} }
for _, testIP := range test.rejectIPs { for _, testIP := range test.rejectIPs {
req := testhelpers.MustNewRequest(http.MethodGet, "/", nil) allowed, ip, err := whiteLister.Contains(testIP)
require.NoError(t, err)
req.RemoteAddr = testIP + ":2342" require.NotNil(t, ip, err)
recorder := httptest.NewRecorder() assert.False(t, allowed, testIP+" should not have passed "+test.desc)
n.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusForbidden, recorder.Code, testIP+" should not have passed "+test.desc)
assert.NotContains(t, recorder.Body.String(), "traefik")
} }
}) })
} }
} }
func TestBrokenIPs(t *testing.T) {
brokenIPs := []string{
"foo",
"10.0.0.350",
"fe:::80",
"",
"\\&$§&/(",
}
whiteLister, err := NewIP([]string{"1.2.3.4/24"})
require.NoError(t, err)
for _, testIP := range brokenIPs {
_, ip, err := whiteLister.Contains(testIP)
assert.Error(t, err)
require.Nil(t, ip, err)
}
}