Merge branch 'v1.5' into master

This commit is contained in:
Fernandez Ludovic 2017-12-15 22:16:48 +01:00
commit f6520727a3
44 changed files with 1035 additions and 463 deletions

View file

@ -43,7 +43,7 @@ If you want your users to access some of your microservices from the Internet, y
- path `domain.com/web` will point the microservice `web` in your private network - path `domain.com/web` will point the microservice `web` in your private network
- domain `backoffice.domain.com` will point the microservices `backoffice` in your private network, load-balancing between your multiple instances - domain `backoffice.domain.com` will point the microservices `backoffice` in your private network, load-balancing between your multiple instances
But a microservices architecture is dynamic... Services are added, removed, killed or upgraded often, eventually several times a day. Microservices are often deployed in dynamic environments where services are added, removed, killed, upgraded or scaled many times a day.
Traditional reverse-proxies are not natively dynamic. You can't change their configuration and hot-reload easily. Traditional reverse-proxies are not natively dynamic. You can't change their configuration and hot-reload easily.

View file

@ -172,7 +172,6 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}}
[frontends."frontend-{{getServiceBackend $container $serviceName}}"] [frontends."frontend-{{getServiceBackend $container $serviceName}}"]
backend = "backend-{{getServiceBackend $container $serviceName}}" backend = "backend-{{getServiceBackend $container $serviceName}}"
passHostHeader = {{getServicePassHostHeader $container $serviceName}} passHostHeader = {{getServicePassHostHeader $container $serviceName}}
redirect = "{{getServiceRedirect $container $serviceName}}"
{{if getWhitelistSourceRange $container}} {{if getWhitelistSourceRange $container}}
whitelistSourceRange = [{{range getWhitelistSourceRange $container}} whitelistSourceRange = [{{range getWhitelistSourceRange $container}}
"{{.}}", "{{.}}",
@ -185,6 +184,14 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}}
basicAuth = [{{range getServiceBasicAuth $container $serviceName}} basicAuth = [{{range getServiceBasicAuth $container $serviceName}}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{if hasServiceRedirect $container $serviceName}}
[frontends."frontend-{{getServiceBackend $container $serviceName}}".redirect]
entryPoint = "{{getServiceRedirectEntryPoint $container $serviceName}}"
regex = "{{getServiceRedirectRegex $container $serviceName}}"
replacement = "{{getServiceRedirectReplacement $container $serviceName}}"
{{end}}
[frontends."frontend-{{getServiceBackend $container $serviceName}}".routes."service-{{$serviceName | replace "/" "" | replace "." "-"}}"] [frontends."frontend-{{getServiceBackend $container $serviceName}}".routes."service-{{$serviceName | replace "/" "" | replace "." "-"}}"]
rule = "{{getServiceFrontendRule $container $serviceName}}" rule = "{{getServiceFrontendRule $container $serviceName}}"
{{if hasServiceRequestHeaders $container $serviceName}} {{if hasServiceRequestHeaders $container $serviceName}}
@ -204,7 +211,6 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}}
[frontends."frontend-{{$frontend}}"] [frontends."frontend-{{$frontend}}"]
backend = "backend-{{getBackend $container}}" backend = "backend-{{getBackend $container}}"
passHostHeader = {{getPassHostHeader $container}} passHostHeader = {{getPassHostHeader $container}}
redirect = "{{getRedirect $container}}"
{{if getWhitelistSourceRange $container}} {{if getWhitelistSourceRange $container}}
whitelistSourceRange = [{{range getWhitelistSourceRange $container}} whitelistSourceRange = [{{range getWhitelistSourceRange $container}}
"{{.}}", "{{.}}",
@ -217,6 +223,14 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}}
basicAuth = [{{range getBasicAuth $container}} basicAuth = [{{range getBasicAuth $container}}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{if hasRedirect $container}}
[frontends."frontend-{{$frontend}}".redirect]
entryPoint = "{{getRedirectEntryPoint $container}}"
regex = "{{getRedirectRegex $container}}"
replacement = "{{getRedirectReplacement $container}}"
{{end}}
[frontends."frontend-{{$frontend}}".headers] [frontends."frontend-{{$frontend}}".headers]
{{if hasSSLRedirectHeaders $container}} {{if hasSSLRedirectHeaders $container}}
SSLRedirect = {{getSSLRedirectHeaders $container}} SSLRedirect = {{getSSLRedirectHeaders $container}}
@ -426,13 +440,20 @@ var _templatesKubernetesTmpl = []byte(`[backends]{{range $backendName, $backend
backend = "{{$frontend.Backend}}" backend = "{{$frontend.Backend}}"
priority = {{$frontend.Priority}} priority = {{$frontend.Priority}}
passHostHeader = {{$frontend.PassHostHeader}} passHostHeader = {{$frontend.PassHostHeader}}
redirect = "{{$frontend.Redirect}}"
basicAuth = [{{range $frontend.BasicAuth}} basicAuth = [{{range $frontend.BasicAuth}}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
whitelistSourceRange = [{{range $frontend.WhitelistSourceRange}} whitelistSourceRange = [{{range $frontend.WhitelistSourceRange}}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{if $frontend.Redirect}}
[frontends."{{$frontendName}}".redirect]
entryPoint = "{{$frontend.RedirectEntryPoint}}"
regex = "{{$frontend.RedirectRegex}}"
replacement = "{{$frontend.RedirectReplacement}}"
{{end}}
[frontends."{{$frontendName}}".headers] [frontends."{{$frontendName}}".headers]
SSLRedirect = {{$frontend.Headers.SSLRedirect}} SSLRedirect = {{$frontend.Headers.SSLRedirect}}
SSLTemporaryRedirect = {{$frontend.Headers.SSLTemporaryRedirect}} SSLTemporaryRedirect = {{$frontend.Headers.SSLTemporaryRedirect}}
@ -764,13 +785,20 @@ var _templatesRancherTmpl = []byte(`{{$backendServers := .Backends}}
backend = "backend-{{getBackend $service}}" backend = "backend-{{getBackend $service}}"
passHostHeader = {{getPassHostHeader $service}} passHostHeader = {{getPassHostHeader $service}}
priority = {{getPriority $service}} priority = {{getPriority $service}}
redirect = "{{getRedirect $service}}"
entryPoints = [{{range getEntryPoints $service}} entryPoints = [{{range getEntryPoints $service}}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
basicAuth = [{{range getBasicAuth $service}} basicAuth = [{{range getBasicAuth $service}}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{if hasRedirect $service}}
[frontends."frontend-{{$frontendName}}".redirect]
entryPoint = "{{getRedirectEntryPoint $service}}"
regex = "{{getRedirectRegex $service}}"
replacement = "{{getRedirectReplacement $service}}"
{{end}}
[frontends."frontend-{{$frontendName}}".routes."route-frontend-{{$frontendName}}"] [frontends."frontend-{{$frontendName}}".routes."route-frontend-{{$frontendName}}"]
rule = "{{getFrontendRule $service}}" rule = "{{getFrontendRule $service}}"
{{end}} {{end}}

View file

@ -57,7 +57,7 @@ func TestDo_globalConfiguration(t *testing.T) {
Optional: false, Optional: false,
}, },
}, },
Redirect: &configuration.Redirect{ Redirect: &types.Redirect{
Replacement: "foo Replacement", Replacement: "foo Replacement",
Regex: "foo Regex", Regex: "foo Regex",
EntryPoint: "foo EntryPoint", EntryPoint: "foo EntryPoint",
@ -103,7 +103,7 @@ func TestDo_globalConfiguration(t *testing.T) {
Optional: false, Optional: false,
}, },
}, },
Redirect: &configuration.Redirect{ Redirect: &types.Redirect{
Replacement: "fii Replacement", Replacement: "fii Replacement",
Regex: "fii Regex", Regex: "fii Regex",
EntryPoint: "fii EntryPoint", EntryPoint: "fii EntryPoint",

View file

@ -317,9 +317,9 @@ func (ep *EntryPoints) Set(value string) error {
Optional: optional, Optional: optional,
} }
} }
var redirect *Redirect var redirect *types.Redirect
if len(result["redirect_entrypoint"]) > 0 || len(result["redirect_regex"]) > 0 || len(result["redirect_replacement"]) > 0 { if len(result["redirect_entrypoint"]) > 0 || len(result["redirect_regex"]) > 0 || len(result["redirect_replacement"]) > 0 {
redirect = &Redirect{ redirect = &types.Redirect{
EntryPoint: result["redirect_entrypoint"], EntryPoint: result["redirect_entrypoint"],
Regex: result["redirect_regex"], Regex: result["redirect_regex"],
Replacement: result["redirect_replacement"], Replacement: result["redirect_replacement"],
@ -423,7 +423,7 @@ type EntryPoint struct {
Network string Network string
Address string Address string
TLS *tls.TLS `export:"true"` TLS *tls.TLS `export:"true"`
Redirect *Redirect `export:"true"` Redirect *types.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"`
@ -431,13 +431,6 @@ type EntryPoint struct {
ForwardedHeaders *ForwardedHeaders `export:"true"` ForwardedHeaders *ForwardedHeaders `export:"true"`
} }
// Redirect configures a redirection of an entry point to another, or to an URL
type Redirect struct {
EntryPoint string
Regex string
Replacement string
}
// Retry contains request retry config // Retry contains request retry config
type Retry struct { type Retry struct {
Attempts int `description:"Number of attempts" export:"true"` Attempts int `description:"Number of attempts" export:"true"`

View file

@ -8,6 +8,7 @@ import (
"github.com/containous/traefik/provider" "github.com/containous/traefik/provider"
"github.com/containous/traefik/provider/file" "github.com/containous/traefik/provider/file"
"github.com/containous/traefik/tls" "github.com/containous/traefik/tls"
"github.com/containous/traefik/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -138,7 +139,7 @@ func TestEntryPoints_Set(t *testing.T) {
expectedEntryPointName: "foo", expectedEntryPointName: "foo",
expectedEntryPoint: &EntryPoint{ expectedEntryPoint: &EntryPoint{
Address: ":8000", Address: ":8000",
Redirect: &Redirect{ Redirect: &types.Redirect{
EntryPoint: "RedirectEntryPoint", EntryPoint: "RedirectEntryPoint",
Regex: "RedirectRegex", Regex: "RedirectRegex",
Replacement: "RedirectReplacement", Replacement: "RedirectReplacement",
@ -171,7 +172,7 @@ func TestEntryPoints_Set(t *testing.T) {
expectedEntryPointName: "foo", expectedEntryPointName: "foo",
expectedEntryPoint: &EntryPoint{ expectedEntryPoint: &EntryPoint{
Address: ":8000", Address: ":8000",
Redirect: &Redirect{ Redirect: &types.Redirect{
EntryPoint: "RedirectEntryPoint", EntryPoint: "RedirectEntryPoint",
Regex: "RedirectRegex", Regex: "RedirectRegex",
Replacement: "RedirectReplacement", Replacement: "RedirectReplacement",

View file

@ -356,7 +356,7 @@ A backend is responsible to load-balance the traffic coming from one or more fro
Various methods of load-balancing are supported: Various methods of load-balancing are supported:
- `wrr`: Weighted Round Robin - `wrr`: Weighted Round Robin.
- `drr`: Dynamic Round Robin: increases weights on servers that perform better than others. - `drr`: Dynamic Round Robin: increases weights on servers that perform better than others.
It also rolls back to original weights if the servers have changed. It also rolls back to original weights if the servers have changed.
@ -373,16 +373,13 @@ It can be configured using:
For example: For example:
- `NetworkErrorRatio() > 0.5`: watch error ratio over 10 second sliding window for a frontend - `NetworkErrorRatio() > 0.5`: watch error ratio over 10 second sliding window for a frontend.
- `LatencyAtQuantileMS(50.0) > 50`: watch latency at quantile in milliseconds. - `LatencyAtQuantileMS(50.0) > 50`: watch latency at quantile in milliseconds.
- `ResponseCodeRatio(500, 600, 0, 600) > 0.5`: ratio of response codes in range [500-600) to [0-600) - `ResponseCodeRatio(500, 600, 0, 600) > 0.5`: ratio of response codes in ranges [500-600) and [0-600).
To proactively prevent backends from being overwhelmed with high load, a maximum connection limit can To proactively prevent backends from being overwhelmed with high load, a maximum connection limit can also be applied to each backend.
also be applied to each backend.
Maximum connections can be configured by specifying an integer value for `maxconn.amount` and Maximum connections can be configured by specifying an integer value for `maxconn.amount` and `maxconn.extractorfunc` which is a strategy used to determine how to categorize requests in order to evaluate the maximum connections.
`maxconn.extractorfunc` which is a strategy used to determine how to categorize requests in order to
evaluate the maximum connections.
For example: For example:
```toml ```toml
@ -499,8 +496,8 @@ Here is an example of backends and servers definition:
Træfik's configuration has two parts: Træfik's configuration has two parts:
- The [static Træfik configuration](/basics#static-trfk-configuration) which is loaded only at the beginning. - The [static Træfik configuration](/basics#static-trfik-configuration) which is loaded only at the beginning.
- The [dynamic Træfik configuration](/basics#dynamic-trfk-configuration) which can be hot-reloaded (no need to restart the process). - The [dynamic Træfik configuration](/basics#dynamic-trfik-configuration) which can be hot-reloaded (no need to restart the process).
### Static Træfik configuration ### Static Træfik configuration
@ -585,7 +582,7 @@ traefik [command] [--flag=flag_argument]
List of Træfik available commands with description : List of Træfik available commands with description :
- `version` : Print version - `version` : Print version
- `storeconfig` : Store the static Traefik configuration into a Key-value stores. Please refer to the [Store Træfik configuration](/user-guide/kv-config/#store-trfk-configuration) section to get documentation on it. - `storeconfig` : Store the static Traefik configuration into a Key-value stores. Please refer to the [Store Træfik configuration](/user-guide/kv-config/#store-configuration-in-key-value-store) section to get documentation on it.
- `bug`: The easiest way to submit a pre-filled issue. - `bug`: The easiest way to submit a pre-filled issue.
- `healthcheck`: Calls Traefik `/ping` to check health. - `healthcheck`: Calls Traefik `/ping` to check health.

View file

@ -135,7 +135,7 @@ Select the provider that matches the DNS domain that will host the challenge TXT
|--------------------------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------| |--------------------------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------|
| [Auroradns](https://www.pcextreme.com/aurora/dns) | `auroradns` | `AURORA_USER_ID`, `AURORA_KEY`, `AURORA_ENDPOINT` | | [Auroradns](https://www.pcextreme.com/aurora/dns) | `auroradns` | `AURORA_USER_ID`, `AURORA_KEY`, `AURORA_ENDPOINT` |
| [Azure](https://azure.microsoft.com/services/dns/) | `azure` | `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_SUBSCRIPTION_ID`, `AZURE_TENANT_ID`, `AZURE_RESOURCE_GROUP` | | [Azure](https://azure.microsoft.com/services/dns/) | `azure` | `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_SUBSCRIPTION_ID`, `AZURE_TENANT_ID`, `AZURE_RESOURCE_GROUP` |
| [Cloudflare](https://www.cloudflare.com) | `cloudflare` | `CLOUDFLARE_EMAIL`, `CLOUDFLARE_API_KEY` | | [Cloudflare](https://www.cloudflare.com) | `cloudflare` | `CLOUDFLARE_EMAIL`, `CLOUDFLARE_API_KEY` - The Cloudflare `Global API Key` needs to be used and not the `Origin CA Key` |
| [DigitalOcean](https://www.digitalocean.com) | `digitalocean` | `DO_AUTH_TOKEN` | | [DigitalOcean](https://www.digitalocean.com) | `digitalocean` | `DO_AUTH_TOKEN` |
| [DNSimple](https://dnsimple.com) | `dnsimple` | `DNSIMPLE_OAUTH_TOKEN`, `DNSIMPLE_BASE_URL` | | [DNSimple](https://dnsimple.com) | `dnsimple` | `DNSIMPLE_OAUTH_TOKEN`, `DNSIMPLE_BASE_URL` |
| [DNS Made Easy](https://dnsmadeeasy.com) | `dnsmadeeasy` | `DNSMADEEASY_API_KEY`, `DNSMADEEASY_API_SECRET`, `DNSMADEEASY_SANDBOX` | | [DNS Made Easy](https://dnsmadeeasy.com) | `dnsmadeeasy` | `DNSMADEEASY_API_KEY`, `DNSMADEEASY_API_SECRET`, `DNSMADEEASY_SANDBOX` |

View file

@ -88,6 +88,12 @@ endpoint = "127.0.0.1:8500"
# #
exposedByDefault = false exposedByDefault = false
# Default domain used.
#
# Optional
#
domain = "consul.localhost"
# Prefix for Consul catalog tags. # Prefix for Consul catalog tags.
# #
# Optional # Optional

View file

@ -150,7 +150,7 @@ 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. |
@ -171,7 +171,11 @@ Labels can be used on containers to override default behaviour.
| `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.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.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. | | `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.redirect=https` | Enables Redirect to another entryPoint for that frontend (e.g. HTTPS) | | `traefik.frontend.redirect.entryPoint=https` | Enables Redirect to another entryPoint for that frontend (e.g. HTTPS) |
| `traefik.frontend.redirect.regex=^http://localhost/(.*)` | Redirect to another URL for that frontend. Must be set with `traefik.frontend.redirect.replacement`. |
| `traefik.frontend.redirect.replacement=http://mydomain/$1` | Redirect to another URL for that frontend. Must be set with `traefik.frontend.redirect.regex`. |
#### Security Headers #### Security Headers
@ -203,7 +207,7 @@ Labels can be used on containers to override default behaviour.
Services labels can be used for overriding default behaviour Services labels can be used for overriding default behaviour
| Label | Description | | Label | Description |
|---------------------------------------------------|--------------------------------------------------------------------------------------------------| |---------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------|
| `traefik.<service-name>.port=PORT` | Overrides `traefik.port`. If several ports need to be exposed, the service labels could be used. | | `traefik.<service-name>.port=PORT` | Overrides `traefik.port`. If several ports need to be exposed, the service labels could be used. |
| `traefik.<service-name>.protocol` | Overrides `traefik.protocol`. | | `traefik.<service-name>.protocol` | Overrides `traefik.protocol`. |
| `traefik.<service-name>.weight` | Assign this service weight. Overrides `traefik.weight`. | | `traefik.<service-name>.weight` | Assign this service weight. Overrides `traefik.weight`. |
@ -214,6 +218,9 @@ Services labels can be used for overriding default behaviour
| `traefik.<service-name>.frontend.priority` | Overrides `traefik.frontend.priority`. | | `traefik.<service-name>.frontend.priority` | Overrides `traefik.frontend.priority`. |
| `traefik.<service-name>.frontend.rule` | Overrides `traefik.frontend.rule`. | | `traefik.<service-name>.frontend.rule` | Overrides `traefik.frontend.rule`. |
| `traefik.<service-name>.frontend.redirect` | Overrides `traefik.frontend.redirect`. | | `traefik.<service-name>.frontend.redirect` | Overrides `traefik.frontend.redirect`. |
| `traefik.<service-name>.frontend.redirect.entryPoint=https` | Overrides `traefik.frontend.redirect.entryPoint`. |
| `traefik.<service-name>.frontend.redirect.regex=^http://localhost/(.*)` | Overrides `traefik.frontend.redirect.regex`. |
| `traefik.<service-name>.frontend.redirect.replacement=http://mydomain/$1` | Overrides `traefik.frontend.redirect.replacement`. |
#### Security Headers #### Security Headers

View file

@ -102,13 +102,21 @@ Annotations can be used on containers to override default behaviour for the whol
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.
- `traefik.frontend.redirect: https`: - `traefik.frontend.redirect.entryPoint: https`:
Enables Redirect to another entryPoint for that frontend (e.g. HTTPS). Enables Redirect to another entryPoint for that frontend (e.g. HTTPS).
- `traefik.frontend.redirect.regex: ^http://localhost/(.*)`:
Redirect to another URL for that frontend. Must be set with `traefik.frontend.redirect.replacement`.
- `traefik.frontend.redirect.replacement: http://mydomain/$1`:
Redirect to another URL for that frontend. Must be set with `traefik.frontend.redirect.regex`.
- `traefik.frontend.entryPoints: http,https` - `traefik.frontend.entryPoints: http,https`
Override the default frontend endpoints. Override the default frontend endpoints.
- `traefik.frontend.passTLSCert: true` - `traefik.frontend.passTLSCert: true`
Override the default frontend PassTLSCert value. Default: `false`. Override the default frontend PassTLSCert value. Default: `false`.
!!! note
Please note that `traefik.frontend.redirect.regex` and `traefik.frontend.redirect.replacement` do not have to be set if `traefik.frontend.redirect.entryPoint` is defined for the redirection (they will not be used in this case).
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`

View file

@ -121,7 +121,7 @@ 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 |
@ -130,7 +130,9 @@ Labels can be used on task containers to override default behaviour:
| `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.frontend.redirect=https` | Enables Redirect to another entryPoint for that frontend (e.g. HTTPS) | | `traefik.frontend.redirect.entryPoint=https` | Enables Redirect to another entryPoint for that frontend (e.g. HTTPS) |
| `traefik.frontend.redirect.regex: ^http://localhost/(.*)` | Redirect to another URL for that frontend.<br>Must be set with `traefik.frontend.redirect.replacement`. |
| `traefik.frontend.redirect.replacement: http://mydomain/$1` | Redirect to another URL for that frontend.<br>Must be set with `traefik.frontend.redirect.regex`. |
| `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.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.stickiness=true` | Enable backend sticky sessions | | `traefik.backend.loadbalancer.stickiness=true` | Enable backend sticky sessions |

View file

@ -22,7 +22,7 @@ If you want your users to access some of your microservices from the Internet, y
- path `domain.com/web` will point the microservice `web` in your private network - path `domain.com/web` will point the microservice `web` in your private network
- domain `backoffice.domain.com` will point the microservices `backoffice` in your private network, load-balancing between your multiple instances - domain `backoffice.domain.com` will point the microservices `backoffice` in your private network, load-balancing between your multiple instances
But a microservices architecture is dynamic... Services are added, removed, killed or upgraded often, eventually several times a day. Microservices are often deployed in dynamic environments where services are added, removed, killed, upgraded or scaled many times a day.
Traditional reverse-proxies are not natively dynamic. You can't change their configuration and hot-reload easily. Traditional reverse-proxies are not natively dynamic. You can't change their configuration and hot-reload easily.
@ -129,7 +129,7 @@ Start it from within the `traefik` folder:
docker-compose up -d docker-compose up -d
``` ```
In a browser you may open [http://localhost:8080](http://localhost:8080) to access Træfik's dashboard and observe the following magic. In a browser, you may open [http://localhost:8080](http://localhost:8080) to access Træfik's dashboard and observe the following magic.
Now, create a folder named `test` and create a `docker-compose.yml` in it with this content: Now, create a folder named `test` and create a `docker-compose.yml` in it with this content:

8
glide.lock generated
View file

@ -1,5 +1,5 @@
hash: 8c5908b11f5078edd9ed93e2710ebb3a29b7e02d1259fddd679f8c46540becc9 hash: f0d5ef854a4c115306c63c15320b595c29f715950eaf5f18418149886ecda400
updated: 2017-11-30T10:34:41.246378337+01:00 updated: 2017-12-15T10:34:41.246378337+01:00
imports: imports:
- name: cloud.google.com/go - name: cloud.google.com/go
version: 2e6a95edb1071d750f6d7db777bf66cd2997af6c version: 2e6a95edb1071d750f6d7db777bf66cd2997af6c
@ -94,7 +94,7 @@ imports:
- name: github.com/containous/staert - name: github.com/containous/staert
version: af517d5b70db9c4b0505e0144fcc62b054057d2a version: af517d5b70db9c4b0505e0144fcc62b054057d2a
- name: github.com/containous/traefik-extra-service-fabric - name: github.com/containous/traefik-extra-service-fabric
version: 8076098dbfe814cba9e895ecbd896f1896b6b2d5 version: c01c1ef60ed612c5e42c1ceae0c6f92e67619cc3
- name: github.com/coreos/bbolt - name: github.com/coreos/bbolt
version: 3c6cbfb299c11444eb2f8c9d48f0d2ce09157423 version: 3c6cbfb299c11444eb2f8c9d48f0d2ce09157423
- name: github.com/coreos/etcd - name: github.com/coreos/etcd
@ -348,7 +348,7 @@ imports:
subpackages: subpackages:
- lib - lib
- name: github.com/jjcollinge/servicefabric - name: github.com/jjcollinge/servicefabric
version: 93a44e59fc887cda489913c6fc5bda834989f3bd version: 8026935326c842b71dee8e2329c1fda41a7a92f4
- name: github.com/jmespath/go-jmespath - name: github.com/jmespath/go-jmespath
version: bd40a432e4c76585ef6b72d3fd96fb9b6dc7b68d version: bd40a432e4c76585ef6b72d3fd96fb9b6dc7b68d
- name: github.com/jonboulle/clockwork - name: github.com/jonboulle/clockwork

View file

@ -12,7 +12,7 @@ import:
- package: github.com/cenk/backoff - package: github.com/cenk/backoff
- package: github.com/containous/flaeg - package: github.com/containous/flaeg
- package: github.com/containous/traefik-extra-service-fabric - package: github.com/containous/traefik-extra-service-fabric
version: ^v1.0.1 version: v1.0.4
- package: github.com/vulcand/oxy - package: github.com/vulcand/oxy
version: 7b6e758ab449705195df638765c4ca472248908a version: 7b6e758ab449705195df638765c4ca472248908a
repo: https://github.com/containous/oxy.git repo: https://github.com/containous/oxy.git

View file

@ -419,6 +419,46 @@ func (s *ConsulCatalogSuite) TestCircuitBreaker(c *check.C) {
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
} }
func (s *ConsulCatalogSuite) TestRefreshConfigPortChange(c *check.C) {
cmd, display := s.traefikCmd(
withConfigFile("fixtures/consul_catalog/simple.toml"),
"--consulCatalog",
"--consulCatalog.exposedByDefault=false",
"--consulCatalog.watch=true",
"--consulCatalog.endpoint="+s.consulIP+":8500",
"--consulCatalog.domain=consul.localhost")
defer display(c)
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
nginx := s.composeProject.Container(c, "nginx1")
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 81, []string{"name=nginx1", "traefik.enable=true"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
c.Assert(err, checker.IsNil)
req.Host = "test.consul.localhost"
err = try.Request(req, 20*time.Second, try.StatusCodeIs(http.StatusBadGateway))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 5*time.Second, try.BodyContains("nginx1"))
c.Assert(err, checker.IsNil)
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{"name=nginx1", "traefik.enable=true"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", nginx.NetworkSettings.IPAddress)
err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, try.BodyContains("nginx1"))
c.Assert(err, checker.IsNil)
err = try.Request(req, 20*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody())
c.Assert(err, checker.IsNil)
}
func (s *ConsulCatalogSuite) TestRetryWithConsulServer(c *check.C) { func (s *ConsulCatalogSuite) TestRetryWithConsulServer(c *check.C) {
//Scale consul to 0 to be able to start traefik before and test retry //Scale consul to 0 to be able to start traefik before and test retry
s.composeProject.Scale(c, "consul", 0) s.composeProject.Scale(c, "consul", 0)

View file

@ -12,6 +12,9 @@ type AddPrefix struct {
func (s *AddPrefix) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *AddPrefix) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r.URL.Path = s.Prefix + r.URL.Path r.URL.Path = s.Prefix + r.URL.Path
if r.URL.RawPath != "" {
r.URL.RawPath = s.Prefix + r.URL.RawPath
}
r.RequestURI = r.URL.RequestURI() r.RequestURI = r.URL.RequestURI()
s.Handler.ServeHTTP(w, r) s.Handler.ServeHTTP(w, r)
} }

View file

@ -9,21 +9,56 @@ import (
) )
func TestAddPrefix(t *testing.T) { func TestAddPrefix(t *testing.T) {
tests := []struct {
desc string
prefix string
path string
expectedPath string
expectedRawPath string
}{
{
desc: "regular path",
prefix: "/a",
path: "/b",
expectedPath: "/a/b",
},
{
desc: "raw path is supported",
prefix: "/a",
path: "/b%2Fc",
expectedPath: "/a/b/c",
expectedRawPath: "/a/b%2Fc",
},
}
path := "/bar" for _, test := range tests {
prefix := "/foo" test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var expectedPath string var actualPath, actualRawPath, requestURI string
handler := &AddPrefix{ handler := &AddPrefix{
Prefix: prefix, Prefix: test.prefix,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
expectedPath = r.URL.Path actualPath = r.URL.Path
actualRawPath = r.URL.RawPath
requestURI = r.RequestURI
}), }),
} }
req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost"+path, nil) req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost"+test.path, nil)
handler.ServeHTTP(nil, req) handler.ServeHTTP(nil, req)
assert.Equal(t, expectedPath, "/foo/bar", "Unexpected path.") assert.Equal(t, test.expectedPath, actualPath, "Unexpected path.")
assert.Equal(t, test.expectedRawPath, actualRawPath, "Unexpected raw path.")
expectedURI := test.expectedPath
if test.expectedRawPath != "" {
// go HTTP uses the raw path when existent in the RequestURI
expectedURI = test.expectedRawPath
}
assert.Equal(t, expectedURI, requestURI, "Unexpected request URI.")
})
}
} }

View file

@ -42,6 +42,7 @@ type Service struct {
Name string Name string
Tags []string Tags []string
Nodes []string Nodes []string
Ports []int
} }
type serviceUpdate struct { type serviceUpdate struct {
@ -185,19 +186,25 @@ func (p *CatalogProvider) watchCatalogServices(stopCh <-chan struct{}, watchCh c
errorCh <- err errorCh <- err
return return
} }
nodesID := getServiceIds(nodes) nodesID := getServiceIds(nodes)
ports := getServicePorts(nodes)
if service, ok := current[key]; ok { if service, ok := current[key]; ok {
service.Tags = value service.Tags = value
service.Nodes = nodesID service.Nodes = nodesID
service.Ports = ports
} else { } else {
service := Service{ service := Service{
Name: key, Name: key,
Tags: value, Tags: value,
Nodes: nodesID, Nodes: nodesID,
Ports: ports,
} }
current[key] = service current[key] = service
} }
} }
// A critical note is that the return of a blocking request is no guarantee of a change. // A critical note is that the return of a blocking request is no guarantee of a change.
// It is possible that there was an idempotent write that does not affect the result of the query. // It is possible that there was an idempotent write that does not affect the result of the query.
// Thus it is required to do extra check for changes... // Thus it is required to do extra check for changes...
@ -304,8 +311,11 @@ func (p *CatalogProvider) getNodes(index map[string][]string) ([]catalogUpdate,
} }
func hasChanged(current map[string]Service, previous map[string]Service) bool { func hasChanged(current map[string]Service, previous map[string]Service) bool {
if len(current) != len(previous) {
return true
}
addedServiceKeys, removedServiceKeys := getChangedServiceKeys(current, previous) addedServiceKeys, removedServiceKeys := getChangedServiceKeys(current, previous)
return len(removedServiceKeys) > 0 || len(addedServiceKeys) > 0 || hasNodeOrTagsChanged(current, previous) return len(removedServiceKeys) > 0 || len(addedServiceKeys) > 0 || hasServiceChanged(current, previous)
} }
func getChangedServiceKeys(current map[string]Service, previous map[string]Service) ([]string, []string) { func getChangedServiceKeys(current map[string]Service, previous map[string]Service) ([]string, []string) {
@ -318,20 +328,24 @@ func getChangedServiceKeys(current map[string]Service, previous map[string]Servi
return fun.Keys(addedKeys).([]string), fun.Keys(removedKeys).([]string) return fun.Keys(addedKeys).([]string), fun.Keys(removedKeys).([]string)
} }
func hasNodeOrTagsChanged(current map[string]Service, previous map[string]Service) bool { func hasServiceChanged(current map[string]Service, previous map[string]Service) bool {
var added []string
var removed []string
for key, value := range current { for key, value := range current {
if prevValue, ok := previous[key]; ok { if prevValue, ok := previous[key]; ok {
addedNodesKeys, removedNodesKeys := getChangedStringKeys(value.Nodes, prevValue.Nodes) addedNodesKeys, removedNodesKeys := getChangedStringKeys(value.Nodes, prevValue.Nodes)
added = append(added, addedNodesKeys...) if len(addedNodesKeys) > 0 || len(removedNodesKeys) > 0 {
removed = append(removed, removedNodesKeys...) return true
}
addedTagsKeys, removedTagsKeys := getChangedStringKeys(value.Tags, prevValue.Tags) addedTagsKeys, removedTagsKeys := getChangedStringKeys(value.Tags, prevValue.Tags)
added = append(added, addedTagsKeys...) if len(addedTagsKeys) > 0 || len(removedTagsKeys) > 0 {
removed = append(removed, removedTagsKeys...) return true
}
addedPortsKeys, removedPortsKeys := getChangedIntKeys(value.Ports, prevValue.Ports)
if len(addedPortsKeys) > 0 || len(removedPortsKeys) > 0 {
return true
} }
} }
return len(added) > 0 || len(removed) > 0 }
return false
} }
func getChangedStringKeys(currState []string, prevState []string) ([]string, []string) { func getChangedStringKeys(currState []string, prevState []string) ([]string, []string) {
@ -344,14 +358,32 @@ func getChangedStringKeys(currState []string, prevState []string) ([]string, []s
return fun.Keys(addedKeys).([]string), fun.Keys(removedKeys).([]string) return fun.Keys(addedKeys).([]string), fun.Keys(removedKeys).([]string)
} }
func getChangedIntKeys(currState []int, prevState []int) ([]int, []int) {
currKeySet := fun.Set(currState).(map[int]bool)
prevKeySet := fun.Set(prevState).(map[int]bool)
addedKeys := fun.Difference(currKeySet, prevKeySet).(map[int]bool)
removedKeys := fun.Difference(prevKeySet, currKeySet).(map[int]bool)
return fun.Keys(addedKeys).([]int), fun.Keys(removedKeys).([]int)
}
func getServiceIds(services []*api.CatalogService) []string { func getServiceIds(services []*api.CatalogService) []string {
var serviceIds []string var serviceIds []string
for _, service := range services { for _, service := range services {
serviceIds = append(serviceIds, service.ServiceID) serviceIds = append(serviceIds, service.ID)
} }
return serviceIds return serviceIds
} }
func getServicePorts(services []*api.CatalogService) []int {
var servicePorts []int
for _, service := range services {
servicePorts = append(servicePorts, service.ServicePort)
}
return servicePorts
}
func (p *CatalogProvider) healthyNodes(service string) (catalogUpdate, error) { func (p *CatalogProvider) healthyNodes(service string) (catalogUpdate, error) {
health := p.client.Health() health := p.client.Health()
opts := &api.QueryOptions{} opts := &api.QueryOptions{}
@ -364,7 +396,7 @@ func (p *CatalogProvider) healthyNodes(service string) (catalogUpdate, error) {
return p.nodeFilter(service, node) return p.nodeFilter(service, node)
}, data).([]*api.ServiceEntry) }, data).([]*api.ServiceEntry)
//Merge tags of nodes matching constraints, in a single slice. // Merge tags of nodes matching constraints, in a single slice.
tags := fun.Foldl(func(node *api.ServiceEntry, set []string) []string { tags := fun.Foldl(func(node *api.ServiceEntry, set []string) []string {
return fun.Keys(fun.Union( return fun.Keys(fun.Union(
fun.Set(set), fun.Set(set),

View file

@ -28,7 +28,7 @@ func (p *CatalogProvider) buildConfiguration(catalog []catalogUpdate) *types.Con
"getTag": getTag, "getTag": getTag,
"hasTag": hasTag, "hasTag": hasTag,
"getEntryPoints": getEntryPoints, "getEntryPoints": getEntryPoints,
"hasMaxconnAttributes": p.hasMaxconnAttributes, "hasMaxconnAttributes": p.hasMaxConnAttributes,
} }
var allNodes []*api.ServiceEntry var allNodes []*api.ServiceEntry
@ -69,7 +69,7 @@ func (p *CatalogProvider) setupFrontEndTemplate() {
} }
func (p *CatalogProvider) getFrontendRule(service serviceUpdate) string { func (p *CatalogProvider) getFrontendRule(service serviceUpdate) string {
customFrontendRule := p.getAttribute("frontend.rule", service.Attributes, "") customFrontendRule := p.getAttribute(label.SuffixFrontendRule, service.Attributes, "")
if customFrontendRule == "" { if customFrontendRule == "" {
customFrontendRule = p.FrontEndRule customFrontendRule = p.FrontEndRule
} }
@ -102,16 +102,16 @@ func (p *CatalogProvider) getFrontendRule(service serviceUpdate) string {
} }
func (p *CatalogProvider) getBasicAuth(tags []string) []string { func (p *CatalogProvider) getBasicAuth(tags []string) []string {
list := p.getAttribute("frontend.auth.basic", tags, "") list := p.getAttribute(label.SuffixFrontendAuthBasic, tags, "")
if list != "" { if list != "" {
return strings.Split(list, ",") return strings.Split(list, ",")
} }
return []string{} return []string{}
} }
func (p *CatalogProvider) hasMaxconnAttributes(attributes []string) bool { func (p *CatalogProvider) hasMaxConnAttributes(attributes []string) bool {
amount := p.getAttribute("backend.maxconn.amount", attributes, "") amount := p.getAttribute(label.SuffixBackendMaxConnAmount, attributes, "")
extractorfunc := p.getAttribute("backend.maxconn.extractorfunc", attributes, "") extractorfunc := p.getAttribute(label.SuffixBackendMaxConnExtractorFunc, attributes, "")
return amount != "" && extractorfunc != "" return amount != "" && extractorfunc != ""
} }

View file

@ -1166,7 +1166,7 @@ func TestHasNodeOrTagschanged(t *testing.T) {
expected: false, expected: false,
}, },
{ {
desc: "Change detected con tags", desc: "Change detected on tags",
current: map[string]Service{ current: map[string]Service{
"foo-service": { "foo-service": {
Name: "foo", Name: "foo",
@ -1183,6 +1183,66 @@ func TestHasNodeOrTagschanged(t *testing.T) {
}, },
expected: true, expected: true,
}, },
{
desc: "Change detected on ports",
current: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{"foo=bar"},
Ports: []int{80},
},
},
previous: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{"foo"},
Ports: []int{81},
},
},
expected: true,
},
{
desc: "Change detected on ports",
current: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{"foo=bar"},
Ports: []int{80},
},
},
previous: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{"foo"},
Ports: []int{81, 82},
},
},
expected: true,
},
{
desc: "No Change detected",
current: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{"foo"},
Ports: []int{80},
},
},
previous: map[string]Service{
"foo-service": {
Name: "foo",
Nodes: []string{"node1"},
Tags: []string{"foo"},
Ports: []int{80},
},
},
expected: false,
},
} }
for _, test := range testCases { for _, test := range testCases {
@ -1190,7 +1250,7 @@ func TestHasNodeOrTagschanged(t *testing.T) {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
actual := hasNodeOrTagsChanged(test.current, test.previous) actual := hasServiceChanged(test.current, test.previous)
assert.Equal(t, test.expected, actual) assert.Equal(t, test.expected, actual)
}) })
} }

View file

@ -24,7 +24,11 @@ func (p *Provider) buildConfiguration(containersInspected []dockerData) *types.C
"getEntryPoints": getFuncSliceStringLabel(label.TraefikFrontendEntryPoints), "getEntryPoints": getFuncSliceStringLabel(label.TraefikFrontendEntryPoints),
"getBasicAuth": getFuncSliceStringLabel(label.TraefikFrontendAuthBasic), "getBasicAuth": getFuncSliceStringLabel(label.TraefikFrontendAuthBasic),
"getFrontendRule": p.getFrontendRule, "getFrontendRule": p.getFrontendRule,
"getRedirect": getFuncStringLabel(label.TraefikFrontendRedirect, label.DefaultFrontendRedirect), "hasRedirect": hasRedirect,
"getRedirectEntryPoint": getFuncStringLabel(label.TraefikFrontendRedirectEntryPoint, label.DefaultFrontendRedirectEntryPoint),
"getRedirectRegex": getFuncStringLabel(label.TraefikFrontendRedirectRegex, ""),
"getRedirectReplacement": getFuncStringLabel(label.TraefikFrontendRedirectReplacement, ""),
"hasCircuitBreakerLabel": hasFunc(label.TraefikBackendCircuitBreakerExpression), "hasCircuitBreakerLabel": hasFunc(label.TraefikBackendCircuitBreakerExpression),
"getCircuitBreakerExpression": getFuncStringLabel(label.TraefikBackendCircuitBreakerExpression, label.DefaultCircuitBreakerExpression), "getCircuitBreakerExpression": getFuncStringLabel(label.TraefikBackendCircuitBreakerExpression, label.DefaultCircuitBreakerExpression),
"hasLoadBalancerLabel": hasLoadBalancerLabel, "hasLoadBalancerLabel": hasLoadBalancerLabel,
@ -36,8 +40,6 @@ func (p *Provider) buildConfiguration(containersInspected []dockerData) *types.C
"hasStickinessLabel": hasFunc(label.TraefikBackendLoadBalancerStickiness), "hasStickinessLabel": hasFunc(label.TraefikBackendLoadBalancerStickiness),
"getStickinessCookieName": getFuncStringLabel(label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName), "getStickinessCookieName": getFuncStringLabel(label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName),
"isBackendLBSwarm": isBackendLBSwarm, // FIXME DEAD ? "isBackendLBSwarm": isBackendLBSwarm, // FIXME DEAD ?
"getServiceBackend": getServiceBackend,
"getServiceRedirect": getFuncServiceStringLabel(label.SuffixFrontendRedirect, label.DefaultFrontendRedirect),
"getWhitelistSourceRange": getFuncSliceStringLabel(label.TraefikFrontendWhitelistSourceRange), "getWhitelistSourceRange": getFuncSliceStringLabel(label.TraefikFrontendWhitelistSourceRange),
"hasRequestHeaders": hasFunc(label.TraefikFrontendRequestHeaders), "hasRequestHeaders": hasFunc(label.TraefikFrontendRequestHeaders),
@ -82,6 +84,7 @@ func (p *Provider) buildConfiguration(containersInspected []dockerData) *types.C
"getIsDevelopmentHeaders": getFuncBoolLabel(label.TraefikFrontendIsDevelopment, false), "getIsDevelopmentHeaders": getFuncBoolLabel(label.TraefikFrontendIsDevelopment, false),
"hasServices": hasServices, "hasServices": hasServices,
"getServiceBackend": getServiceBackend,
"getServiceNames": getServiceNames, "getServiceNames": getServiceNames,
"getServicePort": getServicePort, "getServicePort": getServicePort,
"hasServiceRequestHeaders": hasFuncServiceLabel(label.SuffixFrontendRequestHeaders), "hasServiceRequestHeaders": hasFuncServiceLabel(label.SuffixFrontendRequestHeaders),
@ -95,6 +98,10 @@ func (p *Provider) buildConfiguration(containersInspected []dockerData) *types.C
"getServiceFrontendRule": p.getServiceFrontendRule, "getServiceFrontendRule": p.getServiceFrontendRule,
"getServicePassHostHeader": getFuncServiceStringLabel(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeader), "getServicePassHostHeader": getFuncServiceStringLabel(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeader),
"getServicePriority": getFuncServiceStringLabel(label.SuffixFrontendPriority, label.DefaultFrontendPriority), "getServicePriority": getFuncServiceStringLabel(label.SuffixFrontendPriority, label.DefaultFrontendPriority),
"hasServiceRedirect": hasServiceRedirect,
"getServiceRedirectEntryPoint": getFuncServiceStringLabel(label.SuffixFrontendRedirectEntryPoint, label.DefaultFrontendRedirectEntryPoint),
"getServiceRedirectReplacement": getFuncServiceStringLabel(label.SuffixFrontendRedirectReplacement, ""),
"getServiceRedirectRegex": getFuncServiceStringLabel(label.SuffixFrontendRedirectRegex, ""),
} }
// filter containers // filter containers
filteredContainers := fun.Filter(func(container dockerData) bool { filteredContainers := fun.Filter(func(container dockerData) bool {
@ -125,11 +132,11 @@ func (p *Provider) buildConfiguration(containersInspected []dockerData) *types.C
Servers map[string][]dockerData Servers map[string][]dockerData
Domain string Domain string
}{ }{
filteredContainers, Containers: filteredContainers,
frontends, Frontends: frontends,
backends, Backends: backends,
servers, Servers: servers,
p.Domain, Domain: p.Domain,
} }
configuration, err := p.GetConfiguration("templates/docker.tmpl", DockerFuncMap, templateObjects) configuration, err := p.GetConfiguration("templates/docker.tmpl", DockerFuncMap, templateObjects)

View file

@ -167,6 +167,11 @@ func isBackendLBSwarm(container dockerData) bool {
return label.GetBoolValue(container.Labels, labelBackendLoadBalancerSwarm, false) return label.GetBoolValue(container.Labels, labelBackendLoadBalancerSwarm, false)
} }
func hasRedirect(container dockerData) bool {
return label.Has(container.Labels, label.TraefikFrontendRedirectEntryPoint) ||
label.Has(container.Labels, label.TraefikFrontendRedirectReplacement) && label.Has(container.Labels, label.TraefikFrontendRedirectRegex)
}
// Label functions // Label functions
func getFuncInt64Label(labelName string, defaultValue int64) func(container dockerData) int64 { func getFuncInt64Label(labelName string, defaultValue int64) func(container dockerData) int64 {

View file

@ -10,6 +10,8 @@ import (
docker "github.com/docker/docker/api/types" docker "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestDockerLoadDockerConfig(t *testing.T) { func TestDockerLoadDockerConfig(t *testing.T) {
@ -39,7 +41,6 @@ func TestDockerLoadDockerConfig(t *testing.T) {
PassHostHeader: true, PassHostHeader: true,
EntryPoints: []string{}, EntryPoints: []string{},
BasicAuth: []string{}, BasicAuth: []string{},
Redirect: "",
Routes: map[string]types.Route{ Routes: map[string]types.Route{
"route-frontend-Host-test-docker-localhost-0": { "route-frontend-Host-test-docker-localhost-0": {
Rule: "Host:test.docker.localhost", Rule: "Host:test.docker.localhost",
@ -67,7 +68,7 @@ func TestDockerLoadDockerConfig(t *testing.T) {
label.TraefikBackend: "foobar", label.TraefikBackend: "foobar",
label.TraefikFrontendEntryPoints: "http,https", label.TraefikFrontendEntryPoints: "http,https",
label.TraefikFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", label.TraefikFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
label.TraefikFrontendRedirect: "https", label.TraefikFrontendRedirectEntryPoint: "https",
}), }),
ports(nat.PortMap{ ports(nat.PortMap{
"80/tcp": {}, "80/tcp": {},
@ -91,7 +92,9 @@ func TestDockerLoadDockerConfig(t *testing.T) {
PassHostHeader: true, PassHostHeader: true,
EntryPoints: []string{"http", "https"}, EntryPoints: []string{"http", "https"},
BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
Redirect: "https", Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{ Routes: map[string]types.Route{
"route-frontend-Host-test1-docker-localhost-0": { "route-frontend-Host-test1-docker-localhost-0": {
Rule: "Host:test1.docker.localhost", Rule: "Host:test1.docker.localhost",
@ -103,7 +106,6 @@ func TestDockerLoadDockerConfig(t *testing.T) {
PassHostHeader: true, PassHostHeader: true,
EntryPoints: []string{}, EntryPoints: []string{},
BasicAuth: []string{}, BasicAuth: []string{},
Redirect: "",
Routes: map[string]types.Route{ Routes: map[string]types.Route{
"route-frontend-Host-test2-docker-localhost-1": { "route-frontend-Host-test2-docker-localhost-1": {
Rule: "Host:test2.docker.localhost", Rule: "Host:test2.docker.localhost",
@ -151,7 +153,6 @@ func TestDockerLoadDockerConfig(t *testing.T) {
PassHostHeader: true, PassHostHeader: true,
EntryPoints: []string{"http", "https"}, EntryPoints: []string{"http", "https"},
BasicAuth: []string{}, BasicAuth: []string{},
Redirect: "",
Routes: map[string]types.Route{ Routes: map[string]types.Route{
"route-frontend-Host-test1-docker-localhost-0": { "route-frontend-Host-test1-docker-localhost-0": {
Rule: "Host:test1.docker.localhost", Rule: "Host:test1.docker.localhost",
@ -197,13 +198,10 @@ func TestDockerLoadDockerConfig(t *testing.T) {
ExposedByDefault: true, ExposedByDefault: true,
} }
actualConfig := provider.buildConfiguration(dockerDataList) actualConfig := provider.buildConfiguration(dockerDataList)
// Compare backends require.NotNil(t, actualConfig, "actualConfig")
if !reflect.DeepEqual(actualConfig.Backends, test.expectedBackends) {
t.Errorf("expected %#v, got %#v", test.expectedBackends, actualConfig.Backends) assert.EqualValues(t, test.expectedBackends, actualConfig.Backends)
} assert.EqualValues(t, test.expectedFrontends, actualConfig.Frontends)
if !reflect.DeepEqual(actualConfig.Frontends, test.expectedFrontends) {
t.Errorf("expected %#v, got %#v", test.expectedFrontends, actualConfig.Frontends)
}
}) })
} }
} }

View file

@ -9,6 +9,8 @@ import (
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
docker "github.com/docker/docker/api/types" docker "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestSwarmGetFrontendName(t *testing.T) { func TestSwarmGetFrontendName(t *testing.T) {
@ -416,7 +418,6 @@ func TestSwarmLoadDockerConfig(t *testing.T) {
PassHostHeader: true, PassHostHeader: true,
EntryPoints: []string{}, EntryPoints: []string{},
BasicAuth: []string{}, BasicAuth: []string{},
Redirect: "",
Routes: map[string]types.Route{ Routes: map[string]types.Route{
"route-frontend-Host-test-docker-localhost-0": { "route-frontend-Host-test-docker-localhost-0": {
Rule: "Host:test.docker.localhost", Rule: "Host:test.docker.localhost",
@ -451,7 +452,7 @@ func TestSwarmLoadDockerConfig(t *testing.T) {
label.TraefikBackend: "foobar", label.TraefikBackend: "foobar",
label.TraefikFrontendEntryPoints: "http,https", label.TraefikFrontendEntryPoints: "http,https",
label.TraefikFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", label.TraefikFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
label.TraefikFrontendRedirect: "https", label.TraefikFrontendRedirectEntryPoint: "https",
}), }),
withEndpointSpec(modeVIP), withEndpointSpec(modeVIP),
withEndpoint(virtualIP("1", "127.0.0.1/24")), withEndpoint(virtualIP("1", "127.0.0.1/24")),
@ -472,7 +473,9 @@ func TestSwarmLoadDockerConfig(t *testing.T) {
PassHostHeader: true, PassHostHeader: true,
EntryPoints: []string{"http", "https"}, EntryPoints: []string{"http", "https"},
BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
Redirect: "https", Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{ Routes: map[string]types.Route{
"route-frontend-Host-test1-docker-localhost-0": { "route-frontend-Host-test1-docker-localhost-0": {
Rule: "Host:test1.docker.localhost", Rule: "Host:test1.docker.localhost",
@ -484,7 +487,6 @@ func TestSwarmLoadDockerConfig(t *testing.T) {
PassHostHeader: true, PassHostHeader: true,
EntryPoints: []string{}, EntryPoints: []string{},
BasicAuth: []string{}, BasicAuth: []string{},
Redirect: "",
Routes: map[string]types.Route{ Routes: map[string]types.Route{
"route-frontend-Host-test2-docker-localhost-1": { "route-frontend-Host-test2-docker-localhost-1": {
Rule: "Host:test2.docker.localhost", Rule: "Host:test2.docker.localhost",
@ -531,14 +533,12 @@ func TestSwarmLoadDockerConfig(t *testing.T) {
ExposedByDefault: true, ExposedByDefault: true,
SwarmMode: true, SwarmMode: true,
} }
actualConfig := provider.buildConfiguration(dockerDataList) actualConfig := provider.buildConfiguration(dockerDataList)
// Compare backends require.NotNil(t, actualConfig, "actualConfig")
if !reflect.DeepEqual(actualConfig.Backends, test.expectedBackends) {
t.Errorf("expected %#v, got %#v", test.expectedBackends, actualConfig.Backends) assert.EqualValues(t, test.expectedBackends, actualConfig.Backends)
} assert.EqualValues(t, test.expectedFrontends, actualConfig.Frontends)
if !reflect.DeepEqual(actualConfig.Frontends, test.expectedFrontends) {
t.Errorf("expected %#v, got %#v", test.expectedFrontends, actualConfig.Frontends)
}
}) })
} }
} }

View file

@ -86,6 +86,16 @@ func getServicePort(container dockerData, serviceName string) string {
return getPort(container) return getPort(container)
} }
func hasServiceRedirect(container dockerData, serviceName string) bool {
serviceLabels := getServiceLabels(container, serviceName)
if len(serviceLabels) == 0 {
return false
}
return label.Has(serviceLabels, label.SuffixFrontendRedirectEntryPoint) ||
label.Has(serviceLabels, label.SuffixFrontendRedirectRegex) && label.Has(serviceLabels, label.SuffixFrontendRedirectReplacement)
}
// Service label functions // Service label functions
func getFuncServiceMapLabel(labelSuffix string) func(container dockerData, serviceName string) map[string]string { func getFuncServiceMapLabel(labelSuffix string) func(container dockerData, serviceName string) map[string]string {

View file

@ -121,7 +121,7 @@ func TestDockerGetFuncServiceStringLabel(t *testing.T) {
actual := getFuncServiceStringLabel(test.suffixLabel, test.defaultValue)(dData, "myservice") actual := getFuncServiceStringLabel(test.suffixLabel, test.defaultValue)(dData, "myservice")
if actual != test.expected { if actual != test.expected {
t.Fatalf("got %q, expected %q", actual, test.expected) t.Errorf("got %q, expected %q", actual, test.expected)
} }
}) })
} }
@ -164,7 +164,7 @@ func TestDockerGetFuncServiceSliceStringLabel(t *testing.T) {
actual := getFuncServiceSliceStringLabel(test.suffixLabel)(dData, "myservice") actual := getFuncServiceSliceStringLabel(test.suffixLabel)(dData, "myservice")
if !reflect.DeepEqual(actual, test.expected) { if !reflect.DeepEqual(actual, test.expected) {
t.Fatalf("for container %q: got %q, expected %q", dData.Name, actual, test.expected) t.Errorf("for container %q: got %q, expected %q", dData.Name, actual, test.expected)
} }
}) })
} }

View file

@ -1,7 +1,6 @@
package docker package docker
import ( import (
"reflect"
"strconv" "strconv"
"testing" "testing"
@ -9,6 +8,8 @@ import (
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
docker "github.com/docker/docker/api/types" docker "github.com/docker/docker/api/types"
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestDockerGetServicePort(t *testing.T) { func TestDockerGetServicePort(t *testing.T) {
@ -41,7 +42,7 @@ func TestDockerGetServicePort(t *testing.T) {
dData := parseContainer(test.container) dData := parseContainer(test.container)
actual := getServicePort(dData, "myservice") actual := getServicePort(dData, "myservice")
if actual != test.expected { if actual != test.expected {
t.Fatalf("expected %q, got %q", test.expected, actual) t.Errorf("expected %q, got %q", test.expected, actual)
} }
}) })
} }
@ -79,7 +80,7 @@ func TestDockerGetServiceFrontendRule(t *testing.T) {
dData := parseContainer(test.container) dData := parseContainer(test.container)
actual := provider.getServiceFrontendRule(dData, "myservice") actual := provider.getServiceFrontendRule(dData, "myservice")
if actual != test.expected { if actual != test.expected {
t.Fatalf("expected %q, got %q", test.expected, actual) t.Errorf("expected %q, got %q", test.expected, actual)
} }
}) })
} }
@ -115,7 +116,7 @@ func TestDockerGetServiceBackend(t *testing.T) {
dData := parseContainer(test.container) dData := parseContainer(test.container)
actual := getServiceBackend(dData, "myservice") actual := getServiceBackend(dData, "myservice")
if actual != test.expected { if actual != test.expected {
t.Fatalf("expected %q, got %q", test.expected, actual) t.Errorf("expected %q, got %q", test.expected, actual)
} }
}) })
} }
@ -140,7 +141,7 @@ func TestDockerLoadDockerServiceConfig(t *testing.T) {
"traefik.service.port": "2503", "traefik.service.port": "2503",
"traefik.service.frontend.entryPoints": "http,https", "traefik.service.frontend.entryPoints": "http,https",
"traefik.service.frontend.auth.basic": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "traefik.service.frontend.auth.basic": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
"traefik.service.frontend.redirect": "https", "traefik.service.frontend.redirect.entryPoint": "https",
}), }),
ports(nat.PortMap{ ports(nat.PortMap{
"80/tcp": {}, "80/tcp": {},
@ -154,7 +155,9 @@ func TestDockerLoadDockerServiceConfig(t *testing.T) {
PassHostHeader: true, PassHostHeader: true,
EntryPoints: []string{"http", "https"}, EntryPoints: []string{"http", "https"},
BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
Redirect: "https", Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{ Routes: map[string]types.Route{
"service-service": { "service-service": {
Rule: "Host:foo.docker.localhost", Rule: "Host:foo.docker.localhost",
@ -188,7 +191,7 @@ func TestDockerLoadDockerServiceConfig(t *testing.T) {
"traefik.service.frontend.priority": "5000", "traefik.service.frontend.priority": "5000",
"traefik.service.frontend.entryPoints": "http,https,ws", "traefik.service.frontend.entryPoints": "http,https,ws",
"traefik.service.frontend.auth.basic": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "traefik.service.frontend.auth.basic": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
"traefik.service.frontend.redirect": "https", "traefik.service.frontend.redirect.entryPoint": "https",
}), }),
ports(nat.PortMap{ ports(nat.PortMap{
"80/tcp": {}, "80/tcp": {},
@ -215,7 +218,9 @@ func TestDockerLoadDockerServiceConfig(t *testing.T) {
Priority: 5000, Priority: 5000,
EntryPoints: []string{"http", "https", "ws"}, EntryPoints: []string{"http", "https", "ws"},
BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
Redirect: "https", Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{ Routes: map[string]types.Route{
"service-service": { "service-service": {
Rule: "Path:/mypath", Rule: "Path:/mypath",
@ -227,7 +232,6 @@ func TestDockerLoadDockerServiceConfig(t *testing.T) {
PassHostHeader: true, PassHostHeader: true,
EntryPoints: []string{}, EntryPoints: []string{},
BasicAuth: []string{}, BasicAuth: []string{},
Redirect: "",
Routes: map[string]types.Route{ Routes: map[string]types.Route{
"service-anotherservice": { "service-anotherservice": {
Rule: "Path:/anotherpath", Rule: "Path:/anotherpath",
@ -274,13 +278,10 @@ func TestDockerLoadDockerServiceConfig(t *testing.T) {
} }
actualConfig := provider.buildConfiguration(dockerDataList) actualConfig := provider.buildConfiguration(dockerDataList)
// Compare backends require.NotNil(t, actualConfig, "actualConfig")
if !reflect.DeepEqual(actualConfig.Backends, test.expectedBackends) {
t.Fatalf("expected %#v, got %#v", test.expectedBackends, actualConfig.Backends) assert.EqualValues(t, test.expectedBackends, actualConfig.Backends)
} assert.EqualValues(t, test.expectedFrontends, actualConfig.Frontends)
if !reflect.DeepEqual(actualConfig.Frontends, test.expectedFrontends) {
t.Fatalf("expected %#v, got %#v", test.expectedFrontends, actualConfig.Frontends)
}
}) })
} }
} }

View file

@ -152,9 +152,22 @@ func priority(value int) func(*types.Frontend) {
} }
} }
func redirect(value string) func(*types.Frontend) { func redirectEntryPoint(name string) func(*types.Frontend) {
return func(f *types.Frontend) { return func(f *types.Frontend) {
f.Redirect = value if f.Redirect == nil {
f.Redirect = &types.Redirect{}
}
f.Redirect.EntryPoint = name
}
}
func redirectRegex(regex, replacement string) func(*types.Frontend) {
return func(f *types.Frontend) {
if f.Redirect == nil {
f.Redirect = &types.Redirect{}
}
f.Redirect.Regex = regex
f.Redirect.Replacement = replacement
} }
} }

View file

@ -203,8 +203,6 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
whitelistSourceRange := label.GetSliceStringValue(i.Annotations, annotationKubernetesWhitelistSourceRange) whitelistSourceRange := label.GetSliceStringValue(i.Annotations, annotationKubernetesWhitelistSourceRange)
entryPointRedirect := i.Annotations[label.TraefikFrontendRedirect]
if _, exists := templateObjects.Frontends[r.Host+pa.Path]; !exists { if _, exists := templateObjects.Frontends[r.Host+pa.Path]; !exists {
basicAuthCreds, err := handleBasicAuthConfig(i, k8sClient) basicAuthCreds, err := handleBasicAuthConfig(i, k8sClient)
if err != nil { if err != nil {
@ -245,7 +243,7 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
Priority: priority, Priority: priority,
BasicAuth: basicAuthCreds, BasicAuth: basicAuthCreds,
WhitelistSourceRange: whitelistSourceRange, WhitelistSourceRange: whitelistSourceRange,
Redirect: entryPointRedirect, Redirect: getFrontendRedirect(i),
EntryPoints: entryPoints, EntryPoints: entryPoints,
Headers: headers, Headers: headers,
} }
@ -470,3 +468,23 @@ func equalPorts(servicePort v1.ServicePort, ingressPort intstr.IntOrString) bool
func shouldProcessIngress(ingressClass string) bool { func shouldProcessIngress(ingressClass string) bool {
return ingressClass == "" || ingressClass == "traefik" return ingressClass == "" || ingressClass == "traefik"
} }
func getFrontendRedirect(i *v1beta1.Ingress) *types.Redirect {
frontendRedirectEntryPoint, ok := i.Annotations[label.TraefikFrontendRedirectEntryPoint]
frep := ok && len(frontendRedirectEntryPoint) > 0
frontendRedirectRegex, ok := i.Annotations[label.TraefikFrontendRedirectRegex]
frrg := ok && len(frontendRedirectRegex) > 0
frontendRedirectReplacement, ok := i.Annotations[label.TraefikFrontendRedirectReplacement]
frrp := ok && len(frontendRedirectReplacement) > 0
if frep || frrg && frrp {
return &types.Redirect{
EntryPoint: frontendRedirectEntryPoint,
Regex: frontendRedirectRegex,
Replacement: frontendRedirectReplacement,
}
}
return nil
}

View file

@ -608,7 +608,7 @@ func TestIngressAnnotations(t *testing.T) {
buildIngress( buildIngress(
iNamespace("testing"), iNamespace("testing"),
iAnnotation(annotationKubernetesIngressClass, "traefik"), iAnnotation(annotationKubernetesIngressClass, "traefik"),
iAnnotation(label.TraefikFrontendRedirect, "https"), iAnnotation(label.TraefikFrontendRedirectEntryPoint, "https"),
iRules( iRules(
iRule( iRule(
iHost("redirect"), iHost("redirect"),
@ -752,7 +752,7 @@ func TestIngressAnnotations(t *testing.T) {
), ),
frontend("redirect/https", frontend("redirect/https",
passHostHeader(), passHostHeader(),
redirect("https"), redirectEntryPoint("https"),
routes( routes(
route("/https", "PathPrefix:/https"), route("/https", "PathPrefix:/https"),
route("redirect", "Host:redirect")), route("redirect", "Host:redirect")),
@ -1182,6 +1182,7 @@ func TestBasicAuthInTemplate(t *testing.T) {
require.NoError(t, err, "error loading ingresses") require.NoError(t, err, "error loading ingresses")
actual = provider.loadConfig(*actual) actual = provider.loadConfig(*actual)
require.NotNil(t, actual)
got := actual.Frontends["basic/auth"].BasicAuth got := actual.Frontends["basic/auth"].BasicAuth
if !reflect.DeepEqual(got, []string{"myUser:myEncodedPW"}) { if !reflect.DeepEqual(got, []string{"myUser:myEncodedPW"}) {
t.Fatalf("unexpected credentials: %+v", got) t.Fatalf("unexpected credentials: %+v", got)

View file

@ -22,7 +22,7 @@ const (
DefaultPassHostHeader = "true" DefaultPassHostHeader = "true"
DefaultFrontendPriority = "0" DefaultFrontendPriority = "0"
DefaultCircuitBreakerExpression = "NetworkErrorRatio() > 1" DefaultCircuitBreakerExpression = "NetworkErrorRatio() > 1"
DefaultFrontendRedirect = "" DefaultFrontendRedirectEntryPoint = ""
DefaultBackendLoadBalancerMethod = "wrr" DefaultBackendLoadBalancerMethod = "wrr"
DefaultBackendMaxconnExtractorFunc = "request.host" DefaultBackendMaxconnExtractorFunc = "request.host"
DefaultBackendLoadbalancerStickinessCookieName = "" DefaultBackendLoadbalancerStickinessCookieName = ""

View file

@ -48,7 +48,9 @@ const (
SuffixFrontendPassHostHeader = "frontend.passHostHeader" SuffixFrontendPassHostHeader = "frontend.passHostHeader"
SuffixFrontendPassTLSCert = "frontend.passTLSCert" SuffixFrontendPassTLSCert = "frontend.passTLSCert"
SuffixFrontendPriority = "frontend.priority" SuffixFrontendPriority = "frontend.priority"
SuffixFrontendRedirect = "frontend.redirect" SuffixFrontendRedirectEntryPoint = "frontend.redirect.entryPoint"
SuffixFrontendRedirectRegex = "frontend.redirect.regex"
SuffixFrontendRedirectReplacement = "frontend.redirect.replacement"
SuffixFrontendRule = "frontend.rule" SuffixFrontendRule = "frontend.rule"
SuffixFrontendRuleType = "frontend.rule.type" SuffixFrontendRuleType = "frontend.rule.type"
SuffixFrontendWhitelistSourceRange = "frontend.whitelistSourceRange" SuffixFrontendWhitelistSourceRange = "frontend.whitelistSourceRange"
@ -79,7 +81,9 @@ const (
TraefikFrontendPriority = Prefix + SuffixFrontendPriority TraefikFrontendPriority = Prefix + SuffixFrontendPriority
TraefikFrontendRule = Prefix + SuffixFrontendRule TraefikFrontendRule = Prefix + SuffixFrontendRule
TraefikFrontendRuleType = Prefix + SuffixFrontendRuleType TraefikFrontendRuleType = Prefix + SuffixFrontendRuleType
TraefikFrontendRedirect = Prefix + SuffixFrontendRedirect TraefikFrontendRedirectEntryPoint = Prefix + SuffixFrontendRedirectEntryPoint
TraefikFrontendRedirectRegex = Prefix + SuffixFrontendRedirectRegex
TraefikFrontendRedirectReplacement = Prefix + SuffixFrontendRedirectReplacement
TraefikFrontendValue = Prefix + SuffixFrontendValue TraefikFrontendValue = Prefix + SuffixFrontendValue
TraefikFrontendWhitelistSourceRange = Prefix + SuffixFrontendWhitelistSourceRange TraefikFrontendWhitelistSourceRange = Prefix + SuffixFrontendWhitelistSourceRange
TraefikFrontendRequestHeaders = Prefix + SuffixFrontendRequestHeaders TraefikFrontendRequestHeaders = Prefix + SuffixFrontendRequestHeaders

View file

@ -27,15 +27,18 @@ func (p *Provider) buildConfiguration(services []rancherData) *types.Configurati
"getFrontendRule": p.getFrontendRule, "getFrontendRule": p.getFrontendRule,
"hasCircuitBreakerLabel": hasFunc(label.TraefikBackendCircuitBreakerExpression), "hasCircuitBreakerLabel": hasFunc(label.TraefikBackendCircuitBreakerExpression),
"getCircuitBreakerExpression": getFuncString(label.TraefikBackendCircuitBreakerExpression, label.DefaultCircuitBreakerExpression), "getCircuitBreakerExpression": getFuncString(label.TraefikBackendCircuitBreakerExpression, label.DefaultCircuitBreakerExpression),
"hasLoadBalancerLabel": hasLoadBalancerLabel, // OK "hasLoadBalancerLabel": hasLoadBalancerLabel,
"getLoadBalancerMethod": getFuncString(label.TraefikFrontendRedirect, label.DefaultBackendLoadBalancerMethod), "getLoadBalancerMethod": getFuncString(label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod),
"hasMaxConnLabels": hasMaxConnLabels, // OK "hasMaxConnLabels": hasMaxConnLabels,
"getMaxConnAmount": getFuncInt64(label.TraefikBackendMaxConnAmount, math.MaxInt64), "getMaxConnAmount": getFuncInt64(label.TraefikBackendMaxConnAmount, math.MaxInt64),
"getMaxConnExtractorFunc": getFuncString(label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc), "getMaxConnExtractorFunc": getFuncString(label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc),
"getSticky": getSticky, // deprecated "getSticky": getSticky, // deprecated
"hasStickinessLabel": hasFunc(label.TraefikBackendLoadBalancerStickiness), "hasStickinessLabel": hasFunc(label.TraefikBackendLoadBalancerStickiness),
"getStickinessCookieName": getFuncString(label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName), "getStickinessCookieName": getFuncString(label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName),
"getRedirect": getFuncString(label.TraefikFrontendRedirect, label.DefaultFrontendRedirect), "hasRedirect": hasRedirect,
"getRedirectEntryPoint": getFuncString(label.TraefikFrontendRedirectEntryPoint, label.DefaultFrontendRedirectEntryPoint),
"getRedirectRegex": getFuncString(label.TraefikFrontendRedirectRegex, ""),
"getRedirectReplacement": getFuncString(label.TraefikFrontendRedirectReplacement, ""),
} }
// filter services // filter services
@ -116,7 +119,8 @@ func (p *Provider) getFrontendName(service rancherData) string {
} }
// TODO: Deprecated // TODO: Deprecated
// Deprecated replaced by Stickiness // replaced by Stickiness
// Deprecated
func getSticky(service rancherData) string { func getSticky(service rancherData) string {
if label.Has(service.Labels, label.TraefikBackendLoadBalancerSticky) { if label.Has(service.Labels, label.TraefikBackendLoadBalancerSticky) {
log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness)
@ -143,6 +147,11 @@ func getBackend(service rancherData) string {
return provider.Normalize(backend) return provider.Normalize(backend)
} }
func hasRedirect(service rancherData) bool {
return label.Has(service.Labels, label.TraefikFrontendRedirectEntryPoint) ||
label.Has(service.Labels, label.TraefikFrontendRedirectRegex) && label.Has(service.Labels, label.TraefikFrontendRedirectReplacement)
}
// Label functions // Label functions
func getFuncString(labelName string, defaultValue string) func(service rancherData) string { func getFuncString(labelName string, defaultValue string) func(service rancherData) string {

View file

@ -6,6 +6,7 @@ import (
"github.com/containous/traefik/provider/label" "github.com/containous/traefik/provider/label"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestProviderServiceFilter(t *testing.T) { func TestProviderServiceFilter(t *testing.T) {
@ -363,7 +364,7 @@ func TestProviderLoadRancherConfig(t *testing.T) {
Labels: map[string]string{ Labels: map[string]string{
label.TraefikPort: "80", label.TraefikPort: "80",
label.TraefikFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", label.TraefikFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
label.TraefikFrontendRedirect: "https", label.TraefikFrontendRedirectEntryPoint: "https",
}, },
Health: "healthy", Health: "healthy",
Containers: []string{"127.0.0.1"}, Containers: []string{"127.0.0.1"},
@ -376,7 +377,9 @@ func TestProviderLoadRancherConfig(t *testing.T) {
EntryPoints: []string{}, EntryPoints: []string{},
BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
Priority: 0, Priority: 0,
Redirect: "https", Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{ Routes: map[string]types.Route{
"route-frontend-Host-test-service-rancher-localhost": { "route-frontend-Host-test-service-rancher-localhost": {
Rule: "Host:test.service.rancher.localhost", Rule: "Host:test.service.rancher.localhost",
@ -405,9 +408,77 @@ func TestProviderLoadRancherConfig(t *testing.T) {
t.Parallel() t.Parallel()
actualConfig := provider.buildConfiguration(test.services) actualConfig := provider.buildConfiguration(test.services)
require.NotNil(t, actualConfig)
assert.EqualValues(t, test.expectedBackends, actualConfig.Backends) assert.EqualValues(t, test.expectedBackends, actualConfig.Backends)
assert.EqualValues(t, test.expectedFrontends, actualConfig.Frontends) assert.EqualValues(t, test.expectedFrontends, actualConfig.Frontends)
}) })
} }
} }
func TestHasRedirect(t *testing.T) {
testCases := []struct {
desc string
service rancherData
expected bool
}{
{
desc: "without redirect labels",
service: rancherData{
Name: "test-service",
},
expected: false,
},
{
desc: "with Redirect EntryPoint label",
service: rancherData{
Name: "test-service",
Labels: map[string]string{
label.TraefikFrontendRedirectEntryPoint: "https",
},
},
expected: true,
},
{
desc: "with Redirect regex label",
service: rancherData{
Name: "test-service",
Labels: map[string]string{
label.TraefikFrontendRedirectRegex: `(.+)`,
},
},
expected: false,
},
{
desc: "with Redirect replacement label",
service: rancherData{
Name: "test-service",
Labels: map[string]string{
label.TraefikFrontendRedirectReplacement: "$1",
},
},
expected: false,
},
{
desc: "with Redirect regex & replacement labels",
service: rancherData{
Name: "test-service",
Labels: map[string]string{
label.TraefikFrontendRedirectRegex: `(.+)`,
label.TraefikFrontendRedirectReplacement: "$1",
},
},
expected: true,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := hasRedirect(test.service)
assert.Equal(t, test.expected, actual)
})
}
}

View file

@ -49,6 +49,10 @@ import (
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )
const (
defaultRedirectRegex = `^(?:https?:\/\/)?([\w\._-]+)(?::\d+)?(.*)$`
)
var ( var (
httpServerLogger = stdlog.New(log.WriterLevel(logrus.DebugLevel), "", 0) httpServerLogger = stdlog.New(log.WriterLevel(logrus.DebugLevel), "", 0)
) )
@ -944,7 +948,7 @@ func (s *Server) loadConfig(configurations types.Configurations, globalConfigura
if entryPoint.Redirect != nil { if entryPoint.Redirect != nil {
if redirectHandlers[entryPointName] != nil { if redirectHandlers[entryPointName] != nil {
n.Use(redirectHandlers[entryPointName]) n.Use(redirectHandlers[entryPointName])
} else if handler, err := s.loadEntryPointConfig(entryPointName, entryPoint); err != nil { } else if handler, err := s.buildRedirectHandler(entryPointName, entryPoint.Redirect); err != nil {
log.Errorf("Error loading entrypoint configuration for frontend %s: %v", frontendName, err) log.Errorf("Error loading entrypoint configuration for frontend %s: %v", frontendName, err)
log.Errorf("Skipping frontend %s...", frontendName) log.Errorf("Skipping frontend %s...", frontendName)
continue frontend continue frontend
@ -1125,28 +1129,19 @@ func (s *Server) loadConfig(configurations types.Configurations, globalConfigura
ipWhitelistMiddleware, err := configureIPWhitelistMiddleware(frontend.WhitelistSourceRange) ipWhitelistMiddleware, err := configureIPWhitelistMiddleware(frontend.WhitelistSourceRange)
if err != nil { if err != nil {
log.Fatalf("Error creating IP Whitelister: %s", err) log.Errorf("Error creating IP Whitelister: %s", err)
} else if ipWhitelistMiddleware != nil { } else if ipWhitelistMiddleware != nil {
n.Use(ipWhitelistMiddleware) n.Use(ipWhitelistMiddleware)
log.Infof("Configured IP Whitelists: %s", frontend.WhitelistSourceRange) log.Infof("Configured IP Whitelists: %s", frontend.WhitelistSourceRange)
} }
if len(frontend.Redirect) > 0 { if frontend.Redirect != nil {
proto := "http" rewrite, err := s.buildRedirectHandler(entryPointName, frontend.Redirect)
if s.globalConfiguration.EntryPoints[frontend.Redirect].TLS != nil {
proto = "https"
}
regex, replacement, err := s.buildRedirect(proto, entryPoint)
if err != nil { if err != nil {
log.Fatalf("Error creating Frontend Redirect: %v", err) log.Errorf("Error creating Frontend Redirect: %v", err)
}
rewrite, err := middlewares.NewRewrite(regex, replacement, true)
if err != nil {
log.Fatalf("Error creating Frontend Redirect: %v", err)
} }
n.Use(rewrite) n.Use(rewrite)
log.Debugf("Creating frontend %s redirect to %s", frontendName, proto) log.Debugf("Frontend %s redirect created", frontendName)
} }
if len(frontend.BasicAuth) > 0 { if len(frontend.BasicAuth) > 0 {
@ -1296,41 +1291,57 @@ func (s *Server) wireFrontendBackend(serverRoute *serverRoute, handler http.Hand
serverRoute.route.Handler(handler) serverRoute.route.Handler(handler)
} }
func (s *Server) loadEntryPointConfig(entryPointName string, entryPoint *configuration.EntryPoint) (negroni.Handler, error) { func (s *Server) buildRedirectHandler(srcEntryPointName string, redirect *types.Redirect) (*middlewares.Rewrite, error) {
regex := entryPoint.Redirect.Regex // entry point redirect
replacement := entryPoint.Redirect.Replacement if len(redirect.EntryPoint) > 0 {
var err error return s.buildEntryPointRedirect(srcEntryPointName, redirect.EntryPoint)
if len(entryPoint.Redirect.EntryPoint) > 0 {
var protocol = "http"
if s.globalConfiguration.EntryPoints[entryPoint.Redirect.EntryPoint].TLS != nil {
protocol = "https"
} }
regex, replacement, err = s.buildRedirect(protocol, entryPoint)
// regex redirect
rewrite, err := middlewares.NewRewrite(redirect.Regex, redirect.Replacement, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} log.Debugf("Creating entryPoint redirect %s -> %s -> %s", srcEntryPointName, redirect.Regex, redirect.Replacement)
rewrite, err := middlewares.NewRewrite(regex, replacement, true)
if err != nil {
return nil, err
}
log.Debugf("Creating entryPoint redirect %s -> %s : %s -> %s", entryPointName, entryPoint.Redirect.EntryPoint, regex, replacement)
return rewrite, nil return rewrite, nil
} }
func (s *Server) buildRedirect(protocol string, entryPoint *configuration.EntryPoint) (string, string, error) { func (s *Server) buildEntryPointRedirect(srcEntryPointName string, redirectEntryPoint string) (*middlewares.Rewrite, error) {
regex := `^(?:https?:\/\/)?([\w\._-]+)(?::\d+)?(.*)$` regex, replacement, err := s.buildRedirect(redirectEntryPoint)
if s.globalConfiguration.EntryPoints[entryPoint.Redirect.EntryPoint] == nil { if err != nil {
return "", "", fmt.Errorf("unknown target entrypoint %q", entryPoint.Redirect.EntryPoint) return nil, err
} }
r, _ := regexp.Compile(`(:\d+)`)
match := r.FindStringSubmatch(s.globalConfiguration.EntryPoints[entryPoint.Redirect.EntryPoint].Address) rewrite, err := middlewares.NewRewrite(regex, replacement, true)
if err != nil {
// Impossible case because error is always nil
return nil, err
}
log.Debugf("Creating entryPoint redirect %s -> %s : %s -> %s", srcEntryPointName, redirectEntryPoint, regex, replacement)
return rewrite, nil
}
func (s *Server) buildRedirect(entryPointName string) (string, string, error) {
entryPoint := s.globalConfiguration.EntryPoints[entryPointName]
if entryPoint == nil {
return "", "", fmt.Errorf("unknown target entrypoint %q", entryPointName)
}
exp := regexp.MustCompile(`(:\d+)`)
match := exp.FindStringSubmatch(entryPoint.Address)
if len(match) == 0 { if len(match) == 0 {
return "", "", fmt.Errorf("bad Address format %q", s.globalConfiguration.EntryPoints[entryPoint.Redirect.EntryPoint].Address) return "", "", fmt.Errorf("bad Address format %q", entryPoint.Address)
} }
var protocol = "http"
if s.globalConfiguration.EntryPoints[entryPointName].TLS != nil {
protocol = "https"
}
replacement := protocol + "://$1" + match[0] + "$2" replacement := protocol + "://$1" + match[0] + "$2"
return regex, replacement, nil return defaultRedirectRegex, replacement, nil
} }
func (s *Server) buildDefaultHTTPRouter() *mux.Router { func (s *Server) buildDefaultHTTPRouter() *mux.Router {

View file

@ -924,55 +924,209 @@ func TestServerResponseEmptyBackend(t *testing.T) {
} }
} }
func TestServerLoadConfigBuildRedirect(t *testing.T) { func TestBuildEntryPointRedirect(t *testing.T) {
testCases := []struct { srv := Server{
desc string
replacementProtocol string
globalConfiguration configuration.GlobalConfiguration
originEntryPointName string
expectedReplacement string
}{
{
desc: "Redirect endpoint http to https with HTTPS protocol",
replacementProtocol: "https",
originEntryPointName: "http",
globalConfiguration: configuration.GlobalConfiguration{ globalConfiguration: configuration.GlobalConfiguration{
EntryPoints: configuration.EntryPoints{ EntryPoints: configuration.EntryPoints{
"http": &configuration.EntryPoint{ "http": &configuration.EntryPoint{Address: ":80"},
"https": &configuration.EntryPoint{Address: ":443", TLS: &tls.TLS{}},
},
},
}
testCases := []struct {
desc string
srcEntryPointName string
url string
entryPoint *configuration.EntryPoint
redirect *types.Redirect
expectedURL string
}{
{
desc: "redirect regex",
srcEntryPointName: "http",
url: "http://foo.com",
redirect: &types.Redirect{
Regex: `^(?:http?:\/\/)(foo)(\.com)$`,
Replacement: "https://$1{{\"bar\"}}$2",
},
entryPoint: &configuration.EntryPoint{
Address: ":80", Address: ":80",
Redirect: &configuration.Redirect{ Redirect: &types.Redirect{
Regex: `^(?:http?:\/\/)(foo)(\.com)$`,
Replacement: "https://$1{{\"bar\"}}$2",
},
},
expectedURL: "https://foobar.com",
},
{
desc: "redirect entry point",
srcEntryPointName: "http",
url: "http://foo:80",
redirect: &types.Redirect{
EntryPoint: "https",
},
entryPoint: &configuration.EntryPoint{
Address: ":80",
Redirect: &types.Redirect{
EntryPoint: "https", EntryPoint: "https",
}, },
}, },
"https": &configuration.EntryPoint{ expectedURL: "https://foo:443",
Address: ":443", },
TLS: &tls.TLS{}, {
desc: "redirect entry point with regex (ignored)",
srcEntryPointName: "http",
url: "http://foo.com:80",
redirect: &types.Redirect{
EntryPoint: "https",
Regex: `^(?:http?:\/\/)(foo)(\.com)$`,
Replacement: "https://$1{{\"bar\"}}$2",
},
entryPoint: &configuration.EntryPoint{
Address: ":80",
Redirect: &types.Redirect{
EntryPoint: "https",
Regex: `^(?:http?:\/\/)(foo)(\.com)$`,
Replacement: "https://$1{{\"bar\"}}$2",
}, },
}, },
expectedURL: "https://foo.com:443",
}, },
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
rewrite, err := srv.buildRedirectHandler(test.srcEntryPointName, test.redirect)
require.NoError(t, err)
req := testhelpers.MustNewRequest(http.MethodGet, test.url, nil)
recorder := httptest.NewRecorder()
rewrite.ServeHTTP(recorder, req, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Location", "fail")
}))
location, err := recorder.Result().Location()
require.NoError(t, err)
assert.Equal(t, test.expectedURL, location.String())
})
}
}
func TestServerBuildEntryPointRedirect(t *testing.T) {
srv := Server{
globalConfiguration: configuration.GlobalConfiguration{
EntryPoints: configuration.EntryPoints{
"http": &configuration.EntryPoint{Address: ":80"},
"https": &configuration.EntryPoint{Address: ":443", TLS: &tls.TLS{}},
},
},
}
testCases := []struct {
desc string
srcEntryPointName string
redirectEntryPoint string
url string
expectedURL string
errorExpected bool
}{
{
desc: "existing redirect entry point",
srcEntryPointName: "http",
redirectEntryPoint: "https",
url: "http://foo:80",
expectedURL: "https://foo:443",
},
{
desc: "non-existing redirect entry point",
srcEntryPointName: "http",
redirectEntryPoint: "foo",
url: "http://foo:80",
errorExpected: true,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
rewrite, err := srv.buildEntryPointRedirect(test.srcEntryPointName, test.redirectEntryPoint)
if test.errorExpected {
require.Error(t, err)
} else {
require.NoError(t, err)
recorder := httptest.NewRecorder()
r := testhelpers.MustNewRequest(http.MethodGet, test.url, nil)
rewrite.ServeHTTP(recorder, r, nil)
location, err := recorder.Result().Location()
require.NoError(t, err)
assert.Equal(t, test.expectedURL, location.String())
}
})
}
}
func TestServerBuildRedirect(t *testing.T) {
testCases := []struct {
desc string
globalConfiguration configuration.GlobalConfiguration
redirectEntryPointName string
expectedReplacement string
errorExpected bool
}{
{
desc: "Redirect endpoint http to https with HTTPS protocol",
redirectEntryPointName: "https",
globalConfiguration: configuration.GlobalConfiguration{
EntryPoints: configuration.EntryPoints{
"http": &configuration.EntryPoint{Address: ":80"},
"https": &configuration.EntryPoint{Address: ":443", TLS: &tls.TLS{}},
},
},
expectedReplacement: "https://$1:443$2", expectedReplacement: "https://$1:443$2",
}, },
{ {
desc: "Redirect endpoint http to http02 with HTTP protocol", desc: "Redirect endpoint http to http02 with HTTP protocol",
replacementProtocol: "http", redirectEntryPointName: "http02",
originEntryPointName: "http",
globalConfiguration: configuration.GlobalConfiguration{ globalConfiguration: configuration.GlobalConfiguration{
EntryPoints: configuration.EntryPoints{ EntryPoints: configuration.EntryPoints{
"http": &configuration.EntryPoint{ "http": &configuration.EntryPoint{Address: ":80"},
Address: ":80", "http02": &configuration.EntryPoint{Address: ":88"},
Redirect: &configuration.Redirect{
EntryPoint: "http02",
}, },
}, },
"http02": &configuration.EntryPoint{
Address: ":88",
},
},
},
expectedReplacement: "http://$1:88$2", expectedReplacement: "http://$1:88$2",
}, },
{
desc: "Redirect endpoint to non-existent entry point",
redirectEntryPointName: "foobar",
globalConfiguration: configuration.GlobalConfiguration{
EntryPoints: configuration.EntryPoints{
"http": &configuration.EntryPoint{Address: ":80"},
"http02": &configuration.EntryPoint{Address: ":88"},
},
},
errorExpected: true,
},
{
desc: "Redirect endpoint to an entry point with a malformed address",
redirectEntryPointName: "http02",
globalConfiguration: configuration.GlobalConfiguration{
EntryPoints: configuration.EntryPoints{
"http": &configuration.EntryPoint{Address: ":80"},
"http02": &configuration.EntryPoint{Address: "88"},
},
},
errorExpected: true,
},
} }
for _, test := range testCases { for _, test := range testCases {
@ -982,9 +1136,9 @@ func TestServerLoadConfigBuildRedirect(t *testing.T) {
srv := Server{globalConfiguration: test.globalConfiguration} srv := Server{globalConfiguration: test.globalConfiguration}
_, replacement, err := srv.buildRedirect(test.replacementProtocol, srv.globalConfiguration.EntryPoints[test.originEntryPointName]) _, replacement, err := srv.buildRedirect(test.redirectEntryPointName)
require.NoError(t, err, "build redirect sent an unexpected error") require.Equal(t, test.errorExpected, err != nil, "Expected an error but don't have error, or Expected no error but have an error: %v", err)
assert.Equal(t, test.expectedReplacement, replacement, "build redirect does not return the right replacement pattern") assert.Equal(t, test.expectedReplacement, replacement, "build redirect does not return the right replacement pattern")
}) })
} }

View file

@ -47,7 +47,6 @@
[frontends."frontend-{{getServiceBackend $container $serviceName}}"] [frontends."frontend-{{getServiceBackend $container $serviceName}}"]
backend = "backend-{{getServiceBackend $container $serviceName}}" backend = "backend-{{getServiceBackend $container $serviceName}}"
passHostHeader = {{getServicePassHostHeader $container $serviceName}} passHostHeader = {{getServicePassHostHeader $container $serviceName}}
redirect = "{{getServiceRedirect $container $serviceName}}"
{{if getWhitelistSourceRange $container}} {{if getWhitelistSourceRange $container}}
whitelistSourceRange = [{{range getWhitelistSourceRange $container}} whitelistSourceRange = [{{range getWhitelistSourceRange $container}}
"{{.}}", "{{.}}",
@ -60,6 +59,14 @@
basicAuth = [{{range getServiceBasicAuth $container $serviceName}} basicAuth = [{{range getServiceBasicAuth $container $serviceName}}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{if hasServiceRedirect $container $serviceName}}
[frontends."frontend-{{getServiceBackend $container $serviceName}}".redirect]
entryPoint = "{{getServiceRedirectEntryPoint $container $serviceName}}"
regex = "{{getServiceRedirectRegex $container $serviceName}}"
replacement = "{{getServiceRedirectReplacement $container $serviceName}}"
{{end}}
[frontends."frontend-{{getServiceBackend $container $serviceName}}".routes."service-{{$serviceName | replace "/" "" | replace "." "-"}}"] [frontends."frontend-{{getServiceBackend $container $serviceName}}".routes."service-{{$serviceName | replace "/" "" | replace "." "-"}}"]
rule = "{{getServiceFrontendRule $container $serviceName}}" rule = "{{getServiceFrontendRule $container $serviceName}}"
{{if hasServiceRequestHeaders $container $serviceName}} {{if hasServiceRequestHeaders $container $serviceName}}
@ -79,7 +86,6 @@
[frontends."frontend-{{$frontend}}"] [frontends."frontend-{{$frontend}}"]
backend = "backend-{{getBackend $container}}" backend = "backend-{{getBackend $container}}"
passHostHeader = {{getPassHostHeader $container}} passHostHeader = {{getPassHostHeader $container}}
redirect = "{{getRedirect $container}}"
{{if getWhitelistSourceRange $container}} {{if getWhitelistSourceRange $container}}
whitelistSourceRange = [{{range getWhitelistSourceRange $container}} whitelistSourceRange = [{{range getWhitelistSourceRange $container}}
"{{.}}", "{{.}}",
@ -92,6 +98,14 @@
basicAuth = [{{range getBasicAuth $container}} basicAuth = [{{range getBasicAuth $container}}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{if hasRedirect $container}}
[frontends."frontend-{{$frontend}}".redirect]
entryPoint = "{{getRedirectEntryPoint $container}}"
regex = "{{getRedirectRegex $container}}"
replacement = "{{getRedirectReplacement $container}}"
{{end}}
[frontends."frontend-{{$frontend}}".headers] [frontends."frontend-{{$frontend}}".headers]
{{if hasSSLRedirectHeaders $container}} {{if hasSSLRedirectHeaders $container}}
SSLRedirect = {{getSSLRedirectHeaders $container}} SSLRedirect = {{getSSLRedirectHeaders $container}}

View file

@ -25,13 +25,20 @@
backend = "{{$frontend.Backend}}" backend = "{{$frontend.Backend}}"
priority = {{$frontend.Priority}} priority = {{$frontend.Priority}}
passHostHeader = {{$frontend.PassHostHeader}} passHostHeader = {{$frontend.PassHostHeader}}
redirect = "{{$frontend.Redirect}}"
basicAuth = [{{range $frontend.BasicAuth}} basicAuth = [{{range $frontend.BasicAuth}}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
whitelistSourceRange = [{{range $frontend.WhitelistSourceRange}} whitelistSourceRange = [{{range $frontend.WhitelistSourceRange}}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{if $frontend.Redirect}}
[frontends."{{$frontendName}}".redirect]
entryPoint = "{{$frontend.RedirectEntryPoint}}"
regex = "{{$frontend.RedirectRegex}}"
replacement = "{{$frontend.RedirectReplacement}}"
{{end}}
[frontends."{{$frontendName}}".headers] [frontends."{{$frontendName}}".headers]
SSLRedirect = {{$frontend.Headers.SSLRedirect}} SSLRedirect = {{$frontend.Headers.SSLRedirect}}
SSLTemporaryRedirect = {{$frontend.Headers.SSLTemporaryRedirect}} SSLTemporaryRedirect = {{$frontend.Headers.SSLTemporaryRedirect}}

View file

@ -34,13 +34,20 @@
backend = "backend-{{getBackend $service}}" backend = "backend-{{getBackend $service}}"
passHostHeader = {{getPassHostHeader $service}} passHostHeader = {{getPassHostHeader $service}}
priority = {{getPriority $service}} priority = {{getPriority $service}}
redirect = "{{getRedirect $service}}"
entryPoints = [{{range getEntryPoints $service}} entryPoints = [{{range getEntryPoints $service}}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
basicAuth = [{{range getBasicAuth $service}} basicAuth = [{{range getBasicAuth $service}}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{if hasRedirect $service}}
[frontends."frontend-{{$frontendName}}".redirect]
entryPoint = "{{getRedirectEntryPoint $service}}"
regex = "{{getRedirectRegex $service}}"
replacement = "{{getRedirectReplacement $service}}"
{{end}}
[frontends."frontend-{{$frontendName}}".routes."route-frontend-{{$frontendName}}"] [frontends."frontend-{{$frontendName}}".routes."route-frontend-{{$frontendName}}"]
rule = "{{getFrontendRule $service}}" rule = "{{getFrontendRule $service}}"
{{end}} {{end}}

View file

@ -153,7 +153,14 @@ type Frontend struct {
Headers Headers `json:"headers,omitempty"` Headers Headers `json:"headers,omitempty"`
Errors map[string]ErrorPage `json:"errors,omitempty"` Errors map[string]ErrorPage `json:"errors,omitempty"`
RateLimit *RateLimit `json:"ratelimit,omitempty"` RateLimit *RateLimit `json:"ratelimit,omitempty"`
Redirect string `json:"redirect,omitempty"` Redirect *Redirect `json:"redirect,omitempty"`
}
// Redirect configures a redirection of an entry point to another, or to an URL
type Redirect struct {
EntryPoint string `json:"entryPoint,omitempty"`
Regex string `json:"regex,omitempty"`
Replacement string `json:"replacement,omitempty"`
} }
// LoadBalancerMethod holds the method of load balancing to use. // LoadBalancerMethod holds the method of load balancing to use.

View file

@ -1,23 +1,59 @@
package servicefabric package servicefabric
import "strings" import (
"strconv"
"strings"
)
func hasServiceLabel(service ServiceItemExtended, key string) bool { func getFuncBoolLabel(labelName string, defaultValue bool) func(service ServiceItemExtended) bool {
_, exists := service.Labels[key]
return exists
}
func getFuncBoolLabel(labelName string) func(service ServiceItemExtended) bool {
return func(service ServiceItemExtended) bool { return func(service ServiceItemExtended) bool {
return getBoolLabel(service, labelName) return getBoolValue(service.Labels, labelName, defaultValue)
} }
} }
func getBoolLabel(service ServiceItemExtended, labelName string) bool { func getFuncServiceStringLabel(service ServiceItemExtended, labelName string, defaultValue string) string {
value, exists := service.Labels[labelName] return getStringValue(service.Labels, labelName, defaultValue)
return exists && strings.EqualFold(strings.TrimSpace(value), "true")
} }
func getServiceLabelValue(service ServiceItemExtended, key string) string { func hasFuncService(service ServiceItemExtended, labelName string) bool {
return service.Labels[key] return hasLabel(service.Labels, labelName)
}
func getServiceLabelsWithPrefix(service ServiceItemExtended, prefix string) map[string]string {
results := make(map[string]string)
for k, v := range service.Labels {
if strings.HasPrefix(k, prefix) {
results[k] = v
}
}
return results
}
// must be replace by label.Has()
// Deprecated
func hasLabel(labels map[string]string, labelName string) bool {
value, ok := labels[labelName]
return ok && len(value) > 0
}
// must be replace by label.GetStringValue()
// Deprecated
func getStringValue(labels map[string]string, labelName string, defaultValue string) string {
if value, ok := labels[labelName]; ok && len(value) > 0 {
return value
}
return defaultValue
}
// must be replace by label.GetBoolValue()
// Deprecated
func getBoolValue(labels map[string]string, labelName string, defaultValue bool) bool {
rawValue, ok := labels[labelName]
if ok {
v, err := strconv.ParseBool(rawValue)
if err == nil {
return v
}
}
return defaultValue
} }

View file

@ -69,34 +69,7 @@ func (p *Provider) updateConfig(configurationChan chan<- types.ConfigMessage, po
log.Info("Checking service fabric config") log.Info("Checking service fabric config")
} }
services, err := getClusterServices(sfClient) configuration, err := p.buildConfiguration(sfClient)
if err != nil {
return err
}
templateObjects := struct {
Services []ServiceItemExtended
}{
services,
}
var sfFuncMap = template.FuncMap{
"isPrimary": isPrimary,
"getDefaultEndpoint": p.getDefaultEndpoint,
"getNamedEndpoint": p.getNamedEndpoint,
"getApplicationParameter": p.getApplicationParameter,
"doesAppParamContain": p.doesAppParamContain,
"hasServiceLabel": hasServiceLabel,
"getServiceLabelValue": getServiceLabelValue,
"getServiceLabelValueWithDefault": getServiceLabelValueWithDefault,
"getServiceLabelsWithPrefix": getServiceLabelsWithPrefix,
"getServicesWithLabelValueMap": getServicesWithLabelValueMap,
"getServicesWithLabelValue": getServicesWithLabelValue,
"isExposed": getFuncBoolLabel("expose"),
}
configuration, err := p.GetConfiguration(tmpl, sfFuncMap, templateObjects)
if err != nil { if err != nil {
return err return err
} }
@ -120,24 +93,39 @@ func (p *Provider) updateConfig(configurationChan chan<- types.ConfigMessage, po
return nil return nil
} }
func (p Provider) doesAppParamContain(app sf.ApplicationItem, key, shouldContain string) bool { func (p *Provider) buildConfiguration(sfClient sfClient) (*types.Configuration, error) {
value := p.getApplicationParameter(app, key) var sfFuncMap = template.FuncMap{
return strings.Contains(value, shouldContain) "getServices": getServices,
"hasLabel": hasFuncService,
"getLabelValue": getFuncServiceStringLabel,
"getLabelsWithPrefix": getServiceLabelsWithPrefix,
"isPrimary": isPrimary,
"isExposed": getFuncBoolLabel("expose", false),
"getBackendName": getBackendName,
"getDefaultEndpoint": getDefaultEndpoint,
"getNamedEndpoint": getNamedEndpoint, // FIXME unused
"getApplicationParameter": getApplicationParameter, // FIXME unused
"doesAppParamContain": doesAppParamContain, // FIXME unused
"filterServicesByLabelValue": filterServicesByLabelValue, // FIXME unused
}
services, err := getClusterServices(sfClient)
if err != nil {
return nil, err
}
templateObjects := struct {
Services []ServiceItemExtended
}{
Services: services,
}
return p.GetConfiguration(tmpl, sfFuncMap, templateObjects)
} }
func (p Provider) getApplicationParameter(app sf.ApplicationItem, key string) string { func getDefaultEndpoint(instance replicaInstance) string {
for _, param := range app.Parameters {
if param.Key == key {
return param.Value
}
}
log.Errorf("Parameter %s doesn't exist in app %s", key, app.Name)
return ""
}
func (p Provider) getDefaultEndpoint(instance replicaInstance) string {
id, data := instance.GetReplicaData() id, data := instance.GetReplicaData()
endpoint, err := getDefaultEndpoint(data.Address) endpoint, err := getReplicaDefaultEndpoint(data)
if err != nil { if err != nil {
log.Warnf("No default endpoint for replica %s in service %s endpointData: %s", id, data.Address) log.Warnf("No default endpoint for replica %s in service %s endpointData: %s", id, data.Address)
return "" return ""
@ -145,16 +133,64 @@ func (p Provider) getDefaultEndpoint(instance replicaInstance) string {
return endpoint return endpoint
} }
func (p Provider) getNamedEndpoint(instance replicaInstance, endpointName string) string { func getReplicaDefaultEndpoint(replicaData *sf.ReplicaItemBase) (string, error) {
id, data := instance.GetReplicaData() endpoints, err := decodeEndpointData(replicaData.Address)
endpoint, err := getNamedEndpoint(data.Address, endpointName)
if err != nil { if err != nil {
log.Warnf("No names endpoint of %s for replica %s in endpointData: %s", endpointName, id, data.Address) return "", err
}
var defaultHTTPEndpoint string
for _, v := range endpoints {
if strings.Contains(v, "http") {
defaultHTTPEndpoint = v
break
}
}
if len(defaultHTTPEndpoint) == 0 {
return "", errors.New("no default endpoint found")
}
return defaultHTTPEndpoint, nil
}
func getNamedEndpoint(instance replicaInstance, endpointName string) string {
id, data := instance.GetReplicaData()
endpoint, err := getReplicaNamedEndpoint(data, endpointName)
if err != nil {
log.Warnf("No names endpoint of %s for replica %s in endpointData: %s. Error: %v", endpointName, id, data.Address, err)
return "" return ""
} }
return endpoint return endpoint
} }
func getReplicaNamedEndpoint(replicaData *sf.ReplicaItemBase, endpointName string) (string, error) {
endpoints, err := decodeEndpointData(replicaData.Address)
if err != nil {
return "", err
}
endpoint, exists := endpoints[endpointName]
if !exists {
return "", errors.New("endpoint doesn't exist")
}
return endpoint, nil
}
func doesAppParamContain(app sf.ApplicationItem, key, shouldContain string) bool {
value := getApplicationParameter(app, key)
return strings.Contains(value, shouldContain)
}
func getApplicationParameter(app sf.ApplicationItem, key string) string {
for _, param := range app.Parameters {
if param.Key == key {
return param.Value
}
}
log.Errorf("Parameter %s doesn't exist in app %s", key, app.Name)
return ""
}
func getClusterServices(sfClient sfClient) ([]ServiceItemExtended, error) { func getClusterServices(sfClient sfClient) ([]ServiceItemExtended, error) {
apps, err := sfClient.GetApplications() apps, err := sfClient.GetApplications()
if err != nil { if err != nil {
@ -236,7 +272,7 @@ func getValidInstances(sfClient sfClient, app sf.ApplicationItem, service sf.Ser
return validInstances return validInstances
} }
func getServicesWithLabelValueMap(services []ServiceItemExtended, key string) map[string][]ServiceItemExtended { func getServices(services []ServiceItemExtended, key string) map[string][]ServiceItemExtended {
result := map[string][]ServiceItemExtended{} result := map[string][]ServiceItemExtended{}
for _, service := range services { for _, service := range services {
if value, exists := service.Labels[key]; exists { if value, exists := service.Labels[key]; exists {
@ -250,7 +286,7 @@ func getServicesWithLabelValueMap(services []ServiceItemExtended, key string) ma
return result return result
} }
func getServicesWithLabelValue(services []ServiceItemExtended, key, expectedValue string) []ServiceItemExtended { func filterServicesByLabelValue(services []ServiceItemExtended, key, expectedValue string) []ServiceItemExtended {
var srvWithLabel []ServiceItemExtended var srvWithLabel []ServiceItemExtended
for _, service := range services { for _, service := range services {
value, exists := service.Labels[key] value, exists := service.Labels[key]
@ -261,25 +297,6 @@ func getServicesWithLabelValue(services []ServiceItemExtended, key, expectedValu
return srvWithLabel return srvWithLabel
} }
func getServiceLabelValueWithDefault(service ServiceItemExtended, key, defaultValue string) string {
value, exists := service.Labels[key]
if !exists {
return defaultValue
}
return value
}
func getServiceLabelsWithPrefix(service ServiceItemExtended, prefix string) map[string]string {
results := make(map[string]string)
for k, v := range service.Labels {
if strings.HasPrefix(k, prefix) {
results[k] = v
}
}
return results
}
func isPrimary(instance replicaInstance) bool { func isPrimary(instance replicaInstance) bool {
_, data := instance.GetReplicaData() _, data := instance.GetReplicaData()
return data.ReplicaRole == "Primary" return data.ReplicaRole == "Primary"
@ -290,7 +307,7 @@ func isHealthy(instanceData *sf.ReplicaItemBase) bool {
} }
func hasHTTPEndpoint(instanceData *sf.ReplicaItemBase) bool { func hasHTTPEndpoint(instanceData *sf.ReplicaItemBase) bool {
_, err := getDefaultEndpoint(instanceData.Address) _, err := getReplicaDefaultEndpoint(instanceData)
return err == nil return err == nil
} }
@ -314,37 +331,6 @@ func decodeEndpointData(endpointData string) (map[string]string, error) {
return endpoints, nil return endpoints, nil
} }
func getDefaultEndpoint(endpointData string) (string, error) { func getBackendName(service ServiceItemExtended, partition PartitionItemExtended) string {
endpoints, err := decodeEndpointData(endpointData) return provider.Normalize(service.Name + partition.PartitionInformation.ID)
if err != nil {
return "", err
}
var defaultHTTPEndpointExists bool
var defaultHTTPEndpoint string
for _, v := range endpoints {
if strings.Contains(v, "http") {
defaultHTTPEndpoint = v
defaultHTTPEndpointExists = true
break
}
}
if !defaultHTTPEndpointExists {
return "", errors.New("no default endpoint found")
}
return defaultHTTPEndpoint, nil
}
func getNamedEndpoint(endpointData string, endpointName string) (string, error) {
endpoints, err := decodeEndpointData(endpointData)
if err != nil {
return "", err
}
endpoint, exists := endpoints[endpointName]
if !exists {
return "", errors.New("endpoint doesn't exist")
}
return endpoint, nil
} }

View file

@ -1,8 +1,8 @@
package servicefabric package servicefabric
const tmpl = ` const tmpl = `
{{$groupedServiceMap := getServices .Services "backend.group.name"}}
[backends] [backends]
{{$groupedServiceMap := getServicesWithLabelValueMap .Services "backend.group.name"}}
{{range $aggName, $aggServices := $groupedServiceMap }} {{range $aggName, $aggServices := $groupedServiceMap }}
[backends."{{$aggName}}"] [backends."{{$aggName}}"]
{{range $service := $aggServices}} {{range $service := $aggServices}}
@ -10,7 +10,7 @@ const tmpl = `
{{range $instance := $partition.Instances}} {{range $instance := $partition.Instances}}
[backends."{{$aggName}}".servers."{{$service.ID}}-{{$instance.ID}}"] [backends."{{$aggName}}".servers."{{$service.ID}}-{{$instance.ID}}"]
url = "{{getDefaultEndpoint $instance}}" url = "{{getDefaultEndpoint $instance}}"
weight = {{getServiceLabelValueWithDefault $service "backend.group.weight" "1"}} weight = {{getLabelValue $service "backend.group.weight" "1"}}
{{end}} {{end}}
{{end}} {{end}}
{{end}} {{end}}
@ -20,45 +20,45 @@ const tmpl = `
{{if eq $partition.ServiceKind "Stateless"}} {{if eq $partition.ServiceKind "Stateless"}}
[backends."{{$service.Name}}"] [backends."{{$service.Name}}"]
[backends."{{$service.Name}}".LoadBalancer] [backends."{{$service.Name}}".LoadBalancer]
{{if hasServiceLabel $service "backend.loadbalancer.method"}} {{if hasLabel $service "backend.loadbalancer.method"}}
method = "{{getServiceLabelValue $service "backend.loadbalancer.method" }}" method = "{{getLabelValue $service "backend.loadbalancer.method" "" }}"
{{else}} {{else}}
method = "drr" method = "drr"
{{end}} {{end}}
{{if hasServiceLabel $service "backend.healthcheck"}} {{if hasLabel $service "backend.healthcheck"}}
[backends."{{$service.Name}}".healthcheck] [backends."{{$service.Name}}".healthcheck]
path = "{{getServiceLabelValue $service "backend.healthcheck"}}" path = "{{getLabelValue $service "backend.healthcheck" ""}}"
interval = "{{getServiceLabelValueWithDefault $service "backend.healthcheck.interval" "10s"}}" interval = "{{getLabelValue $service "backend.healthcheck.interval" "10s"}}"
{{end}} {{end}}
{{if hasServiceLabel $service "backend.loadbalancer.stickiness"}} {{if hasLabel $service "backend.loadbalancer.stickiness"}}
[backends."{{$service.Name}}".LoadBalancer.stickiness] [backends."{{$service.Name}}".LoadBalancer.stickiness]
{{end}} {{end}}
{{if hasServiceLabel $service "backend.circuitbreaker"}} {{if hasLabel $service "backend.circuitbreaker"}}
[backends."{{$service.Name}}".circuitbreaker] [backends."{{$service.Name}}".circuitbreaker]
expression = "{{getServiceLabelValue $service "backend.circuitbreaker"}}" expression = "{{getLabelValue $service "backend.circuitbreaker" ""}}"
{{end}} {{end}}
{{if hasServiceLabel $service "backend.maxconn.amount"}} {{if hasLabel $service "backend.maxconn.amount"}}
[backends."{{$service.Name}}".maxconn] [backends."{{$service.Name}}".maxconn]
amount = {{getServiceLabelValue $service "backend.maxconn.amount"}} amount = {{getLabelValue $service "backend.maxconn.amount" ""}}
{{if hasServiceLabel $service "backend.maxconn.extractorfunc"}} {{if hasLabel $service "backend.maxconn.extractorfunc"}}
extractorfunc = "{{getServiceLabelValue $service "backend.maxconn.extractorfunc"}}" extractorfunc = "{{getLabelValue $service "backend.maxconn.extractorfunc" ""}}"
{{end}} {{end}}
{{end}} {{end}}
{{range $instance := $partition.Instances}} {{range $instance := $partition.Instances}}
[backends."{{$service.Name}}".servers."{{$instance.ID}}"] [backends."{{$service.Name}}".servers."{{$instance.ID}}"]
url = "{{getDefaultEndpoint $instance}}" url = "{{getDefaultEndpoint $instance}}"
weight = {{getServiceLabelValueWithDefault $service "backend.weight" "1"}} weight = {{getLabelValue $service "backend.weight" "1"}}
{{end}} {{end}}
{{else if eq $partition.ServiceKind "Stateful"}} {{else if eq $partition.ServiceKind "Stateful"}}
{{range $replica := $partition.Replicas}} {{range $replica := $partition.Replicas}}
{{if isPrimary $replica}} {{if isPrimary $replica}}
{{$backendName := (print $service.Name $partition.PartitionInformation.ID)}} {{$backendName := getBackendName $service.Name $partition}}
[backends."{{$backendName}}".servers."{{$replica.ID}}"] [backends."{{$backendName}}".servers."{{$replica.ID}}"]
url = "{{getDefaultEndpoint $replica}}" url = "{{getDefaultEndpoint $replica}}"
weight = 1 weight = 1
@ -81,11 +81,11 @@ const tmpl = `
[frontends."{{$groupName}}"] [frontends."{{$groupName}}"]
backend = "{{$groupName}}" backend = "{{$groupName}}"
{{if hasServiceLabel $service "frontend.priority"}} {{if hasLabel $service "frontend.priority"}}
priority = 100 priority = 100
{{end}} {{end}}
{{range $key, $value := getServiceLabelsWithPrefix $service "frontend.rule"}} {{range $key, $value := getLabelsWithPrefix $service "frontend.rule"}}
[frontends."{{$groupName}}".routes."{{$key}}"] [frontends."{{$groupName}}".routes."{{$key}}"]
rule = "{{$value}}" rule = "{{$value}}"
{{end}} {{end}}
@ -97,27 +97,27 @@ const tmpl = `
[frontends."{{$service.Name}}"] [frontends."{{$service.Name}}"]
backend = "{{$service.Name}}" backend = "{{$service.Name}}"
{{if hasServiceLabel $service "frontend.passHostHeader"}} {{if hasLabel $service "frontend.passHostHeader"}}
passHostHeader = {{getServiceLabelValue $service "frontend.passHostHeader" }} passHostHeader = {{getLabelValue $service "frontend.passHostHeader" ""}}
{{end}} {{end}}
{{if hasServiceLabel $service "frontend.whitelistSourceRange"}} {{if hasLabel $service "frontend.whitelistSourceRange"}}
whitelistSourceRange = {{getServiceLabelValue $service "frontend.whitelistSourceRange" }} whitelistSourceRange = {{getLabelValue $service "frontend.whitelistSourceRange" ""}}
{{end}} {{end}}
{{if hasServiceLabel $service "frontend.priority"}} {{if hasLabel $service "frontend.priority"}}
priority = {{getServiceLabelValue $service "frontend.priority"}} priority = {{getLabelValue $service "frontend.priority" ""}}
{{end}} {{end}}
{{if hasServiceLabel $service "frontend.basicAuth"}} {{if hasLabel $service "frontend.basicAuth"}}
basicAuth = {{getServiceLabelValue $service "frontend.basicAuth"}} basicAuth = {{getLabelValue $service "frontend.basicAuth" ""}}
{{end}} {{end}}
{{if hasServiceLabel $service "frontend.entryPoints"}} {{if hasLabel $service "frontend.entryPoints"}}
entryPoints = {{getServiceLabelValue $service "frontend.entryPoints"}} entryPoints = {{getLabelValue $service "frontend.entryPoints" ""}}
{{end}} {{end}}
{{range $key, $value := getServiceLabelsWithPrefix $service "frontend.rule"}} {{range $key, $value := getLabelsWithPrefix $service "frontend.rule"}}
[frontends."{{$service.Name}}".routes."{{$key}}"] [frontends."{{$service.Name}}".routes."{{$key}}"]
rule = "{{$value}}" rule = "{{$value}}"
{{end}} {{end}}
@ -126,11 +126,11 @@ const tmpl = `
{{range $partition := $service.Partitions}} {{range $partition := $service.Partitions}}
{{$partitionId := $partition.PartitionInformation.ID}} {{$partitionId := $partition.PartitionInformation.ID}}
{{if hasServiceLabel $service "frontend.rule"}} {{if hasLabel $service "frontend.rule"}}
[frontends."{{$service.Name}}/{{$partitionId}}"] [frontends."{{$service.Name}}/{{$partitionId}}"]
backend = "{{$service.Name}}/{{$partitionId}}" backend = "{{getBackendName $service.Name $partition}}"
[frontends."{{$service.Name}}/{{$partitionId}}".routes.default] [frontends."{{$service.Name}}/{{$partitionId}}".routes.default]
rule = {{getServiceLabelValue $service "frontend.rule.partition.$partitionId"}} rule = {{getLabelValue $service "frontend.rule.partition.$partitionId" ""}}
{{end}} {{end}}
{{end}} {{end}}

View file

@ -12,6 +12,7 @@ import (
"strings" "strings"
) )
// DefaultAPIVersion is a default Service Fabric REST API version
const DefaultAPIVersion = "3.0" const DefaultAPIVersion = "3.0"
// Client for Service Fabric. // Client for Service Fabric.