diff --git a/README.md b/README.md index b8c09ca96..8e2b7809a 100644 --- a/README.md +++ b/README.md @@ -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 - 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. diff --git a/autogen/gentemplates/gen.go b/autogen/gentemplates/gen.go index c70d8fe14..6dc094e33 100644 --- a/autogen/gentemplates/gen.go +++ b/autogen/gentemplates/gen.go @@ -172,7 +172,6 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}} [frontends."frontend-{{getServiceBackend $container $serviceName}}"] backend = "backend-{{getServiceBackend $container $serviceName}}" passHostHeader = {{getServicePassHostHeader $container $serviceName}} - redirect = "{{getServiceRedirect $container $serviceName}}" {{if getWhitelistSourceRange $container}} whitelistSourceRange = [{{range getWhitelistSourceRange $container}} "{{.}}", @@ -185,7 +184,15 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}} basicAuth = [{{range getServiceBasicAuth $container $serviceName}} "{{.}}", {{end}}] - [frontends."frontend-{{getServiceBackend $container $serviceName}}".routes."service-{{$serviceName | replace "/" "" | replace "." "-"}}"] + + {{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 "." "-"}}"] rule = "{{getServiceFrontendRule $container $serviceName}}" {{if hasServiceRequestHeaders $container $serviceName}} [frontends."frontend-{{getServiceBackend $container $serviceName}}".headers.customrequestheaders] @@ -204,7 +211,6 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}} [frontends."frontend-{{$frontend}}"] backend = "backend-{{getBackend $container}}" passHostHeader = {{getPassHostHeader $container}} - redirect = "{{getRedirect $container}}" {{if getWhitelistSourceRange $container}} whitelistSourceRange = [{{range getWhitelistSourceRange $container}} "{{.}}", @@ -217,6 +223,14 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}} basicAuth = [{{range getBasicAuth $container}} "{{.}}", {{end}}] + + {{if hasRedirect $container}} + [frontends."frontend-{{$frontend}}".redirect] + entryPoint = "{{getRedirectEntryPoint $container}}" + regex = "{{getRedirectRegex $container}}" + replacement = "{{getRedirectReplacement $container}}" + {{end}} + [frontends."frontend-{{$frontend}}".headers] {{if hasSSLRedirectHeaders $container}} SSLRedirect = {{getSSLRedirectHeaders $container}} @@ -426,13 +440,20 @@ var _templatesKubernetesTmpl = []byte(`[backends]{{range $backendName, $backend backend = "{{$frontend.Backend}}" priority = {{$frontend.Priority}} passHostHeader = {{$frontend.PassHostHeader}} - redirect = "{{$frontend.Redirect}}" basicAuth = [{{range $frontend.BasicAuth}} "{{.}}", {{end}}] whitelistSourceRange = [{{range $frontend.WhitelistSourceRange}} "{{.}}", {{end}}] + + {{if $frontend.Redirect}} + [frontends."{{$frontendName}}".redirect] + entryPoint = "{{$frontend.RedirectEntryPoint}}" + regex = "{{$frontend.RedirectRegex}}" + replacement = "{{$frontend.RedirectReplacement}}" + {{end}} + [frontends."{{$frontendName}}".headers] SSLRedirect = {{$frontend.Headers.SSLRedirect}} SSLTemporaryRedirect = {{$frontend.Headers.SSLTemporaryRedirect}} @@ -764,13 +785,20 @@ var _templatesRancherTmpl = []byte(`{{$backendServers := .Backends}} backend = "backend-{{getBackend $service}}" passHostHeader = {{getPassHostHeader $service}} priority = {{getPriority $service}} - redirect = "{{getRedirect $service}}" entryPoints = [{{range getEntryPoints $service}} "{{.}}", {{end}}] basicAuth = [{{range getBasicAuth $service}} "{{.}}", {{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}}"] rule = "{{getFrontendRule $service}}" {{end}} diff --git a/cmd/traefik/anonymize/anonymize_config_test.go b/cmd/traefik/anonymize/anonymize_config_test.go index cf27291d9..abfd4732a 100644 --- a/cmd/traefik/anonymize/anonymize_config_test.go +++ b/cmd/traefik/anonymize/anonymize_config_test.go @@ -57,7 +57,7 @@ func TestDo_globalConfiguration(t *testing.T) { Optional: false, }, }, - Redirect: &configuration.Redirect{ + Redirect: &types.Redirect{ Replacement: "foo Replacement", Regex: "foo Regex", EntryPoint: "foo EntryPoint", @@ -103,7 +103,7 @@ func TestDo_globalConfiguration(t *testing.T) { Optional: false, }, }, - Redirect: &configuration.Redirect{ + Redirect: &types.Redirect{ Replacement: "fii Replacement", Regex: "fii Regex", EntryPoint: "fii EntryPoint", diff --git a/configuration/configuration.go b/configuration/configuration.go index 199d5ee0d..bc9a1816a 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -317,9 +317,9 @@ func (ep *EntryPoints) Set(value string) error { 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 { - redirect = &Redirect{ + redirect = &types.Redirect{ EntryPoint: result["redirect_entrypoint"], Regex: result["redirect_regex"], Replacement: result["redirect_replacement"], @@ -422,22 +422,15 @@ func (ep *EntryPoints) Type() string { type EntryPoint struct { Network string Address string - TLS *tls.TLS `export:"true"` - Redirect *Redirect `export:"true"` - Auth *types.Auth `export:"true"` + TLS *tls.TLS `export:"true"` + Redirect *types.Redirect `export:"true"` + Auth *types.Auth `export:"true"` WhitelistSourceRange []string Compress bool `export:"true"` ProxyProtocol *ProxyProtocol `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 type Retry struct { Attempts int `description:"Number of attempts" export:"true"` diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index 9d2f4783c..1acb639c6 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -8,6 +8,7 @@ import ( "github.com/containous/traefik/provider" "github.com/containous/traefik/provider/file" "github.com/containous/traefik/tls" + "github.com/containous/traefik/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -138,7 +139,7 @@ func TestEntryPoints_Set(t *testing.T) { expectedEntryPointName: "foo", expectedEntryPoint: &EntryPoint{ Address: ":8000", - Redirect: &Redirect{ + Redirect: &types.Redirect{ EntryPoint: "RedirectEntryPoint", Regex: "RedirectRegex", Replacement: "RedirectReplacement", @@ -171,7 +172,7 @@ func TestEntryPoints_Set(t *testing.T) { expectedEntryPointName: "foo", expectedEntryPoint: &EntryPoint{ Address: ":8000", - Redirect: &Redirect{ + Redirect: &types.Redirect{ EntryPoint: "RedirectEntryPoint", Regex: "RedirectRegex", Replacement: "RedirectReplacement", diff --git a/docs/basics.md b/docs/basics.md index 76fc323c3..15d3f977b 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -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: -- `wrr`: Weighted Round Robin +- `wrr`: Weighted Round Robin. - `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. @@ -373,16 +373,13 @@ It can be configured using: 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. -- `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 -also be applied to each backend. +To proactively prevent backends from being overwhelmed with high load, a maximum connection limit can also be applied to each backend. -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. +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. For example: ```toml @@ -499,8 +496,8 @@ Here is an example of backends and servers definition: Træfik's configuration has two parts: -- The [static Træfik configuration](/basics#static-trfk-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 [static Træfik configuration](/basics#static-trfik-configuration) which is loaded only at the beginning. +- The [dynamic Træfik configuration](/basics#dynamic-trfik-configuration) which can be hot-reloaded (no need to restart the process). ### Static Træfik configuration @@ -585,7 +582,7 @@ traefik [command] [--flag=flag_argument] List of Træfik available commands with description : - `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. - `healthcheck`: Calls Traefik `/ping` to check health. diff --git a/docs/configuration/acme.md b/docs/configuration/acme.md index 99f293e1b..8308e7d41 100644 --- a/docs/configuration/acme.md +++ b/docs/configuration/acme.md @@ -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` | | [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` | | [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` | diff --git a/docs/configuration/backends/consul.md b/docs/configuration/backends/consul.md index 25d809906..e59d6ca18 100644 --- a/docs/configuration/backends/consul.md +++ b/docs/configuration/backends/consul.md @@ -88,6 +88,12 @@ endpoint = "127.0.0.1:8500" # exposedByDefault = false +# Default domain used. +# +# Optional +# +domain = "consul.localhost" + # Prefix for Consul catalog tags. # # Optional diff --git a/docs/configuration/backends/docker.md b/docs/configuration/backends/docker.md index 9c7f0a2a7..55b7a22ab 100644 --- a/docs/configuration/backends/docker.md +++ b/docs/configuration/backends/docker.md @@ -149,29 +149,33 @@ To enable constraints see [backend-specific constraints section](/configuration/ Labels can be used on containers to override default behaviour. -| Label | Description | -|-----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `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.extractorfunc=client.ip` | Set the function to be used against the request to determine what to limit maximum connections to the backend by. Must be used in conjunction with the above label to take effect. | -| `traefik.backend.loadbalancer.method=drr` | Override the default `wrr` load balancer algorithm | -| `traefik.backend.loadbalancer.stickiness=true` | Enable backend sticky sessions | -| `traefik.backend.loadbalancer.stickiness.cookieName=NAME` | Manually set the cookie name for sticky sessions | -| `traefik.backend.loadbalancer.sticky=true` | Enable backend sticky sessions (DEPRECATED) | -| `traefik.backend.loadbalancer.swarm=true` | Use Swarm's inbuilt load balancer (only relevant under Swarm Mode). | -| `traefik.backend.circuitbreaker.expression=EXPR` | Create a [circuit breaker](/basics/#backends) to be used against the backend | -| `traefik.port=80` | Register this port. Useful when the container exposes multiples ports. | -| `traefik.protocol=https` | Override the default `http` protocol | -| `traefik.weight=10` | Assign this weight to the container | -| `traefik.enable=false` | Disable this container in Træfik | -| `traefik.frontend.rule=EXPR` | Override the default frontend rule. Default: `Host:{containerName}.{domain}` or `Host:{service}.{project_name}.{domain}` if you are using `docker-compose`. | -| `traefik.frontend.passHostHeader=true` | Forward client `Host` header to the backend. | -| `traefik.frontend.priority=10` | Override default frontend priority | -| `traefik.frontend.entryPoints=http,https` | Assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints` | -| `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash` | -| `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 `) 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) | +| Label | Description | +|------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `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.extractorfunc=client.ip` | Set the function to be used against the request to determine what to limit maximum connections to the backend by. Must be used in conjunction with the above label to take effect. | +| `traefik.backend.loadbalancer.method=drr` | Override the default `wrr` load balancer algorithm | +| `traefik.backend.loadbalancer.stickiness=true` | Enable backend sticky sessions | +| `traefik.backend.loadbalancer.stickiness.cookieName=NAME` | Manually set the cookie name for sticky sessions | +| `traefik.backend.loadbalancer.sticky=true` | Enable backend sticky sessions (DEPRECATED) | +| `traefik.backend.loadbalancer.swarm=true` | Use Swarm's inbuilt load balancer (only relevant under Swarm Mode). | +| `traefik.backend.circuitbreaker.expression=EXPR` | Create a [circuit breaker](/basics/#backends) to be used against the backend | +| `traefik.port=80` | Register this port. Useful when the container exposes multiples ports. | +| `traefik.protocol=https` | Override the default `http` protocol | +| `traefik.weight=10` | Assign this weight to the container | +| `traefik.enable=false` | Disable this container in Træfik | +| `traefik.frontend.rule=EXPR` | Override the default frontend rule. Default: `Host:{containerName}.{domain}` or `Host:{service}.{project_name}.{domain}` if you are using `docker-compose`. | +| `traefik.frontend.passHostHeader=true` | Forward client `Host` header to the backend. | +| `traefik.frontend.priority=10` | Override default frontend priority | +| `traefik.frontend.entryPoints=http,https` | Assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints` | +| `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash` | +| `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 `) 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.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 @@ -202,18 +206,21 @@ Labels can be used on containers to override default behaviour. Services labels can be used for overriding default behaviour -| Label | Description | -|---------------------------------------------------|--------------------------------------------------------------------------------------------------| -| `traefik..port=PORT` | Overrides `traefik.port`. If several ports need to be exposed, the service labels could be used. | -| `traefik..protocol` | Overrides `traefik.protocol`. | -| `traefik..weight` | Assign this service weight. Overrides `traefik.weight`. | -| `traefik..frontend.backend=BACKEND` | Assign this service frontend to `BACKEND`. Default is to assign to the service backend. | -| `traefik..frontend.entryPoints` | Overrides `traefik.frontend.entrypoints` | -| `traefik..frontend.auth.basic` | Sets a Basic Auth for that frontend | -| `traefik..frontend.passHostHeader` | Overrides `traefik.frontend.passHostHeader`. | -| `traefik..frontend.priority` | Overrides `traefik.frontend.priority`. | -| `traefik..frontend.rule` | Overrides `traefik.frontend.rule`. | -| `traefik..frontend.redirect` | Overrides `traefik.frontend.redirect`. | +| Label | Description | +|---------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------| +| `traefik..port=PORT` | Overrides `traefik.port`. If several ports need to be exposed, the service labels could be used. | +| `traefik..protocol` | Overrides `traefik.protocol`. | +| `traefik..weight` | Assign this service weight. Overrides `traefik.weight`. | +| `traefik..frontend.backend=BACKEND` | Assign this service frontend to `BACKEND`. Default is to assign to the service backend. | +| `traefik..frontend.entryPoints` | Overrides `traefik.frontend.entrypoints` | +| `traefik..frontend.auth.basic` | Sets a Basic Auth for that frontend | +| `traefik..frontend.passHostHeader` | Overrides `traefik.frontend.passHostHeader`. | +| `traefik..frontend.priority` | Overrides `traefik.frontend.priority`. | +| `traefik..frontend.rule` | Overrides `traefik.frontend.rule`. | +| `traefik..frontend.redirect` | Overrides `traefik.frontend.redirect`. | +| `traefik..frontend.redirect.entryPoint=https` | Overrides `traefik.frontend.redirect.entryPoint`. | +| `traefik..frontend.redirect.regex=^http://localhost/(.*)` | Overrides `traefik.frontend.redirect.regex`. | +| `traefik..frontend.redirect.replacement=http://mydomain/$1` | Overrides `traefik.frontend.redirect.replacement`. | #### Security Headers diff --git a/docs/configuration/backends/kubernetes.md b/docs/configuration/backends/kubernetes.md index 069054602..02e0f4617 100644 --- a/docs/configuration/backends/kubernetes.md +++ b/docs/configuration/backends/kubernetes.md @@ -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`. - `traefik.frontend.priority: "3"` 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). +- `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` Override the default frontend endpoints. - `traefik.frontend.passTLSCert: true` 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: - `traefik.backend.loadbalancer.method=drr` diff --git a/docs/configuration/backends/rancher.md b/docs/configuration/backends/rancher.md index 1acd56800..d36ef33f8 100644 --- a/docs/configuration/backends/rancher.md +++ b/docs/configuration/backends/rancher.md @@ -120,19 +120,21 @@ secretKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" Labels can be used on task containers to override default behaviour: -| Label | Description | -|-----------------------------------------------------------------------|------------------------------------------------------------------------------------------| -| `traefik.protocol=https` | Override the default `http` protocol | -| `traefik.weight=10` | Assign this weight to the container | -| `traefik.enable=false` | Disable this container in Træfik | -| `traefik.frontend.rule=Host:test.traefik.io` | Override the default frontend rule (Default: `Host:{containerName}.{domain}`). | -| `traefik.frontend.passHostHeader=true` | Forward client `Host` header to the backend. | -| `traefik.frontend.priority=10` | Override default frontend priority | -| `traefik.frontend.entryPoints=http,https` | Assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. | -| `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash`. | -| `traefik.frontend.redirect=https` | Enables Redirect to another entryPoint for that frontend (e.g. HTTPS) | -| `traefik.backend.circuitbreaker.expression=NetworkErrorRatio() > 0.5` | Create a [circuit breaker](/basics/#backends) to be used against the backend | -| `traefik.backend.loadbalancer.method=drr` | Override the default `wrr` load balancer algorithm | -| `traefik.backend.loadbalancer.stickiness=true` | Enable backend sticky sessions | -| `traefik.backend.loadbalancer.stickiness.cookieName=NAME` | Manually set the cookie name for sticky sessions | -| `traefik.backend.loadbalancer.sticky=true` | Enable backend sticky sessions (DEPRECATED) | \ No newline at end of file +| Label | Description | +|-----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------| +| `traefik.protocol=https` | Override the default `http` protocol | +| `traefik.weight=10` | Assign this weight to the container | +| `traefik.enable=false` | Disable this container in Træfik | +| `traefik.frontend.rule=Host:test.traefik.io` | Override the default frontend rule (Default: `Host:{containerName}.{domain}`). | +| `traefik.frontend.passHostHeader=true` | Forward client `Host` header to the backend. | +| `traefik.frontend.priority=10` | Override default frontend priority | +| `traefik.frontend.entryPoints=http,https` | Assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. | +| `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash`. | +| `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`. | +| `traefik.backend.circuitbreaker.expression=NetworkErrorRatio() > 0.5` | Create a [circuit breaker](/basics/#backends) to be used against the backend | +| `traefik.backend.loadbalancer.method=drr` | Override the default `wrr` load balancer algorithm | +| `traefik.backend.loadbalancer.stickiness=true` | Enable backend sticky sessions | +| `traefik.backend.loadbalancer.stickiness.cookieName=NAME` | Manually set the cookie name for sticky sessions | +| `traefik.backend.loadbalancer.sticky=true` | Enable backend sticky sessions (DEPRECATED) | \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 0ee4f79d9..4ff7d5dcf 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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 - 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. @@ -129,7 +129,7 @@ Start it from within the `traefik` folder: 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: diff --git a/glide.lock b/glide.lock index b34e5ef07..9a795d352 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 8c5908b11f5078edd9ed93e2710ebb3a29b7e02d1259fddd679f8c46540becc9 -updated: 2017-11-30T10:34:41.246378337+01:00 +hash: f0d5ef854a4c115306c63c15320b595c29f715950eaf5f18418149886ecda400 +updated: 2017-12-15T10:34:41.246378337+01:00 imports: - name: cloud.google.com/go version: 2e6a95edb1071d750f6d7db777bf66cd2997af6c @@ -94,7 +94,7 @@ imports: - name: github.com/containous/staert version: af517d5b70db9c4b0505e0144fcc62b054057d2a - name: github.com/containous/traefik-extra-service-fabric - version: 8076098dbfe814cba9e895ecbd896f1896b6b2d5 + version: c01c1ef60ed612c5e42c1ceae0c6f92e67619cc3 - name: github.com/coreos/bbolt version: 3c6cbfb299c11444eb2f8c9d48f0d2ce09157423 - name: github.com/coreos/etcd @@ -348,7 +348,7 @@ imports: subpackages: - lib - name: github.com/jjcollinge/servicefabric - version: 93a44e59fc887cda489913c6fc5bda834989f3bd + version: 8026935326c842b71dee8e2329c1fda41a7a92f4 - name: github.com/jmespath/go-jmespath version: bd40a432e4c76585ef6b72d3fd96fb9b6dc7b68d - name: github.com/jonboulle/clockwork diff --git a/glide.yaml b/glide.yaml index 512c1f240..b31ba43e5 100644 --- a/glide.yaml +++ b/glide.yaml @@ -12,7 +12,7 @@ import: - package: github.com/cenk/backoff - package: github.com/containous/flaeg - package: github.com/containous/traefik-extra-service-fabric - version: ^v1.0.1 + version: v1.0.4 - package: github.com/vulcand/oxy version: 7b6e758ab449705195df638765c4ca472248908a repo: https://github.com/containous/oxy.git diff --git a/integration/consul_catalog_test.go b/integration/consul_catalog_test.go index 0241d8ab3..40765059c 100644 --- a/integration/consul_catalog_test.go +++ b/integration/consul_catalog_test.go @@ -419,6 +419,46 @@ func (s *ConsulCatalogSuite) TestCircuitBreaker(c *check.C) { 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) { //Scale consul to 0 to be able to start traefik before and test retry s.composeProject.Scale(c, "consul", 0) diff --git a/middlewares/addPrefix.go b/middlewares/addPrefix.go index 467ca7cf3..306903ae2 100644 --- a/middlewares/addPrefix.go +++ b/middlewares/addPrefix.go @@ -12,6 +12,9 @@ type AddPrefix struct { func (s *AddPrefix) ServeHTTP(w http.ResponseWriter, r *http.Request) { 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() s.Handler.ServeHTTP(w, r) } diff --git a/middlewares/addPrefix_test.go b/middlewares/addPrefix_test.go index a3af7a7a4..53a1b84c0 100644 --- a/middlewares/addPrefix_test.go +++ b/middlewares/addPrefix_test.go @@ -9,21 +9,56 @@ import ( ) func TestAddPrefix(t *testing.T) { - - path := "/bar" - prefix := "/foo" - - var expectedPath string - handler := &AddPrefix{ - Prefix: prefix, - Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - expectedPath = r.URL.Path - }), + 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", + }, } - req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost"+path, nil) + for _, test := range tests { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() - handler.ServeHTTP(nil, req) + var actualPath, actualRawPath, requestURI string + handler := &AddPrefix{ + Prefix: test.prefix, + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + actualPath = r.URL.Path + actualRawPath = r.URL.RawPath + requestURI = r.RequestURI + }), + } - assert.Equal(t, expectedPath, "/foo/bar", "Unexpected path.") + req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost"+test.path, nil) + + handler.ServeHTTP(nil, req) + + 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.") + }) + } } diff --git a/provider/consul/consul_catalog.go b/provider/consul/consul_catalog.go index 02512065e..5ea91f283 100644 --- a/provider/consul/consul_catalog.go +++ b/provider/consul/consul_catalog.go @@ -42,6 +42,7 @@ type Service struct { Name string Tags []string Nodes []string + Ports []int } type serviceUpdate struct { @@ -185,19 +186,25 @@ func (p *CatalogProvider) watchCatalogServices(stopCh <-chan struct{}, watchCh c errorCh <- err return } + nodesID := getServiceIds(nodes) + ports := getServicePorts(nodes) + if service, ok := current[key]; ok { service.Tags = value service.Nodes = nodesID + service.Ports = ports } else { service := Service{ Name: key, Tags: value, Nodes: nodesID, + Ports: ports, } current[key] = service } } + // 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. // 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 { + if len(current) != len(previous) { + return true + } 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) { @@ -318,20 +328,24 @@ func getChangedServiceKeys(current map[string]Service, previous map[string]Servi return fun.Keys(addedKeys).([]string), fun.Keys(removedKeys).([]string) } -func hasNodeOrTagsChanged(current map[string]Service, previous map[string]Service) bool { - var added []string - var removed []string +func hasServiceChanged(current map[string]Service, previous map[string]Service) bool { for key, value := range current { if prevValue, ok := previous[key]; ok { addedNodesKeys, removedNodesKeys := getChangedStringKeys(value.Nodes, prevValue.Nodes) - added = append(added, addedNodesKeys...) - removed = append(removed, removedNodesKeys...) + if len(addedNodesKeys) > 0 || len(removedNodesKeys) > 0 { + return true + } addedTagsKeys, removedTagsKeys := getChangedStringKeys(value.Tags, prevValue.Tags) - added = append(added, addedTagsKeys...) - removed = append(removed, removedTagsKeys...) + if len(addedTagsKeys) > 0 || len(removedTagsKeys) > 0 { + 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) { @@ -344,14 +358,32 @@ func getChangedStringKeys(currState []string, prevState []string) ([]string, []s 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 { var serviceIds []string for _, service := range services { - serviceIds = append(serviceIds, service.ServiceID) + serviceIds = append(serviceIds, service.ID) } 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) { health := p.client.Health() opts := &api.QueryOptions{} @@ -364,7 +396,7 @@ func (p *CatalogProvider) healthyNodes(service string) (catalogUpdate, error) { return p.nodeFilter(service, node) }, 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 { return fun.Keys(fun.Union( fun.Set(set), diff --git a/provider/consul/consul_catalog_config.go b/provider/consul/consul_catalog_config.go index eca6c45fa..82025a555 100644 --- a/provider/consul/consul_catalog_config.go +++ b/provider/consul/consul_catalog_config.go @@ -28,7 +28,7 @@ func (p *CatalogProvider) buildConfiguration(catalog []catalogUpdate) *types.Con "getTag": getTag, "hasTag": hasTag, "getEntryPoints": getEntryPoints, - "hasMaxconnAttributes": p.hasMaxconnAttributes, + "hasMaxconnAttributes": p.hasMaxConnAttributes, } var allNodes []*api.ServiceEntry @@ -69,7 +69,7 @@ func (p *CatalogProvider) setupFrontEndTemplate() { } func (p *CatalogProvider) getFrontendRule(service serviceUpdate) string { - customFrontendRule := p.getAttribute("frontend.rule", service.Attributes, "") + customFrontendRule := p.getAttribute(label.SuffixFrontendRule, service.Attributes, "") if customFrontendRule == "" { customFrontendRule = p.FrontEndRule } @@ -102,16 +102,16 @@ func (p *CatalogProvider) getFrontendRule(service serviceUpdate) string { } func (p *CatalogProvider) getBasicAuth(tags []string) []string { - list := p.getAttribute("frontend.auth.basic", tags, "") + list := p.getAttribute(label.SuffixFrontendAuthBasic, tags, "") if list != "" { return strings.Split(list, ",") } return []string{} } -func (p *CatalogProvider) hasMaxconnAttributes(attributes []string) bool { - amount := p.getAttribute("backend.maxconn.amount", attributes, "") - extractorfunc := p.getAttribute("backend.maxconn.extractorfunc", attributes, "") +func (p *CatalogProvider) hasMaxConnAttributes(attributes []string) bool { + amount := p.getAttribute(label.SuffixBackendMaxConnAmount, attributes, "") + extractorfunc := p.getAttribute(label.SuffixBackendMaxConnExtractorFunc, attributes, "") return amount != "" && extractorfunc != "" } diff --git a/provider/consul/consul_catalog_test.go b/provider/consul/consul_catalog_test.go index da04121a8..71ee0780c 100644 --- a/provider/consul/consul_catalog_test.go +++ b/provider/consul/consul_catalog_test.go @@ -1166,7 +1166,7 @@ func TestHasNodeOrTagschanged(t *testing.T) { expected: false, }, { - desc: "Change detected con tags", + desc: "Change detected on tags", current: map[string]Service{ "foo-service": { Name: "foo", @@ -1183,6 +1183,66 @@ func TestHasNodeOrTagschanged(t *testing.T) { }, 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 { @@ -1190,7 +1250,7 @@ func TestHasNodeOrTagschanged(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - actual := hasNodeOrTagsChanged(test.current, test.previous) + actual := hasServiceChanged(test.current, test.previous) assert.Equal(t, test.expected, actual) }) } diff --git a/provider/docker/config.go b/provider/docker/config.go index 34fca901a..0e91bbbc6 100644 --- a/provider/docker/config.go +++ b/provider/docker/config.go @@ -13,18 +13,22 @@ import ( func (p *Provider) buildConfiguration(containersInspected []dockerData) *types.Configuration { var DockerFuncMap = template.FuncMap{ - "getBackend": getBackend, - "getIPAddress": p.getIPAddress, - "getPort": getPort, - "getWeight": getFuncStringLabel(label.TraefikWeight, label.DefaultWeight), - "getDomain": getFuncStringLabel(label.TraefikDomain, p.Domain), - "getProtocol": getFuncStringLabel(label.TraefikProtocol, label.DefaultProtocol), - "getPassHostHeader": getFuncStringLabel(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeader), - "getPriority": getFuncStringLabel(label.TraefikFrontendPriority, label.DefaultFrontendPriority), - "getEntryPoints": getFuncSliceStringLabel(label.TraefikFrontendEntryPoints), - "getBasicAuth": getFuncSliceStringLabel(label.TraefikFrontendAuthBasic), - "getFrontendRule": p.getFrontendRule, - "getRedirect": getFuncStringLabel(label.TraefikFrontendRedirect, label.DefaultFrontendRedirect), + "getBackend": getBackend, + "getIPAddress": p.getIPAddress, + "getPort": getPort, + "getWeight": getFuncStringLabel(label.TraefikWeight, label.DefaultWeight), + "getDomain": getFuncStringLabel(label.TraefikDomain, p.Domain), + "getProtocol": getFuncStringLabel(label.TraefikProtocol, label.DefaultProtocol), + "getPassHostHeader": getFuncStringLabel(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeader), + "getPriority": getFuncStringLabel(label.TraefikFrontendPriority, label.DefaultFrontendPriority), + "getEntryPoints": getFuncSliceStringLabel(label.TraefikFrontendEntryPoints), + "getBasicAuth": getFuncSliceStringLabel(label.TraefikFrontendAuthBasic), + "getFrontendRule": p.getFrontendRule, + "hasRedirect": hasRedirect, + "getRedirectEntryPoint": getFuncStringLabel(label.TraefikFrontendRedirectEntryPoint, label.DefaultFrontendRedirectEntryPoint), + "getRedirectRegex": getFuncStringLabel(label.TraefikFrontendRedirectRegex, ""), + "getRedirectReplacement": getFuncStringLabel(label.TraefikFrontendRedirectReplacement, ""), + "hasCircuitBreakerLabel": hasFunc(label.TraefikBackendCircuitBreakerExpression), "getCircuitBreakerExpression": getFuncStringLabel(label.TraefikBackendCircuitBreakerExpression, label.DefaultCircuitBreakerExpression), "hasLoadBalancerLabel": hasLoadBalancerLabel, @@ -36,8 +40,6 @@ func (p *Provider) buildConfiguration(containersInspected []dockerData) *types.C "hasStickinessLabel": hasFunc(label.TraefikBackendLoadBalancerStickiness), "getStickinessCookieName": getFuncStringLabel(label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName), "isBackendLBSwarm": isBackendLBSwarm, // FIXME DEAD ? - "getServiceBackend": getServiceBackend, - "getServiceRedirect": getFuncServiceStringLabel(label.SuffixFrontendRedirect, label.DefaultFrontendRedirect), "getWhitelistSourceRange": getFuncSliceStringLabel(label.TraefikFrontendWhitelistSourceRange), "hasRequestHeaders": hasFunc(label.TraefikFrontendRequestHeaders), @@ -81,20 +83,25 @@ func (p *Provider) buildConfiguration(containersInspected []dockerData) *types.C "hasIsDevelopmentHeaders": hasFunc(label.TraefikFrontendIsDevelopment), "getIsDevelopmentHeaders": getFuncBoolLabel(label.TraefikFrontendIsDevelopment, false), - "hasServices": hasServices, - "getServiceNames": getServiceNames, - "getServicePort": getServicePort, - "hasServiceRequestHeaders": hasFuncServiceLabel(label.SuffixFrontendRequestHeaders), - "getServiceRequestHeaders": getFuncServiceMapLabel(label.SuffixFrontendRequestHeaders), - "hasServiceResponseHeaders": hasFuncServiceLabel(label.SuffixFrontendResponseHeaders), - "getServiceResponseHeaders": getFuncServiceMapLabel(label.SuffixFrontendResponseHeaders), - "getServiceWeight": getFuncServiceStringLabel(label.SuffixWeight, label.DefaultWeight), - "getServiceProtocol": getFuncServiceStringLabel(label.SuffixProtocol, label.DefaultProtocol), - "getServiceEntryPoints": getFuncServiceSliceStringLabel(label.SuffixFrontendEntryPoints), - "getServiceBasicAuth": getFuncServiceSliceStringLabel(label.SuffixFrontendAuthBasic), - "getServiceFrontendRule": p.getServiceFrontendRule, - "getServicePassHostHeader": getFuncServiceStringLabel(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeader), - "getServicePriority": getFuncServiceStringLabel(label.SuffixFrontendPriority, label.DefaultFrontendPriority), + "hasServices": hasServices, + "getServiceBackend": getServiceBackend, + "getServiceNames": getServiceNames, + "getServicePort": getServicePort, + "hasServiceRequestHeaders": hasFuncServiceLabel(label.SuffixFrontendRequestHeaders), + "getServiceRequestHeaders": getFuncServiceMapLabel(label.SuffixFrontendRequestHeaders), + "hasServiceResponseHeaders": hasFuncServiceLabel(label.SuffixFrontendResponseHeaders), + "getServiceResponseHeaders": getFuncServiceMapLabel(label.SuffixFrontendResponseHeaders), + "getServiceWeight": getFuncServiceStringLabel(label.SuffixWeight, label.DefaultWeight), + "getServiceProtocol": getFuncServiceStringLabel(label.SuffixProtocol, label.DefaultProtocol), + "getServiceEntryPoints": getFuncServiceSliceStringLabel(label.SuffixFrontendEntryPoints), + "getServiceBasicAuth": getFuncServiceSliceStringLabel(label.SuffixFrontendAuthBasic), + "getServiceFrontendRule": p.getServiceFrontendRule, + "getServicePassHostHeader": getFuncServiceStringLabel(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeader), + "getServicePriority": getFuncServiceStringLabel(label.SuffixFrontendPriority, label.DefaultFrontendPriority), + "hasServiceRedirect": hasServiceRedirect, + "getServiceRedirectEntryPoint": getFuncServiceStringLabel(label.SuffixFrontendRedirectEntryPoint, label.DefaultFrontendRedirectEntryPoint), + "getServiceRedirectReplacement": getFuncServiceStringLabel(label.SuffixFrontendRedirectReplacement, ""), + "getServiceRedirectRegex": getFuncServiceStringLabel(label.SuffixFrontendRedirectRegex, ""), } // filter containers filteredContainers := fun.Filter(func(container dockerData) bool { @@ -125,11 +132,11 @@ func (p *Provider) buildConfiguration(containersInspected []dockerData) *types.C Servers map[string][]dockerData Domain string }{ - filteredContainers, - frontends, - backends, - servers, - p.Domain, + Containers: filteredContainers, + Frontends: frontends, + Backends: backends, + Servers: servers, + Domain: p.Domain, } configuration, err := p.GetConfiguration("templates/docker.tmpl", DockerFuncMap, templateObjects) diff --git a/provider/docker/config_container.go b/provider/docker/config_container.go index 12326ca1a..896eb2924 100644 --- a/provider/docker/config_container.go +++ b/provider/docker/config_container.go @@ -167,6 +167,11 @@ func isBackendLBSwarm(container dockerData) bool { 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 func getFuncInt64Label(labelName string, defaultValue int64) func(container dockerData) int64 { diff --git a/provider/docker/config_container_docker_test.go b/provider/docker/config_container_docker_test.go index b4ba6c283..1a246a185 100644 --- a/provider/docker/config_container_docker_test.go +++ b/provider/docker/config_container_docker_test.go @@ -10,6 +10,8 @@ import ( docker "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/go-connections/nat" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestDockerLoadDockerConfig(t *testing.T) { @@ -39,7 +41,6 @@ func TestDockerLoadDockerConfig(t *testing.T) { PassHostHeader: true, EntryPoints: []string{}, BasicAuth: []string{}, - Redirect: "", Routes: map[string]types.Route{ "route-frontend-Host-test-docker-localhost-0": { Rule: "Host:test.docker.localhost", @@ -64,10 +65,10 @@ func TestDockerLoadDockerConfig(t *testing.T) { containerJSON( name("test1"), labels(map[string]string{ - label.TraefikBackend: "foobar", - label.TraefikFrontendEntryPoints: "http,https", - label.TraefikFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", - label.TraefikFrontendRedirect: "https", + label.TraefikBackend: "foobar", + label.TraefikFrontendEntryPoints: "http,https", + label.TraefikFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + label.TraefikFrontendRedirectEntryPoint: "https", }), ports(nat.PortMap{ "80/tcp": {}, @@ -91,7 +92,9 @@ func TestDockerLoadDockerConfig(t *testing.T) { PassHostHeader: true, EntryPoints: []string{"http", "https"}, BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, - Redirect: "https", + Redirect: &types.Redirect{ + EntryPoint: "https", + }, Routes: map[string]types.Route{ "route-frontend-Host-test1-docker-localhost-0": { Rule: "Host:test1.docker.localhost", @@ -103,7 +106,6 @@ func TestDockerLoadDockerConfig(t *testing.T) { PassHostHeader: true, EntryPoints: []string{}, BasicAuth: []string{}, - Redirect: "", Routes: map[string]types.Route{ "route-frontend-Host-test2-docker-localhost-1": { Rule: "Host:test2.docker.localhost", @@ -151,7 +153,6 @@ func TestDockerLoadDockerConfig(t *testing.T) { PassHostHeader: true, EntryPoints: []string{"http", "https"}, BasicAuth: []string{}, - Redirect: "", Routes: map[string]types.Route{ "route-frontend-Host-test1-docker-localhost-0": { Rule: "Host:test1.docker.localhost", @@ -197,13 +198,10 @@ func TestDockerLoadDockerConfig(t *testing.T) { ExposedByDefault: true, } actualConfig := provider.buildConfiguration(dockerDataList) - // Compare backends - if !reflect.DeepEqual(actualConfig.Backends, test.expectedBackends) { - t.Errorf("expected %#v, got %#v", test.expectedBackends, actualConfig.Backends) - } - if !reflect.DeepEqual(actualConfig.Frontends, test.expectedFrontends) { - t.Errorf("expected %#v, got %#v", test.expectedFrontends, actualConfig.Frontends) - } + require.NotNil(t, actualConfig, "actualConfig") + + assert.EqualValues(t, test.expectedBackends, actualConfig.Backends) + assert.EqualValues(t, test.expectedFrontends, actualConfig.Frontends) }) } } diff --git a/provider/docker/config_container_swarm_test.go b/provider/docker/config_container_swarm_test.go index ed3bb18f0..366cd76b5 100644 --- a/provider/docker/config_container_swarm_test.go +++ b/provider/docker/config_container_swarm_test.go @@ -9,6 +9,8 @@ import ( "github.com/containous/traefik/types" docker "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestSwarmGetFrontendName(t *testing.T) { @@ -416,7 +418,6 @@ func TestSwarmLoadDockerConfig(t *testing.T) { PassHostHeader: true, EntryPoints: []string{}, BasicAuth: []string{}, - Redirect: "", Routes: map[string]types.Route{ "route-frontend-Host-test-docker-localhost-0": { Rule: "Host:test.docker.localhost", @@ -447,11 +448,11 @@ func TestSwarmLoadDockerConfig(t *testing.T) { swarmService( serviceName("test1"), serviceLabels(map[string]string{ - label.TraefikPort: "80", - label.TraefikBackend: "foobar", - label.TraefikFrontendEntryPoints: "http,https", - label.TraefikFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", - label.TraefikFrontendRedirect: "https", + label.TraefikPort: "80", + label.TraefikBackend: "foobar", + label.TraefikFrontendEntryPoints: "http,https", + label.TraefikFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + label.TraefikFrontendRedirectEntryPoint: "https", }), withEndpointSpec(modeVIP), withEndpoint(virtualIP("1", "127.0.0.1/24")), @@ -472,7 +473,9 @@ func TestSwarmLoadDockerConfig(t *testing.T) { PassHostHeader: true, EntryPoints: []string{"http", "https"}, BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, - Redirect: "https", + Redirect: &types.Redirect{ + EntryPoint: "https", + }, Routes: map[string]types.Route{ "route-frontend-Host-test1-docker-localhost-0": { Rule: "Host:test1.docker.localhost", @@ -484,7 +487,6 @@ func TestSwarmLoadDockerConfig(t *testing.T) { PassHostHeader: true, EntryPoints: []string{}, BasicAuth: []string{}, - Redirect: "", Routes: map[string]types.Route{ "route-frontend-Host-test2-docker-localhost-1": { Rule: "Host:test2.docker.localhost", @@ -531,14 +533,12 @@ func TestSwarmLoadDockerConfig(t *testing.T) { ExposedByDefault: true, SwarmMode: true, } + actualConfig := provider.buildConfiguration(dockerDataList) - // Compare backends - if !reflect.DeepEqual(actualConfig.Backends, test.expectedBackends) { - t.Errorf("expected %#v, got %#v", test.expectedBackends, actualConfig.Backends) - } - if !reflect.DeepEqual(actualConfig.Frontends, test.expectedFrontends) { - t.Errorf("expected %#v, got %#v", test.expectedFrontends, actualConfig.Frontends) - } + require.NotNil(t, actualConfig, "actualConfig") + + assert.EqualValues(t, test.expectedBackends, actualConfig.Backends) + assert.EqualValues(t, test.expectedFrontends, actualConfig.Frontends) }) } } diff --git a/provider/docker/config_service.go b/provider/docker/config_service.go index ca84e4499..caaee02c0 100644 --- a/provider/docker/config_service.go +++ b/provider/docker/config_service.go @@ -86,6 +86,16 @@ func getServicePort(container dockerData, serviceName string) string { 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 func getFuncServiceMapLabel(labelSuffix string) func(container dockerData, serviceName string) map[string]string { diff --git a/provider/docker/config_service_test.go b/provider/docker/config_service_test.go index d6251d801..48b89bfa0 100644 --- a/provider/docker/config_service_test.go +++ b/provider/docker/config_service_test.go @@ -121,7 +121,7 @@ func TestDockerGetFuncServiceStringLabel(t *testing.T) { actual := getFuncServiceStringLabel(test.suffixLabel, test.defaultValue)(dData, "myservice") 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") 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) } }) } diff --git a/provider/docker/service_test.go b/provider/docker/service_test.go index 440ebccbb..73f46804b 100644 --- a/provider/docker/service_test.go +++ b/provider/docker/service_test.go @@ -1,7 +1,6 @@ package docker import ( - "reflect" "strconv" "testing" @@ -9,6 +8,8 @@ import ( "github.com/containous/traefik/types" docker "github.com/docker/docker/api/types" "github.com/docker/go-connections/nat" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestDockerGetServicePort(t *testing.T) { @@ -41,7 +42,7 @@ func TestDockerGetServicePort(t *testing.T) { dData := parseContainer(test.container) actual := getServicePort(dData, "myservice") 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) actual := provider.getServiceFrontendRule(dData, "myservice") 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) actual := getServiceBackend(dData, "myservice") if actual != test.expected { - t.Fatalf("expected %q, got %q", test.expected, actual) + t.Errorf("expected %q, got %q", test.expected, actual) } }) } @@ -137,10 +138,10 @@ func TestDockerLoadDockerServiceConfig(t *testing.T) { containerJSON( name("foo"), labels(map[string]string{ - "traefik.service.port": "2503", - "traefik.service.frontend.entryPoints": "http,https", - "traefik.service.frontend.auth.basic": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", - "traefik.service.frontend.redirect": "https", + "traefik.service.port": "2503", + "traefik.service.frontend.entryPoints": "http,https", + "traefik.service.frontend.auth.basic": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + "traefik.service.frontend.redirect.entryPoint": "https", }), ports(nat.PortMap{ "80/tcp": {}, @@ -154,7 +155,9 @@ func TestDockerLoadDockerServiceConfig(t *testing.T) { PassHostHeader: true, EntryPoints: []string{"http", "https"}, BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, - Redirect: "https", + Redirect: &types.Redirect{ + EntryPoint: "https", + }, Routes: map[string]types.Route{ "service-service": { Rule: "Host:foo.docker.localhost", @@ -179,16 +182,16 @@ func TestDockerLoadDockerServiceConfig(t *testing.T) { containerJSON( name("test1"), labels(map[string]string{ - "traefik.service.port": "2503", - "traefik.service.protocol": "https", - "traefik.service.weight": "80", - "traefik.service.frontend.backend": "foobar", - "traefik.service.frontend.passHostHeader": "false", - "traefik.service.frontend.rule": "Path:/mypath", - "traefik.service.frontend.priority": "5000", - "traefik.service.frontend.entryPoints": "http,https,ws", - "traefik.service.frontend.auth.basic": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", - "traefik.service.frontend.redirect": "https", + "traefik.service.port": "2503", + "traefik.service.protocol": "https", + "traefik.service.weight": "80", + "traefik.service.frontend.backend": "foobar", + "traefik.service.frontend.passHostHeader": "false", + "traefik.service.frontend.rule": "Path:/mypath", + "traefik.service.frontend.priority": "5000", + "traefik.service.frontend.entryPoints": "http,https,ws", + "traefik.service.frontend.auth.basic": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + "traefik.service.frontend.redirect.entryPoint": "https", }), ports(nat.PortMap{ "80/tcp": {}, @@ -215,7 +218,9 @@ func TestDockerLoadDockerServiceConfig(t *testing.T) { Priority: 5000, EntryPoints: []string{"http", "https", "ws"}, BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, - Redirect: "https", + Redirect: &types.Redirect{ + EntryPoint: "https", + }, Routes: map[string]types.Route{ "service-service": { Rule: "Path:/mypath", @@ -227,7 +232,6 @@ func TestDockerLoadDockerServiceConfig(t *testing.T) { PassHostHeader: true, EntryPoints: []string{}, BasicAuth: []string{}, - Redirect: "", Routes: map[string]types.Route{ "service-anotherservice": { Rule: "Path:/anotherpath", @@ -274,13 +278,10 @@ func TestDockerLoadDockerServiceConfig(t *testing.T) { } actualConfig := provider.buildConfiguration(dockerDataList) - // Compare backends - if !reflect.DeepEqual(actualConfig.Backends, test.expectedBackends) { - t.Fatalf("expected %#v, got %#v", test.expectedBackends, actualConfig.Backends) - } - if !reflect.DeepEqual(actualConfig.Frontends, test.expectedFrontends) { - t.Fatalf("expected %#v, got %#v", test.expectedFrontends, actualConfig.Frontends) - } + require.NotNil(t, actualConfig, "actualConfig") + + assert.EqualValues(t, test.expectedBackends, actualConfig.Backends) + assert.EqualValues(t, test.expectedFrontends, actualConfig.Frontends) }) } } diff --git a/provider/kubernetes/builder_configuration_test.go b/provider/kubernetes/builder_configuration_test.go index 7d94c800c..4fab7c342 100644 --- a/provider/kubernetes/builder_configuration_test.go +++ b/provider/kubernetes/builder_configuration_test.go @@ -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) { - 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 } } diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index 2d4fdebc7..85508c914 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -203,8 +203,6 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) whitelistSourceRange := label.GetSliceStringValue(i.Annotations, annotationKubernetesWhitelistSourceRange) - entryPointRedirect := i.Annotations[label.TraefikFrontendRedirect] - if _, exists := templateObjects.Frontends[r.Host+pa.Path]; !exists { basicAuthCreds, err := handleBasicAuthConfig(i, k8sClient) if err != nil { @@ -245,7 +243,7 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) Priority: priority, BasicAuth: basicAuthCreds, WhitelistSourceRange: whitelistSourceRange, - Redirect: entryPointRedirect, + Redirect: getFrontendRedirect(i), EntryPoints: entryPoints, Headers: headers, } @@ -470,3 +468,23 @@ func equalPorts(servicePort v1.ServicePort, ingressPort intstr.IntOrString) bool func shouldProcessIngress(ingressClass string) bool { 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 +} diff --git a/provider/kubernetes/kubernetes_test.go b/provider/kubernetes/kubernetes_test.go index 7b3d23937..f2e6bc124 100644 --- a/provider/kubernetes/kubernetes_test.go +++ b/provider/kubernetes/kubernetes_test.go @@ -608,7 +608,7 @@ func TestIngressAnnotations(t *testing.T) { buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesIngressClass, "traefik"), - iAnnotation(label.TraefikFrontendRedirect, "https"), + iAnnotation(label.TraefikFrontendRedirectEntryPoint, "https"), iRules( iRule( iHost("redirect"), @@ -752,7 +752,7 @@ func TestIngressAnnotations(t *testing.T) { ), frontend("redirect/https", passHostHeader(), - redirect("https"), + redirectEntryPoint("https"), routes( route("/https", "PathPrefix:/https"), route("redirect", "Host:redirect")), @@ -1182,6 +1182,7 @@ func TestBasicAuthInTemplate(t *testing.T) { require.NoError(t, err, "error loading ingresses") actual = provider.loadConfig(*actual) + require.NotNil(t, actual) got := actual.Frontends["basic/auth"].BasicAuth if !reflect.DeepEqual(got, []string{"myUser:myEncodedPW"}) { t.Fatalf("unexpected credentials: %+v", got) diff --git a/provider/label/label.go b/provider/label/label.go index 2f8bc029c..8342260d0 100644 --- a/provider/label/label.go +++ b/provider/label/label.go @@ -22,7 +22,7 @@ const ( DefaultPassHostHeader = "true" DefaultFrontendPriority = "0" DefaultCircuitBreakerExpression = "NetworkErrorRatio() > 1" - DefaultFrontendRedirect = "" + DefaultFrontendRedirectEntryPoint = "" DefaultBackendLoadBalancerMethod = "wrr" DefaultBackendMaxconnExtractorFunc = "request.host" DefaultBackendLoadbalancerStickinessCookieName = "" diff --git a/provider/label/names.go b/provider/label/names.go index 4c356ae97..c622e16c4 100644 --- a/provider/label/names.go +++ b/provider/label/names.go @@ -48,7 +48,9 @@ const ( SuffixFrontendPassHostHeader = "frontend.passHostHeader" SuffixFrontendPassTLSCert = "frontend.passTLSCert" SuffixFrontendPriority = "frontend.priority" - SuffixFrontendRedirect = "frontend.redirect" + SuffixFrontendRedirectEntryPoint = "frontend.redirect.entryPoint" + SuffixFrontendRedirectRegex = "frontend.redirect.regex" + SuffixFrontendRedirectReplacement = "frontend.redirect.replacement" SuffixFrontendRule = "frontend.rule" SuffixFrontendRuleType = "frontend.rule.type" SuffixFrontendWhitelistSourceRange = "frontend.whitelistSourceRange" @@ -79,7 +81,9 @@ const ( TraefikFrontendPriority = Prefix + SuffixFrontendPriority TraefikFrontendRule = Prefix + SuffixFrontendRule TraefikFrontendRuleType = Prefix + SuffixFrontendRuleType - TraefikFrontendRedirect = Prefix + SuffixFrontendRedirect + TraefikFrontendRedirectEntryPoint = Prefix + SuffixFrontendRedirectEntryPoint + TraefikFrontendRedirectRegex = Prefix + SuffixFrontendRedirectRegex + TraefikFrontendRedirectReplacement = Prefix + SuffixFrontendRedirectReplacement TraefikFrontendValue = Prefix + SuffixFrontendValue TraefikFrontendWhitelistSourceRange = Prefix + SuffixFrontendWhitelistSourceRange TraefikFrontendRequestHeaders = Prefix + SuffixFrontendRequestHeaders diff --git a/provider/rancher/config.go b/provider/rancher/config.go index 1b88e1a4c..d85cf6191 100644 --- a/provider/rancher/config.go +++ b/provider/rancher/config.go @@ -27,15 +27,18 @@ func (p *Provider) buildConfiguration(services []rancherData) *types.Configurati "getFrontendRule": p.getFrontendRule, "hasCircuitBreakerLabel": hasFunc(label.TraefikBackendCircuitBreakerExpression), "getCircuitBreakerExpression": getFuncString(label.TraefikBackendCircuitBreakerExpression, label.DefaultCircuitBreakerExpression), - "hasLoadBalancerLabel": hasLoadBalancerLabel, // OK - "getLoadBalancerMethod": getFuncString(label.TraefikFrontendRedirect, label.DefaultBackendLoadBalancerMethod), - "hasMaxConnLabels": hasMaxConnLabels, // OK + "hasLoadBalancerLabel": hasLoadBalancerLabel, + "getLoadBalancerMethod": getFuncString(label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod), + "hasMaxConnLabels": hasMaxConnLabels, "getMaxConnAmount": getFuncInt64(label.TraefikBackendMaxConnAmount, math.MaxInt64), "getMaxConnExtractorFunc": getFuncString(label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc), "getSticky": getSticky, // deprecated "hasStickinessLabel": hasFunc(label.TraefikBackendLoadBalancerStickiness), "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 @@ -116,7 +119,8 @@ func (p *Provider) getFrontendName(service rancherData) string { } // TODO: Deprecated -// Deprecated replaced by Stickiness +// replaced by Stickiness +// Deprecated func getSticky(service rancherData) string { if label.Has(service.Labels, label.TraefikBackendLoadBalancerSticky) { 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) } +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 func getFuncString(labelName string, defaultValue string) func(service rancherData) string { diff --git a/provider/rancher/config_test.go b/provider/rancher/config_test.go index 8aa2c70c5..b2cb35017 100644 --- a/provider/rancher/config_test.go +++ b/provider/rancher/config_test.go @@ -6,6 +6,7 @@ import ( "github.com/containous/traefik/provider/label" "github.com/containous/traefik/types" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestProviderServiceFilter(t *testing.T) { @@ -361,9 +362,9 @@ func TestProviderLoadRancherConfig(t *testing.T) { { Name: "test/service", Labels: map[string]string{ - label.TraefikPort: "80", - label.TraefikFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", - label.TraefikFrontendRedirect: "https", + label.TraefikPort: "80", + label.TraefikFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + label.TraefikFrontendRedirectEntryPoint: "https", }, Health: "healthy", Containers: []string{"127.0.0.1"}, @@ -376,7 +377,9 @@ func TestProviderLoadRancherConfig(t *testing.T) { EntryPoints: []string{}, BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, Priority: 0, - Redirect: "https", + Redirect: &types.Redirect{ + EntryPoint: "https", + }, Routes: map[string]types.Route{ "route-frontend-Host-test-service-rancher-localhost": { Rule: "Host:test.service.rancher.localhost", @@ -405,9 +408,77 @@ func TestProviderLoadRancherConfig(t *testing.T) { t.Parallel() actualConfig := provider.buildConfiguration(test.services) + require.NotNil(t, actualConfig) assert.EqualValues(t, test.expectedBackends, actualConfig.Backends) 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) + }) + } +} diff --git a/server/server.go b/server/server.go index 0a9a8d2db..7c252929e 100644 --- a/server/server.go +++ b/server/server.go @@ -49,6 +49,10 @@ import ( "golang.org/x/net/http2" ) +const ( + defaultRedirectRegex = `^(?:https?:\/\/)?([\w\._-]+)(?::\d+)?(.*)$` +) + var ( 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 redirectHandlers[entryPointName] != nil { 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("Skipping frontend %s...", frontendName) continue frontend @@ -1125,28 +1129,19 @@ func (s *Server) loadConfig(configurations types.Configurations, globalConfigura ipWhitelistMiddleware, err := configureIPWhitelistMiddleware(frontend.WhitelistSourceRange) if err != nil { - log.Fatalf("Error creating IP Whitelister: %s", err) + log.Errorf("Error creating IP Whitelister: %s", err) } else if ipWhitelistMiddleware != nil { n.Use(ipWhitelistMiddleware) log.Infof("Configured IP Whitelists: %s", frontend.WhitelistSourceRange) } - if len(frontend.Redirect) > 0 { - proto := "http" - if s.globalConfiguration.EntryPoints[frontend.Redirect].TLS != nil { - proto = "https" - } - - regex, replacement, err := s.buildRedirect(proto, entryPoint) + if frontend.Redirect != nil { + rewrite, err := s.buildRedirectHandler(entryPointName, frontend.Redirect) if err != nil { - log.Fatalf("Error creating Frontend Redirect: %v", err) - } - rewrite, err := middlewares.NewRewrite(regex, replacement, true) - if err != nil { - log.Fatalf("Error creating Frontend Redirect: %v", err) + log.Errorf("Error creating Frontend Redirect: %v", err) } 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 { @@ -1296,41 +1291,57 @@ func (s *Server) wireFrontendBackend(serverRoute *serverRoute, handler http.Hand serverRoute.route.Handler(handler) } -func (s *Server) loadEntryPointConfig(entryPointName string, entryPoint *configuration.EntryPoint) (negroni.Handler, error) { - regex := entryPoint.Redirect.Regex - replacement := entryPoint.Redirect.Replacement - var err error - 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) - if err != nil { - return nil, err - } +func (s *Server) buildRedirectHandler(srcEntryPointName string, redirect *types.Redirect) (*middlewares.Rewrite, error) { + // entry point redirect + if len(redirect.EntryPoint) > 0 { + return s.buildEntryPointRedirect(srcEntryPointName, redirect.EntryPoint) } - rewrite, err := middlewares.NewRewrite(regex, replacement, true) + + // regex redirect + rewrite, err := middlewares.NewRewrite(redirect.Regex, redirect.Replacement, true) if err != nil { return nil, err } - log.Debugf("Creating entryPoint redirect %s -> %s : %s -> %s", entryPointName, entryPoint.Redirect.EntryPoint, regex, replacement) + log.Debugf("Creating entryPoint redirect %s -> %s -> %s", srcEntryPointName, redirect.Regex, redirect.Replacement) return rewrite, nil } -func (s *Server) buildRedirect(protocol string, entryPoint *configuration.EntryPoint) (string, string, error) { - regex := `^(?:https?:\/\/)?([\w\._-]+)(?::\d+)?(.*)$` - if s.globalConfiguration.EntryPoints[entryPoint.Redirect.EntryPoint] == nil { - return "", "", fmt.Errorf("unknown target entrypoint %q", entryPoint.Redirect.EntryPoint) +func (s *Server) buildEntryPointRedirect(srcEntryPointName string, redirectEntryPoint string) (*middlewares.Rewrite, error) { + regex, replacement, err := s.buildRedirect(redirectEntryPoint) + if err != nil { + 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 { - 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" - return regex, replacement, nil + return defaultRedirectRegex, replacement, nil } func (s *Server) buildDefaultHTTPRouter() *mux.Router { diff --git a/server/server_test.go b/server/server_test.go index 7768717ff..4e99d4c96 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -924,55 +924,209 @@ func TestServerResponseEmptyBackend(t *testing.T) { } } -func TestServerLoadConfigBuildRedirect(t *testing.T) { +func TestBuildEntryPointRedirect(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 - replacementProtocol string - globalConfiguration configuration.GlobalConfiguration - originEntryPointName string - expectedReplacement string + desc string + srcEntryPointName string + url string + entryPoint *configuration.EntryPoint + redirect *types.Redirect + expectedURL string }{ { - desc: "Redirect endpoint http to https with HTTPS protocol", - replacementProtocol: "https", - originEntryPointName: "http", - globalConfiguration: configuration.GlobalConfiguration{ - EntryPoints: configuration.EntryPoints{ - "http": &configuration.EntryPoint{ - Address: ":80", - Redirect: &configuration.Redirect{ - EntryPoint: "https", - }, - }, - "https": &configuration.EntryPoint{ - Address: ":443", - TLS: &tls.TLS{}, - }, + 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", + 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", + }, + }, + expectedURL: "https://foo:443", + }, + { + 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", }, { - desc: "Redirect endpoint http to http02 with HTTP protocol", - replacementProtocol: "http", - originEntryPointName: "http", + desc: "Redirect endpoint http to http02 with HTTP protocol", + redirectEntryPointName: "http02", globalConfiguration: configuration.GlobalConfiguration{ EntryPoints: configuration.EntryPoints{ - "http": &configuration.EntryPoint{ - Address: ":80", - Redirect: &configuration.Redirect{ - EntryPoint: "http02", - }, - }, - "http02": &configuration.EntryPoint{ - Address: ":88", - }, + "http": &configuration.EntryPoint{Address: ":80"}, + "http02": &configuration.EntryPoint{Address: ":88"}, }, }, - 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 { @@ -982,9 +1136,9 @@ func TestServerLoadConfigBuildRedirect(t *testing.T) { 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") }) } diff --git a/templates/docker.tmpl b/templates/docker.tmpl index fa94c67d1..f570128f1 100644 --- a/templates/docker.tmpl +++ b/templates/docker.tmpl @@ -47,7 +47,6 @@ [frontends."frontend-{{getServiceBackend $container $serviceName}}"] backend = "backend-{{getServiceBackend $container $serviceName}}" passHostHeader = {{getServicePassHostHeader $container $serviceName}} - redirect = "{{getServiceRedirect $container $serviceName}}" {{if getWhitelistSourceRange $container}} whitelistSourceRange = [{{range getWhitelistSourceRange $container}} "{{.}}", @@ -60,7 +59,15 @@ basicAuth = [{{range getServiceBasicAuth $container $serviceName}} "{{.}}", {{end}}] - [frontends."frontend-{{getServiceBackend $container $serviceName}}".routes."service-{{$serviceName | replace "/" "" | replace "." "-"}}"] + + {{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 "." "-"}}"] rule = "{{getServiceFrontendRule $container $serviceName}}" {{if hasServiceRequestHeaders $container $serviceName}} [frontends."frontend-{{getServiceBackend $container $serviceName}}".headers.customrequestheaders] @@ -79,7 +86,6 @@ [frontends."frontend-{{$frontend}}"] backend = "backend-{{getBackend $container}}" passHostHeader = {{getPassHostHeader $container}} - redirect = "{{getRedirect $container}}" {{if getWhitelistSourceRange $container}} whitelistSourceRange = [{{range getWhitelistSourceRange $container}} "{{.}}", @@ -92,6 +98,14 @@ basicAuth = [{{range getBasicAuth $container}} "{{.}}", {{end}}] + + {{if hasRedirect $container}} + [frontends."frontend-{{$frontend}}".redirect] + entryPoint = "{{getRedirectEntryPoint $container}}" + regex = "{{getRedirectRegex $container}}" + replacement = "{{getRedirectReplacement $container}}" + {{end}} + [frontends."frontend-{{$frontend}}".headers] {{if hasSSLRedirectHeaders $container}} SSLRedirect = {{getSSLRedirectHeaders $container}} diff --git a/templates/kubernetes.tmpl b/templates/kubernetes.tmpl index 7a671d323..305fd4f72 100644 --- a/templates/kubernetes.tmpl +++ b/templates/kubernetes.tmpl @@ -25,13 +25,20 @@ backend = "{{$frontend.Backend}}" priority = {{$frontend.Priority}} passHostHeader = {{$frontend.PassHostHeader}} - redirect = "{{$frontend.Redirect}}" basicAuth = [{{range $frontend.BasicAuth}} "{{.}}", {{end}}] whitelistSourceRange = [{{range $frontend.WhitelistSourceRange}} "{{.}}", {{end}}] + + {{if $frontend.Redirect}} + [frontends."{{$frontendName}}".redirect] + entryPoint = "{{$frontend.RedirectEntryPoint}}" + regex = "{{$frontend.RedirectRegex}}" + replacement = "{{$frontend.RedirectReplacement}}" + {{end}} + [frontends."{{$frontendName}}".headers] SSLRedirect = {{$frontend.Headers.SSLRedirect}} SSLTemporaryRedirect = {{$frontend.Headers.SSLTemporaryRedirect}} diff --git a/templates/rancher.tmpl b/templates/rancher.tmpl index 034168e76..3faad513d 100644 --- a/templates/rancher.tmpl +++ b/templates/rancher.tmpl @@ -34,13 +34,20 @@ backend = "backend-{{getBackend $service}}" passHostHeader = {{getPassHostHeader $service}} priority = {{getPriority $service}} - redirect = "{{getRedirect $service}}" entryPoints = [{{range getEntryPoints $service}} "{{.}}", {{end}}] basicAuth = [{{range getBasicAuth $service}} "{{.}}", {{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}}"] rule = "{{getFrontendRule $service}}" {{end}} diff --git a/types/types.go b/types/types.go index c92318d3a..0625b2271 100644 --- a/types/types.go +++ b/types/types.go @@ -153,7 +153,14 @@ type Frontend struct { Headers Headers `json:"headers,omitempty"` Errors map[string]ErrorPage `json:"errors,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. diff --git a/vendor/github.com/containous/traefik-extra-service-fabric/labels.go b/vendor/github.com/containous/traefik-extra-service-fabric/labels.go index 2dea665ae..f2878d20a 100644 --- a/vendor/github.com/containous/traefik-extra-service-fabric/labels.go +++ b/vendor/github.com/containous/traefik-extra-service-fabric/labels.go @@ -1,23 +1,59 @@ package servicefabric -import "strings" +import ( + "strconv" + "strings" +) -func hasServiceLabel(service ServiceItemExtended, key string) bool { - _, exists := service.Labels[key] - return exists -} - -func getFuncBoolLabel(labelName string) func(service ServiceItemExtended) bool { +func getFuncBoolLabel(labelName string, defaultValue bool) 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 { - value, exists := service.Labels[labelName] - return exists && strings.EqualFold(strings.TrimSpace(value), "true") +func getFuncServiceStringLabel(service ServiceItemExtended, labelName string, defaultValue string) string { + return getStringValue(service.Labels, labelName, defaultValue) } -func getServiceLabelValue(service ServiceItemExtended, key string) string { - return service.Labels[key] +func hasFuncService(service ServiceItemExtended, labelName string) bool { + 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 } diff --git a/vendor/github.com/containous/traefik-extra-service-fabric/servicefabric.go b/vendor/github.com/containous/traefik-extra-service-fabric/servicefabric.go index 79ed5ef57..8183d9480 100644 --- a/vendor/github.com/containous/traefik-extra-service-fabric/servicefabric.go +++ b/vendor/github.com/containous/traefik-extra-service-fabric/servicefabric.go @@ -69,34 +69,7 @@ func (p *Provider) updateConfig(configurationChan chan<- types.ConfigMessage, po log.Info("Checking service fabric config") } - services, err := getClusterServices(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) - + configuration, err := p.buildConfiguration(sfClient) if err != nil { return err } @@ -120,24 +93,39 @@ func (p *Provider) updateConfig(configurationChan chan<- types.ConfigMessage, po return nil } -func (p Provider) doesAppParamContain(app sf.ApplicationItem, key, shouldContain string) bool { - value := p.getApplicationParameter(app, key) - return strings.Contains(value, shouldContain) -} - -func (p Provider) getApplicationParameter(app sf.ApplicationItem, key string) string { - for _, param := range app.Parameters { - if param.Key == key { - return param.Value - } +func (p *Provider) buildConfiguration(sfClient sfClient) (*types.Configuration, error) { + var sfFuncMap = template.FuncMap{ + "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 } - log.Errorf("Parameter %s doesn't exist in app %s", key, app.Name) - return "" + + 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) getDefaultEndpoint(instance replicaInstance) string { +func getDefaultEndpoint(instance replicaInstance) string { id, data := instance.GetReplicaData() - endpoint, err := getDefaultEndpoint(data.Address) + endpoint, err := getReplicaDefaultEndpoint(data) if err != nil { log.Warnf("No default endpoint for replica %s in service %s endpointData: %s", id, data.Address) return "" @@ -145,16 +133,64 @@ func (p Provider) getDefaultEndpoint(instance replicaInstance) string { return endpoint } -func (p Provider) getNamedEndpoint(instance replicaInstance, endpointName string) string { - id, data := instance.GetReplicaData() - endpoint, err := getNamedEndpoint(data.Address, endpointName) +func getReplicaDefaultEndpoint(replicaData *sf.ReplicaItemBase) (string, error) { + endpoints, err := decodeEndpointData(replicaData.Address) 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 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) { apps, err := sfClient.GetApplications() if err != nil { @@ -236,7 +272,7 @@ func getValidInstances(sfClient sfClient, app sf.ApplicationItem, service sf.Ser return validInstances } -func getServicesWithLabelValueMap(services []ServiceItemExtended, key string) map[string][]ServiceItemExtended { +func getServices(services []ServiceItemExtended, key string) map[string][]ServiceItemExtended { result := map[string][]ServiceItemExtended{} for _, service := range services { if value, exists := service.Labels[key]; exists { @@ -250,7 +286,7 @@ func getServicesWithLabelValueMap(services []ServiceItemExtended, key string) ma return result } -func getServicesWithLabelValue(services []ServiceItemExtended, key, expectedValue string) []ServiceItemExtended { +func filterServicesByLabelValue(services []ServiceItemExtended, key, expectedValue string) []ServiceItemExtended { var srvWithLabel []ServiceItemExtended for _, service := range services { value, exists := service.Labels[key] @@ -261,25 +297,6 @@ func getServicesWithLabelValue(services []ServiceItemExtended, key, expectedValu 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 { _, data := instance.GetReplicaData() return data.ReplicaRole == "Primary" @@ -290,7 +307,7 @@ func isHealthy(instanceData *sf.ReplicaItemBase) bool { } func hasHTTPEndpoint(instanceData *sf.ReplicaItemBase) bool { - _, err := getDefaultEndpoint(instanceData.Address) + _, err := getReplicaDefaultEndpoint(instanceData) return err == nil } @@ -314,37 +331,6 @@ func decodeEndpointData(endpointData string) (map[string]string, error) { return endpoints, nil } -func getDefaultEndpoint(endpointData string) (string, error) { - endpoints, err := decodeEndpointData(endpointData) - 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 +func getBackendName(service ServiceItemExtended, partition PartitionItemExtended) string { + return provider.Normalize(service.Name + partition.PartitionInformation.ID) } diff --git a/vendor/github.com/containous/traefik-extra-service-fabric/servicefabric_tmpl.go b/vendor/github.com/containous/traefik-extra-service-fabric/servicefabric_tmpl.go index d7af11912..74be5033d 100644 --- a/vendor/github.com/containous/traefik-extra-service-fabric/servicefabric_tmpl.go +++ b/vendor/github.com/containous/traefik-extra-service-fabric/servicefabric_tmpl.go @@ -1,8 +1,8 @@ package servicefabric const tmpl = ` +{{$groupedServiceMap := getServices .Services "backend.group.name"}} [backends] - {{$groupedServiceMap := getServicesWithLabelValueMap .Services "backend.group.name"}} {{range $aggName, $aggServices := $groupedServiceMap }} [backends."{{$aggName}}"] {{range $service := $aggServices}} @@ -10,7 +10,7 @@ const tmpl = ` {{range $instance := $partition.Instances}} [backends."{{$aggName}}".servers."{{$service.ID}}-{{$instance.ID}}"] url = "{{getDefaultEndpoint $instance}}" - weight = {{getServiceLabelValueWithDefault $service "backend.group.weight" "1"}} + weight = {{getLabelValue $service "backend.group.weight" "1"}} {{end}} {{end}} {{end}} @@ -20,45 +20,45 @@ const tmpl = ` {{if eq $partition.ServiceKind "Stateless"}} [backends."{{$service.Name}}"] [backends."{{$service.Name}}".LoadBalancer] - {{if hasServiceLabel $service "backend.loadbalancer.method"}} - method = "{{getServiceLabelValue $service "backend.loadbalancer.method" }}" + {{if hasLabel $service "backend.loadbalancer.method"}} + method = "{{getLabelValue $service "backend.loadbalancer.method" "" }}" {{else}} method = "drr" {{end}} - {{if hasServiceLabel $service "backend.healthcheck"}} + {{if hasLabel $service "backend.healthcheck"}} [backends."{{$service.Name}}".healthcheck] - path = "{{getServiceLabelValue $service "backend.healthcheck"}}" - interval = "{{getServiceLabelValueWithDefault $service "backend.healthcheck.interval" "10s"}}" + path = "{{getLabelValue $service "backend.healthcheck" ""}}" + interval = "{{getLabelValue $service "backend.healthcheck.interval" "10s"}}" {{end}} - {{if hasServiceLabel $service "backend.loadbalancer.stickiness"}} + {{if hasLabel $service "backend.loadbalancer.stickiness"}} [backends."{{$service.Name}}".LoadBalancer.stickiness] {{end}} - {{if hasServiceLabel $service "backend.circuitbreaker"}} + {{if hasLabel $service "backend.circuitbreaker"}} [backends."{{$service.Name}}".circuitbreaker] - expression = "{{getServiceLabelValue $service "backend.circuitbreaker"}}" + expression = "{{getLabelValue $service "backend.circuitbreaker" ""}}" {{end}} - {{if hasServiceLabel $service "backend.maxconn.amount"}} + {{if hasLabel $service "backend.maxconn.amount"}} [backends."{{$service.Name}}".maxconn] - amount = {{getServiceLabelValue $service "backend.maxconn.amount"}} - {{if hasServiceLabel $service "backend.maxconn.extractorfunc"}} - extractorfunc = "{{getServiceLabelValue $service "backend.maxconn.extractorfunc"}}" + amount = {{getLabelValue $service "backend.maxconn.amount" ""}} + {{if hasLabel $service "backend.maxconn.extractorfunc"}} + extractorfunc = "{{getLabelValue $service "backend.maxconn.extractorfunc" ""}}" {{end}} {{end}} {{range $instance := $partition.Instances}} [backends."{{$service.Name}}".servers."{{$instance.ID}}"] url = "{{getDefaultEndpoint $instance}}" - weight = {{getServiceLabelValueWithDefault $service "backend.weight" "1"}} + weight = {{getLabelValue $service "backend.weight" "1"}} {{end}} {{else if eq $partition.ServiceKind "Stateful"}} {{range $replica := $partition.Replicas}} {{if isPrimary $replica}} - {{$backendName := (print $service.Name $partition.PartitionInformation.ID)}} + {{$backendName := getBackendName $service.Name $partition}} [backends."{{$backendName}}".servers."{{$replica.ID}}"] url = "{{getDefaultEndpoint $replica}}" weight = 1 @@ -81,11 +81,11 @@ const tmpl = ` [frontends."{{$groupName}}"] backend = "{{$groupName}}" - {{if hasServiceLabel $service "frontend.priority"}} + {{if hasLabel $service "frontend.priority"}} priority = 100 {{end}} - {{range $key, $value := getServiceLabelsWithPrefix $service "frontend.rule"}} + {{range $key, $value := getLabelsWithPrefix $service "frontend.rule"}} [frontends."{{$groupName}}".routes."{{$key}}"] rule = "{{$value}}" {{end}} @@ -97,27 +97,27 @@ const tmpl = ` [frontends."{{$service.Name}}"] backend = "{{$service.Name}}" - {{if hasServiceLabel $service "frontend.passHostHeader"}} - passHostHeader = {{getServiceLabelValue $service "frontend.passHostHeader" }} + {{if hasLabel $service "frontend.passHostHeader"}} + passHostHeader = {{getLabelValue $service "frontend.passHostHeader" ""}} {{end}} - {{if hasServiceLabel $service "frontend.whitelistSourceRange"}} - whitelistSourceRange = {{getServiceLabelValue $service "frontend.whitelistSourceRange" }} + {{if hasLabel $service "frontend.whitelistSourceRange"}} + whitelistSourceRange = {{getLabelValue $service "frontend.whitelistSourceRange" ""}} {{end}} - {{if hasServiceLabel $service "frontend.priority"}} - priority = {{getServiceLabelValue $service "frontend.priority"}} + {{if hasLabel $service "frontend.priority"}} + priority = {{getLabelValue $service "frontend.priority" ""}} {{end}} - {{if hasServiceLabel $service "frontend.basicAuth"}} - basicAuth = {{getServiceLabelValue $service "frontend.basicAuth"}} + {{if hasLabel $service "frontend.basicAuth"}} + basicAuth = {{getLabelValue $service "frontend.basicAuth" ""}} {{end}} - {{if hasServiceLabel $service "frontend.entryPoints"}} - entryPoints = {{getServiceLabelValue $service "frontend.entryPoints"}} + {{if hasLabel $service "frontend.entryPoints"}} + entryPoints = {{getLabelValue $service "frontend.entryPoints" ""}} {{end}} - {{range $key, $value := getServiceLabelsWithPrefix $service "frontend.rule"}} + {{range $key, $value := getLabelsWithPrefix $service "frontend.rule"}} [frontends."{{$service.Name}}".routes."{{$key}}"] rule = "{{$value}}" {{end}} @@ -126,11 +126,11 @@ const tmpl = ` {{range $partition := $service.Partitions}} {{$partitionId := $partition.PartitionInformation.ID}} - {{if hasServiceLabel $service "frontend.rule"}} + {{if hasLabel $service "frontend.rule"}} [frontends."{{$service.Name}}/{{$partitionId}}"] - backend = "{{$service.Name}}/{{$partitionId}}" + backend = "{{getBackendName $service.Name $partition}}" [frontends."{{$service.Name}}/{{$partitionId}}".routes.default] - rule = {{getServiceLabelValue $service "frontend.rule.partition.$partitionId"}} + rule = {{getLabelValue $service "frontend.rule.partition.$partitionId" ""}} {{end}} {{end}} diff --git a/vendor/github.com/jjcollinge/servicefabric/servicefabric.go b/vendor/github.com/jjcollinge/servicefabric/servicefabric.go index a46b3ef56..7500ee658 100644 --- a/vendor/github.com/jjcollinge/servicefabric/servicefabric.go +++ b/vendor/github.com/jjcollinge/servicefabric/servicefabric.go @@ -12,6 +12,7 @@ import ( "strings" ) +// DefaultAPIVersion is a default Service Fabric REST API version const DefaultAPIVersion = "3.0" // Client for Service Fabric.