diff --git a/cmd/configuration.go b/cmd/configuration.go index 49c9dd742..fdd99de30 100644 --- a/cmd/configuration.go +++ b/cmd/configuration.go @@ -220,7 +220,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { SamplingServerURL: "http://localhost:5778/sampling", SamplingType: "const", SamplingParam: 1.0, - LocalAgentHostPort: "127.0.0.1:6832", + LocalAgentHostPort: "127.0.0.1:6831", }, Zipkin: &zipkin.Config{ HTTPEndpoint: "http://localhost:9411/api/v1/spans", diff --git a/configuration/configuration.go b/configuration/configuration.go index ed80485ac..3eded8d85 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -327,7 +327,7 @@ func (gc *GlobalConfiguration) initTracing() { SamplingServerURL: "http://localhost:5778/sampling", SamplingType: "const", SamplingParam: 1.0, - LocalAgentHostPort: "127.0.0.1:6832", + LocalAgentHostPort: "127.0.0.1:6831", } } if gc.Tracing.Zipkin != nil { diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index b9e7206d5..3fb2c89f6 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -138,7 +138,7 @@ func TestSetEffectiveConfigurationTracing(t *testing.T) { SamplingServerURL: "http://localhost:5778/sampling", SamplingType: "const", SamplingParam: 1.0, - LocalAgentHostPort: "127.0.0.1:6832", + LocalAgentHostPort: "127.0.0.1:6831", }, Zipkin: nil, }, @@ -151,7 +151,7 @@ func TestSetEffectiveConfigurationTracing(t *testing.T) { SamplingServerURL: "http://localhost:5778/sampling", SamplingType: "const", SamplingParam: 1.0, - LocalAgentHostPort: "127.0.0.1:6832", + LocalAgentHostPort: "127.0.0.1:6831", }, }, expected: &tracing.Tracing{ @@ -173,7 +173,7 @@ func TestSetEffectiveConfigurationTracing(t *testing.T) { SamplingServerURL: "http://localhost:5778/sampling", SamplingType: "const", SamplingParam: 1.0, - LocalAgentHostPort: "127.0.0.1:6832", + LocalAgentHostPort: "127.0.0.1:6831", }, Zipkin: &zipkin.Config{ HTTPEndpoint: "http://powpow:9411/api/v1/spans", diff --git a/docs/basics.md b/docs/basics.md index b7ba02fbf..15204edef 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -262,7 +262,7 @@ This allows for setting headers such as `X-Script-Name` to be added to the reque !!! warning If the custom header name is the same as one header name of the request or response, it will be replaced. -In this example, all matches to the path `/cheese` will have the `X-Script-Name` header added to the proxied request, and the `X-Custom-Response-Header` added to the response. +In this example, all matches to the path `/cheese` will have the `X-Script-Name` header added to the proxied request and the `X-Custom-Response-Header` header added to the response. ```toml [frontends] @@ -276,7 +276,7 @@ In this example, all matches to the path `/cheese` will have the `X-Script-Name` rule = "PathPrefixStrip:/cheese" ``` -In this second example, all matches to the path `/cheese` will have the `X-Script-Name` header added to the proxied request, the `X-Custom-Request-Header` removed to the request and the `X-Custom-Response-Header` removed to the response. +In this second example, all matches to the path `/cheese` will have the `X-Script-Name` header added to the proxied request, the `X-Custom-Request-Header` header removed from the request, and the `X-Custom-Response-Header` header removed from the response. ```toml [frontends] @@ -323,12 +323,49 @@ In this example, traffic routed through the first frontend will have the `X-Fram A backend is responsible to load-balance the traffic coming from one or more frontends to a set of http servers. +#### Servers + +Servers are simply defined using a `url`. You can also apply a custom `weight` to each server (this will be used by load-balancing). + +!!! note + Paths in `url` are ignored. Use `Modifier` to specify paths instead. + +Here is an example of backends and servers definition: + +```toml +[backends] + [backends.backend1] + # ... + [backends.backend1.servers.server1] + url = "http://172.17.0.2:80" + weight = 10 + [backends.backend1.servers.server2] + url = "http://172.17.0.3:80" + weight = 1 + [backends.backend2] + # ... + [backends.backend2.servers.server1] + url = "http://172.17.0.4:80" + weight = 1 + [backends.backend2.servers.server2] + url = "http://172.17.0.5:80" + weight = 2 +``` + +- Two backends are defined: `backend1` and `backend2` +- `backend1` will forward the traffic to two servers: `http://172.17.0.2:80"` with weight `10` and `http://172.17.0.3:80` with weight `1`. +- `backend2` will forward the traffic to two servers: `http://172.17.0.4:80"` with weight `1` and `http://172.17.0.5:80` with weight `2`. + +#### Load-balancing + Various methods of load-balancing are supported: - `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. +#### Circuit breakers + A circuit breaker can also be applied to a backend, preventing high loads on failing servers. Initial state is Standby. CB observes the statistics and does not modify the request. In case the condition matches, CB enters Tripped state, where it responds with predefined code or redirects to another frontend. @@ -346,6 +383,26 @@ For example: - `LatencyAtQuantileMS(50.0) > 50`: watch latency at quantile in milliseconds. - `ResponseCodeRatio(500, 600, 0, 600) > 0.5`: ratio of response codes in ranges [500-600) and [0-600). +Here is an example of backends and servers definition: + +```toml +[backends] + [backends.backend1] + [backends.backend1.circuitbreaker] + expression = "NetworkErrorRatio() > 0.5" + [backends.backend1.servers.server1] + url = "http://172.17.0.2:80" + weight = 10 + [backends.backend1.servers.server2] + url = "http://172.17.0.3:80" + weight = 1 +``` + +- `backend1` will forward the traffic to two servers: `http://172.17.0.2:80"` with weight `10` and `http://172.17.0.3:80` with weight `1` using default `wrr` load-balancing strategy. +- a circuit breaker is added on `backend1` using the expression `NetworkErrorRatio() > 0.5`: watch error ratio over 10 second sliding window + +#### Maximum connections + 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. @@ -357,13 +414,14 @@ For example: [backends.backend1.maxconn] amount = 10 extractorfunc = "request.host" + # ... ``` - `backend1` will return `HTTP code 429 Too Many Requests` if there are already 10 requests in progress for the same Host header. - Another possible value for `extractorfunc` is `client.ip` which will categorize requests based on client source ip. - Lastly `extractorfunc` can take the value of `request.header.ANY_HEADER` which will categorize requests based on `ANY_HEADER` that you provide. -### Sticky sessions +#### Sticky sessions Sticky sessions are supported with both load balancers. When sticky sessions are enabled, a cookie is set on the initial request. @@ -371,7 +429,6 @@ The default cookie name is an abbreviation of a sha1 (ex: `_1d52e`). On subsequent requests, the client will be directed to the backend stored in the cookie if it is still healthy. If not, a new backend will be assigned. - ```toml [backends] [backends.backend1] @@ -395,10 +452,10 @@ The deprecated way: sticky = true ``` -### Health Check +#### Health Check A health check can be configured in order to remove a backend from LB rotation as long as it keeps returning HTTP status codes other than `200 OK` to HTTP GET requests periodically carried out by Traefik. -The check is defined by a pathappended to the backend URL and an interval (given in a format understood by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration)) specifying how often the health check should be executed (the default being 30 seconds). +The check is defined by a path appended to the backend URL and an interval (given in a format understood by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration)) specifying how often the health check should be executed (the default being 30 seconds). Each backend must respond to the health check within 5 seconds. By default, the port of the backend server is used, however, this may be overridden. @@ -438,43 +495,6 @@ Additional http headers and hostname to healthcheck request can be specified, fo myheader2 = "bar" ``` -### Servers - -Servers are simply defined using a `url`. You can also apply a custom `weight` to each server (this will be used by load-balancing). - -!!! note - Paths in `url` are ignored. Use `Modifier` to specify paths instead. - -Here is an example of backends and servers definition: - -```toml -[backends] - [backends.backend1] - [backends.backend1.circuitbreaker] - expression = "NetworkErrorRatio() > 0.5" - [backends.backend1.servers.server1] - url = "http://172.17.0.2:80" - weight = 10 - [backends.backend1.servers.server2] - url = "http://172.17.0.3:80" - weight = 1 - [backends.backend2] - [backends.backend2.LoadBalancer] - method = "drr" - [backends.backend2.servers.server1] - url = "http://172.17.0.4:80" - weight = 1 - [backends.backend2.servers.server2] - url = "http://172.17.0.5:80" - weight = 2 -``` - -- Two backends are defined: `backend1` and `backend2` -- `backend1` will forward the traffic to two servers: `http://172.17.0.2:80"` with weight `10` and `http://172.17.0.3:80` with weight `1` using default `wrr` load-balancing strategy. -- `backend2` will forward the traffic to two servers: `http://172.17.0.4:80"` with weight `1` and `http://172.17.0.5:80` with weight `2` using `drr` load-balancing strategy. -- a circuit breaker is added on `backend1` using the expression `NetworkErrorRatio() > 0.5`: watch error ratio over 10 second sliding window - - ## Configuration Træfik's configuration has two parts: diff --git a/docs/configuration/backends/docker.md b/docs/configuration/backends/docker.md index 27c9167ec..729ed7833 100644 --- a/docs/configuration/backends/docker.md +++ b/docs/configuration/backends/docker.md @@ -196,6 +196,7 @@ Labels can be used on containers to override default behavior. | Label | Description | |------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `traefik.docker.network` | Set the docker network to use for connections to this container. [1] | +| `traefik.domain` | Default domain used for frontend rules. | | `traefik.enable=false` | Disable this container in Træfik | | `traefik.port=80` | Register this port. Useful when the container exposes multiples ports. | | `traefik.protocol=https` | Override the default `http` protocol | @@ -285,6 +286,7 @@ Segment labels override the default behavior. | Label | Description | |---------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------| +| `traefik..domain` | Default domain used for frontend rules. | | `traefik..port=PORT` | Overrides `traefik.port`. If several ports need to be exposed, the segment labels could be used. | | `traefik..protocol` | Overrides `traefik.protocol`. | | `traefik..weight` | Assign this segment weight. Overrides `traefik.weight`. | diff --git a/docs/configuration/backends/ecs.md b/docs/configuration/backends/ecs.md index 51e61d088..d94732aff 100644 --- a/docs/configuration/backends/ecs.md +++ b/docs/configuration/backends/ecs.md @@ -33,6 +33,7 @@ clusters = ["default"] watch = true # Default domain used. +# Can be overridden by setting the "traefik.domain" label. # # Optional # Default: "" @@ -135,6 +136,7 @@ Labels can be used on task containers to override default behaviour: | Label | Description | |------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `traefik.domain` | Default domain used for frontend rules. | | `traefik.enable=false` | Disable this container in Træfik | | `traefik.port=80` | Override the default `port` value. Overrides `NetworkBindings` from Docker Container | | `traefik.protocol=https` | Override the default `http` protocol | diff --git a/docs/configuration/backends/file.md b/docs/configuration/backends/file.md index 785155d17..58e950651 100644 --- a/docs/configuration/backends/file.md +++ b/docs/configuration/backends/file.md @@ -140,19 +140,20 @@ Træfik can be configured with a file. # ... ``` -## Configuration mode +## Configuration Mode -You have three choices: +You have two choices: -- [Simple](/configuration/backends/file/#simple) -- [Rules in a Separate File](/configuration/backends/file/#rules-in-a-separate-file) -- [Multiple `.toml` Files](/configuration/backends/file/#multiple-toml-files) +- [Rules in Træfik configuration file](/configuration/backends/file/#rules-in-trfik-configuration-file) +- [Rules in dedicated files](/configuration/backends/file/#rules-in-dedicated-files) To enable the file backend, you must either pass the `--file` option to the Træfik binary or put the `[file]` section (with or without inner settings) in the configuration file. The configuration file allows managing both backends/frontends and HTTPS certificates (which are not [Let's Encrypt](https://letsencrypt.org) certificates generated through Træfik). -### Simple +TOML templating can be used if rules are not defined in the Træfik configuration file. + +### Rules in Træfik Configuration File Add your configuration at the end of the global configuration file `traefik.toml`: @@ -197,9 +198,16 @@ defaultEntryPoints = ["http", "https"] Adding certificates directly to the entryPoint is still maintained but certificates declared in this way cannot be managed dynamically. It's recommended to use the file provider to declare certificates. -### Rules in a Separate File +!!! warning + TOML templating cannot be used if rules are defined in the Træfik configuration file. -Put your rules in a separate file, for example `rules.toml`: +### Rules in Dedicated Files + +Træfik allows defining rules in one or more separate files. + +#### One Separate File + +You have to specify the file path in the `file.filename` option. ```toml # traefik.toml @@ -213,8 +221,31 @@ defaultEntryPoints = ["http", "https"] [file] filename = "rules.toml" + watch = true ``` +The option `file.watch` allows Træfik to watch file changes automatically. + +#### Multiple Separated Files + +You could have multiple `.toml` files in a directory (and recursively in its sub-directories): + +```toml +[file] + directory = "/path/to/config/" + watch = true +``` + +The option `file.watch` allows Træfik to watch file changes automatically. + +#### Separate Files Content + +If you are defining rules in one or more separate files, you can use two formats. + +##### Simple Format + +Backends, Frontends and TLS certificates are defined one at time, as described in the file `rules.toml`: + ```toml # rules.toml [backends] @@ -239,18 +270,34 @@ defaultEntryPoints = ["http", "https"] # ... ``` -### Multiple `.toml` Files +##### TOML Templating -You could have multiple `.toml` files in a directory (and recursively in its sub-directories): +!!! warning + TOML templating can only be used **if rules are defined in one or more separate files**. + Templating will not work in the Træfik configuration file. + +Træfik allows using TOML templating. + +Thus, it's possible to define easily lot of Backends, Frontends and TLS certificates as described in the file `template-rules.toml` : ```toml -[file] - directory = "/path/to/config/" -``` +# template-rules.toml +[backends] +{{ range $i, $e := until 100 }} + [backends.backend{{ $e }}] + #... +{{ end }} -If you want Træfik to watch file changes automatically, just add: +[frontends] +{{ range $i, $e := until 100 }} + [frontends.frontend{{ $e }}] + #... +{{ end }} -```toml -[file] - watch = true + +# HTTPS certificate +{{ range $i, $e := until 100 }} +[[tls]] + #... +{{ end }} ``` diff --git a/docs/configuration/backends/kubernetes.md b/docs/configuration/backends/kubernetes.md index caa5d3f57..e15865f81 100644 --- a/docs/configuration/backends/kubernetes.md +++ b/docs/configuration/backends/kubernetes.md @@ -239,7 +239,7 @@ The following security annotations are applicable on the Ingress object: ### Authentication -Is possible to add additional authentication annotations to the Ingress object. +Additional authentication annotations can be added to the Ingress object. The source of the authentication is a Secret object that contains the credentials. | Annotation | Description | @@ -253,3 +253,12 @@ The following limitations hold: - The realm is not configurable; the only supported (and default) value is `traefik`. - The Secret must contain a single file only. + +### TLS certificates management + +TLS certificates can be managed in Secrets objects. +More information are available in the [User Guide](/user-guide/kubernetes/#add-a-tls-certificate-to-the-ingress). + +!!! note + Only TLS certificates provided by users can be stored in Kubernetes Secrets. + [Let's Encrypt](https://letsencrypt.org) certificates cannot be managed in Kubernets Secrets yet. \ No newline at end of file diff --git a/docs/configuration/backends/marathon.md b/docs/configuration/backends/marathon.md index facfa66cf..c47ca2e92 100644 --- a/docs/configuration/backends/marathon.md +++ b/docs/configuration/backends/marathon.md @@ -171,6 +171,7 @@ The following labels can be defined on Marathon applications. They adjust the be | Label | Description | |------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `traefik.domain` | Default domain used for frontend rules. | | `traefik.enable=false` | Disable this container in Træfik | | `traefik.port=80` | Register this port. Useful when the container exposes multiples ports. | | `traefik.portIndex=1` | Register port by index in the application's ports array. Useful when the application exposes multiple ports. | @@ -256,6 +257,7 @@ Segment labels override the default behavior. | Label | Description | |---------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------| +| `traefik..domain` | Default domain used for frontend rules. | | `traefik..portIndex=1` | Create a service binding with frontend/backend using this port index. Overrides `traefik.portIndex`. | | `traefik..port=PORT` | Overrides `traefik.port`. If several ports need to be exposed, the service labels could be used. | | `traefik..protocol=http` | Overrides `traefik.protocol`. | diff --git a/docs/configuration/backends/mesos.md b/docs/configuration/backends/mesos.md index df982a769..9554705e9 100644 --- a/docs/configuration/backends/mesos.md +++ b/docs/configuration/backends/mesos.md @@ -108,6 +108,7 @@ The following labels can be defined on Mesos tasks. They adjust the behavior for | Label | Description | |------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `traefik.domain` | Default domain used for frontend rules. | | `traefik.enable=false` | Disable this container in Træfik | | `traefik.port=80` | Register this port. Useful when the container exposes multiples ports. | | `traefik.portIndex=1` | Register port by index in the application's ports array. Useful when the application exposes multiple ports. | diff --git a/docs/configuration/backends/rancher.md b/docs/configuration/backends/rancher.md index 81b33875d..c889d0382 100644 --- a/docs/configuration/backends/rancher.md +++ b/docs/configuration/backends/rancher.md @@ -140,6 +140,7 @@ Labels can be used on task containers to override default behavior: | Label | Description | |------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `traefik.domain` | Default domain used for frontend rules. | | `traefik.enable=false` | Disable this container in Træfik | | `traefik.port=80` | Register this port. Useful when the container exposes multiples ports. | | `traefik.protocol=https` | Override the default `http` protocol | @@ -223,6 +224,7 @@ Segment labels override the default behavior. | Label | Description | |---------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------| +| `traefik..domain` | Default domain used for frontend rules. | | `traefik..port=PORT` | Overrides `traefik.port`. If several ports need to be exposed, the segment labels could be used. | | `traefik..protocol` | Overrides `traefik.protocol`. | | `traefik..weight` | Assign this segment weight. Overrides `traefik.weight`. | diff --git a/docs/configuration/backends/servicefabric.md b/docs/configuration/backends/servicefabric.md index aaf96d8b1..37c4f99b5 100644 --- a/docs/configuration/backends/servicefabric.md +++ b/docs/configuration/backends/servicefabric.md @@ -61,7 +61,7 @@ Here is an example of an extension setting Træfik labels: - + diff --git a/docs/configuration/entrypoints.md b/docs/configuration/entrypoints.md index 23f30733c..2ffb1db9b 100644 --- a/docs/configuration/entrypoints.md +++ b/docs/configuration/entrypoints.md @@ -120,7 +120,7 @@ Compress:true WhiteList.SourceRange:10.42.0.0/16,152.89.1.33/32,afed:be44::/16 WhiteList.UseXForwardedFor:true ProxyProtocol.TrustedIPs:192.168.0.1 -ProxyProtocol.Insecure:tue +ProxyProtocol.Insecure:true ForwardedHeaders.TrustedIPs:10.0.0.3/24,20.0.0.3/24 Auth.Basic.Users:test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0 Auth.Digest.Users:test:traefik:a2688e031edb4be6a3797f3882655c05,test2:traefik:518845800f9e2bfb1f1f740ec24f074e diff --git a/docs/configuration/tracing.md b/docs/configuration/tracing.md index b8d2d50a3..60ca7aba2 100644 --- a/docs/configuration/tracing.md +++ b/docs/configuration/tracing.md @@ -48,11 +48,14 @@ Træfik supports two backends: Jaeger and Zipkin. # Local Agent Host Port instructs reporter to send spans to jaeger-agent at this address # - # Default: "127.0.0.1:6832" + # Default: "127.0.0.1:6831" # - localAgentHostPort = "127.0.0.1:6832" + localAgentHostPort = "127.0.0.1:6831" ``` +!!! warning + Træfik is only able to send data over compact thrift protocol to the [Jaeger agent](https://www.jaegertracing.io/docs/deployment/#agent). + ## Zipkin ```toml diff --git a/docs/index.md b/docs/index.md index f402a3e70..99a896c01 100644 --- a/docs/index.md +++ b/docs/index.md @@ -19,13 +19,14 @@ Telling Træfik where your orchestrator is could be the _only_ configuration ste Imagine that you have deployed a bunch of microservices with the help of an orchestrator (like Swarm or Kubernetes) or a service registry (like etcd or consul). Now you want users to access these microservices, and you need a reverse proxy. -Traditional reverse-proxies require that you configure _each_ route that will connect paths and subdomains to _each_ microservice. In an environment where you add, remove, kill, upgrade, or scale your services _many_ times a day, the task of keeping the routes up to date becomes tedious. +Traditional reverse-proxies require that you configure _each_ route that will connect paths and subdomains to _each_ microservice. +In an environment where you add, remove, kill, upgrade, or scale your services _many_ times a day, the task of keeping the routes up to date becomes tedious. **This is when Træfik can help you!** -Træfik listens to your service registry/orchestrator API and instantly generates the routes so your microservices are connected to the outside world -- without further intervention from your part. +Træfik listens to your service registry/orchestrator API and instantly generates the routes so your microservices are connected to the outside world -- without further intervention from your part. -**Run Træfik and let it do the work for you!** +**Run Træfik and let it do the work for you!** _(But if you'd rather configure some of your routes manually, Træfik supports that too!)_ ![Architecture](img/architecture.png) @@ -90,19 +91,19 @@ services: Start your `reverse-proxy` with the following command: ```shell -docker-compose up -d reverse-proxy +docker-compose up -d reverse-proxy ``` You can open a browser and go to [http://localhost:8080](http://localhost:8080) to see Træfik's dashboard (we'll go back there once we have launched a service in step 2). -### 2 — Launch a Service — Træfik Detects It and Creates a Route for You +### 2 — Launch a Service — Træfik Detects It and Creates a Route for You -Now that we have a Træfik instance up and running, we will deploy new services. +Now that we have a Træfik instance up and running, we will deploy new services. -Edit your `docker-compose.yml` file and add the following at the end of your file. +Edit your `docker-compose.yml` file and add the following at the end of your file. ```yaml -# ... +# ... whoami: image: emilevauge/whoami #A container that exposes an API to show it's IP address labels: @@ -112,7 +113,7 @@ Edit your `docker-compose.yml` file and add the following at the end of your fil The above defines `whoami`: a simple web service that outputs information about the machine it is deployed on (its IP address, host, and so on). Start the `whoami` service with the following command: - + ```shell docker-compose up -d whoami ``` @@ -135,9 +136,9 @@ IP: 172.27.0.3 ### 3 — Launch More Instances — Traefik Load Balances Them Run more instances of your `whoami` service with the following command: - + ```shell -docker-compose up -d --scale whoami=2 +docker-compose up -d --scale whoami=2 ``` Go back to your browser ([http://localhost:8080](http://localhost:8080)) and see that Træfik has automatically detected the new instance of the container. @@ -164,9 +165,10 @@ IP: 172.27.0.4 ### 4 — Enjoy Træfik's Magic -Now that you have a basic understanding of how Træfik can automatically create the routes to your services and load balance them, it might be time to dive into [the documentation](https://docs.traefik.io/) and let Træfik work for you! Whatever your infrastructure is, there is probably [an available Træfik backend](https://docs.traefik.io/configuration/backends/available) that will do the job. +Now that you have a basic understanding of how Træfik can automatically create the routes to your services and load balance them, it might be time to dive into [the documentation](/) and let Træfik work for you! +Whatever your infrastructure is, there is probably [an available Træfik backend](/#supported-backends) that will do the job. -Our recommendation would be to see for yourself how simple it is to enable HTTPS with [Træfik's let's encrypt integration](https://docs.traefik.io/user-guide/examples/#lets-encrypt-support) using the dedicated [user guide](https://docs.traefik.io/user-guide/docker-and-lets-encrypt/). +Our recommendation would be to see for yourself how simple it is to enable HTTPS with [Træfik's let's encrypt integration](/user-guide/examples/#lets-encrypt-support) using the dedicated [user guide](/user-guide/docker-and-lets-encrypt/). ## Resources @@ -196,4 +198,4 @@ Using the tiny Docker image: ```shell docker run -d -p 8080:8080 -p 80:80 -v $PWD/traefik.toml:/etc/traefik/traefik.toml traefik -``` \ No newline at end of file +``` diff --git a/examples/quickstart/README.md b/examples/quickstart/README.md index 7deb4faf6..7c3817a21 100644 --- a/examples/quickstart/README.md +++ b/examples/quickstart/README.md @@ -101,6 +101,7 @@ IP: 172.27.0.4 ### 4 — Enjoy Træfik's Magic -Now that you have a basic understanding of how Træfik can automatically create the routes to your services and load balance them, it might be time to dive into [the documentation](https://docs.traefik.io/) and let Træfik work for you! Whatever your infrastructure is, there is probably [an available Træfik backend](https://docs.traefik.io/configuration/backends/available) that will do the job. +Now that you have a basic understanding of how Træfik can automatically create the routes to your services and load balance them, it might be time to dive into [the documentation](https://docs.traefik.io/) and let Træfik work for you! +Whatever your infrastructure is, there is probably [an available Træfik backend](https://docs.traefik.io/#supported-backends) that will do the job. Our recommendation would be to see for yourself how simple it is to enable HTTPS with [Træfik's let's encrypt integration](https://docs.traefik.io/user-guide/examples/#lets-encrypt-support) using the dedicated [user guide](https://docs.traefik.io/user-guide/docker-and-lets-encrypt/). \ No newline at end of file diff --git a/integration/consul_test.go b/integration/consul_test.go index 0761709bc..7014e5149 100644 --- a/integration/consul_test.go +++ b/integration/consul_test.go @@ -194,12 +194,14 @@ func (s *ConsulSuite) TestNominalConfiguration(c *check.C) { c.Assert(err, checker.IsNil) req, err = http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/test2", nil) - try.Request(req, 500*time.Millisecond, try.StatusCodeIs(http.StatusNotFound)) + c.Assert(err, checker.IsNil) + err = try.Request(req, 500*time.Millisecond, try.StatusCodeIs(http.StatusNotFound)) c.Assert(err, checker.IsNil) req, err = http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) req.Host = "test2.localhost" - try.Request(req, 500*time.Millisecond, try.StatusCodeIs(http.StatusNotFound)) + err = try.Request(req, 500*time.Millisecond, try.StatusCodeIs(http.StatusNotFound)) c.Assert(err, checker.IsNil) } diff --git a/middlewares/accesslog/logger.go b/middlewares/accesslog/logger.go index 09cab0239..4754d7bba 100644 --- a/middlewares/accesslog/logger.go +++ b/middlewares/accesslog/logger.go @@ -267,23 +267,26 @@ func (l *LogHandler) redactHeaders(headers http.Header, fields logrus.Fields, pr } func (l *LogHandler) keepAccessLog(statusCode, retryAttempts int) bool { - switch { - case l.config.Filters == nil: + if l.config.Filters == nil { // no filters were specified return true - case len(l.httpCodeRanges) == 0 && l.config.Filters.RetryAttempts == false: + } + + if len(l.httpCodeRanges) == 0 && !l.config.Filters.RetryAttempts { // empty filters were specified, e.g. by passing --accessLog.filters only (without other filter options) return true - case l.httpCodeRanges.Contains(statusCode): - return true - case l.config.Filters.RetryAttempts == true && retryAttempts > 0: - return true - default: - return false } -} -//------------------------------------------------------------------------------------------------- + if l.httpCodeRanges.Contains(statusCode) { + return true + } + + if l.config.Filters.RetryAttempts && retryAttempts > 0 { + return true + } + + return false +} var requestCounter uint64 // Request ID diff --git a/middlewares/auth/forward.go b/middlewares/auth/forward.go index e4eea0976..3cdb85d5b 100644 --- a/middlewares/auth/forward.go +++ b/middlewares/auth/forward.go @@ -25,6 +25,7 @@ func Forward(config *types.Forward, w http.ResponseWriter, r *http.Request, next return http.ErrUseLastResponse }, } + if config.TLS != nil { tlsConfig, err := config.TLS.CreateTLSConfig() if err != nil { @@ -32,10 +33,12 @@ func Forward(config *types.Forward, w http.ResponseWriter, r *http.Request, next w.WriteHeader(http.StatusInternalServerError) return } + httpClient.Transport = &http.Transport{ TLSClientConfig: tlsConfig, } } + forwardReq, err := http.NewRequest(http.MethodGet, config.Address, nil) tracing.LogRequest(tracing.GetSpan(r), forwardReq) if err != nil { @@ -68,6 +71,8 @@ func Forward(config *types.Forward, w http.ResponseWriter, r *http.Request, next if forwardResponse.StatusCode < http.StatusOK || forwardResponse.StatusCode >= http.StatusMultipleChoices { log.Debugf("Remote error %s. StatusCode: %d", config.Address, forwardResponse.StatusCode) + utils.CopyHeaders(w.Header(), forwardResponse.Header) + // Grab the location header, if any. redirectURL, err := forwardResponse.Location() @@ -79,12 +84,7 @@ func Forward(config *types.Forward, w http.ResponseWriter, r *http.Request, next } } else if redirectURL.String() != "" { // Set the location in our response if one was sent back. - w.Header().Add("Location", redirectURL.String()) - } - - // Pass any Set-Cookie headers the forward auth server provides - for _, cookie := range forwardResponse.Cookies() { - w.Header().Add("Set-Cookie", cookie.String()) + w.Header().Set("Location", redirectURL.String()) } tracing.LogResponseCode(tracing.GetSpan(r), forwardResponse.StatusCode) diff --git a/middlewares/auth/forward_test.go b/middlewares/auth/forward_test.go index 05ffdba84..a52014420 100644 --- a/middlewares/auth/forward_test.go +++ b/middlewares/auth/forward_test.go @@ -11,6 +11,7 @@ import ( "github.com/containous/traefik/testhelpers" "github.com/containous/traefik/types" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/urfave/negroni" ) @@ -110,7 +111,6 @@ func TestForwardAuthRedirect(t *testing.T) { assert.Equal(t, http.StatusFound, res.StatusCode, "they should be equal") location, err := res.Location() - assert.NoError(t, err, "there should be no error") assert.Equal(t, "http://example.com/redirect-test", location.String(), "they should be equal") @@ -119,10 +119,11 @@ func TestForwardAuthRedirect(t *testing.T) { assert.NotEmpty(t, string(body), "there should be something in the body") } -func TestForwardAuthCookie(t *testing.T) { +func TestForwardAuthFailResponseHeaders(t *testing.T) { authTs := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cookie := &http.Cookie{Name: "example", Value: "testing", Path: "/"} http.SetCookie(w, cookie) + w.Header().Add("X-Foo", "bar") http.Error(w, "Forbidden", http.StatusForbidden) })) defer authTs.Close() @@ -142,23 +143,36 @@ func TestForwardAuthCookie(t *testing.T) { ts := httptest.NewServer(n) defer ts.Close() - client := &http.Client{} req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil) + client := &http.Client{} res, err := client.Do(req) assert.NoError(t, err, "there should be no error") assert.Equal(t, http.StatusForbidden, res.StatusCode, "they should be equal") + require.Len(t, res.Cookies(), 1) for _, cookie := range res.Cookies() { assert.Equal(t, "testing", cookie.Value, "they should be equal") } + expectedHeaders := http.Header{ + "Content-Length": []string{"10"}, + "Content-Type": []string{"text/plain; charset=utf-8"}, + "X-Foo": []string{"bar"}, + "Set-Cookie": []string{"example=testing; Path=/"}, + "X-Content-Type-Options": []string{"nosniff"}, + } + + assert.Len(t, res.Header, 6) + for key, value := range expectedHeaders { + assert.Equal(t, value, res.Header[key]) + } + body, err := ioutil.ReadAll(res.Body) assert.NoError(t, err, "there should be no error") assert.Equal(t, "Forbidden\n", string(body), "they should be equal") } func Test_writeHeader(t *testing.T) { - testCases := []struct { name string headers map[string]string diff --git a/middlewares/errorpages/error_pages.go b/middlewares/errorpages/error_pages.go index 49c218639..d73aa1eb0 100644 --- a/middlewares/errorpages/error_pages.go +++ b/middlewares/errorpages/error_pages.go @@ -3,15 +3,17 @@ package errorpages import ( "bufio" "bytes" + "errors" + "fmt" "net" "net/http" + "net/url" "strconv" "strings" "github.com/containous/traefik/log" "github.com/containous/traefik/middlewares" "github.com/containous/traefik/types" - "github.com/pkg/errors" "github.com/vulcand/oxy/forward" "github.com/vulcand/oxy/utils" ) @@ -75,8 +77,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request, next http. recorder := newResponseRecorder(w) next.ServeHTTP(recorder, req) - w.WriteHeader(recorder.GetCode()) - // check the recorder code against the configured http status code ranges for _, block := range h.httpCodeRanges { if recorder.GetCode() >= block[0] && recorder.GetCode() <= block[1] { @@ -88,20 +88,43 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request, next http. query = strings.Replace(query, "{status}", strconv.Itoa(recorder.GetCode()), -1) } - if newReq, err := http.NewRequest(http.MethodGet, h.backendURL+query, nil); err != nil { + pageReq, err := newRequest(h.backendURL + query) + if err != nil { + log.Error(err) + w.WriteHeader(recorder.GetCode()) w.Write([]byte(http.StatusText(recorder.GetCode()))) - } else { - h.backendHandler.ServeHTTP(w, newReq) + return } + + utils.CopyHeaders(pageReq.Header, req.Header) + utils.CopyHeaders(w.Header(), recorder.Header()) + w.WriteHeader(recorder.GetCode()) + h.backendHandler.ServeHTTP(w, pageReq) return } } // did not catch a configured status code so proceed with the request utils.CopyHeaders(w.Header(), recorder.Header()) + w.WriteHeader(recorder.GetCode()) w.Write(recorder.GetBody().Bytes()) } +func newRequest(baseURL string) (*http.Request, error) { + u, err := url.Parse(baseURL) + if err != nil { + return nil, fmt.Errorf("error pages: error when parse URL: %v", err) + } + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return nil, fmt.Errorf("error pages: error when create query: %v", err) + } + + req.RequestURI = u.RequestURI() + return req, nil +} + type responseRecorder interface { http.ResponseWriter http.Flusher diff --git a/middlewares/errorpages/error_pages_test.go b/middlewares/errorpages/error_pages_test.go index 4700e5b3b..2264dc336 100644 --- a/middlewares/errorpages/error_pages_test.go +++ b/middlewares/errorpages/error_pages_test.go @@ -65,7 +65,7 @@ func TestHandler(t *testing.T) { errorPage: &types.ErrorPage{Backend: "error", Query: "/{status}", Status: []string{"503-503"}}, backendCode: http.StatusServiceUnavailable, backendErrorHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.RequestURI() == "/"+strconv.Itoa(503) { + if r.RequestURI == "/503" { fmt.Fprintln(w, "My 503 page.") } else { fmt.Fprintln(w, "Failed") @@ -82,7 +82,7 @@ func TestHandler(t *testing.T) { errorPage: &types.ErrorPage{Backend: "error", Query: "/{status}", Status: []string{"503"}}, backendCode: http.StatusServiceUnavailable, backendErrorHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.RequestURI() == "/"+strconv.Itoa(503) { + if r.RequestURI == "/503" { fmt.Fprintln(w, "My 503 page.") } else { fmt.Fprintln(w, "Failed") @@ -239,7 +239,7 @@ func TestHandlerOldWay(t *testing.T) { func TestHandlerOldWayIntegration(t *testing.T) { errorPagesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.RequestURI() == "/"+strconv.Itoa(503) { + if r.URL.RequestURI() == "/503" { fmt.Fprintln(w, "My 503 page.") } else { fmt.Fprintln(w, "Test Server") @@ -318,6 +318,7 @@ func TestHandlerOldWayIntegration(t *testing.T) { require.NoError(t, err) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Foo", "bar") w.WriteHeader(test.backendCode) fmt.Fprintln(w, http.StatusText(test.backendCode)) }) @@ -330,6 +331,7 @@ func TestHandlerOldWayIntegration(t *testing.T) { n.ServeHTTP(recorder, req) test.validate(t, recorder) + assert.Equal(t, "bar", recorder.Header().Get("X-Foo"), "missing header") }) } } diff --git a/middlewares/ip_whitelister.go b/middlewares/ip_whitelister.go index 60ff8a30e..a352aad12 100644 --- a/middlewares/ip_whitelister.go +++ b/middlewares/ip_whitelister.go @@ -38,21 +38,15 @@ func NewIPWhiteLister(whiteList []string, useXForwardedFor bool) (*IPWhiteLister } func (wl *IPWhiteLister) handle(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - allowed, ip, err := wl.whiteLister.IsAuthorized(r) + err := wl.whiteLister.IsAuthorized(r) if err != nil { - tracing.SetErrorAndDebugLog(r, "request %+v matched none of the white list - rejecting", r) + tracing.SetErrorAndDebugLog(r, "request %+v - rejecting: %v", r, err) reject(w) return } - if allowed { - tracing.SetErrorAndDebugLog(r, "request %+v matched white list %s - passing", r, wl.whiteLister) - next.ServeHTTP(w, r) - return - } - - tracing.SetErrorAndDebugLog(r, "source-IP %s matched none of the white list - rejecting", ip) - reject(w) + tracing.SetErrorAndDebugLog(r, "request %+v matched white list %s - passing", r, wl.whiteLister) + next.ServeHTTP(w, r) } func (wl *IPWhiteLister) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { @@ -63,5 +57,8 @@ func reject(w http.ResponseWriter) { statusCode := http.StatusForbidden w.WriteHeader(statusCode) - w.Write([]byte(http.StatusText(statusCode))) + _, err := w.Write([]byte(http.StatusText(statusCode))) + if err != nil { + log.Error(err) + } } diff --git a/middlewares/ip_whitelister_test.go b/middlewares/ip_whitelister_test.go index 33b0c56e5..07ee800ba 100644 --- a/middlewares/ip_whitelister_test.go +++ b/middlewares/ip_whitelister_test.go @@ -88,6 +88,13 @@ func TestIPWhiteLister_ServeHTTP(t *testing.T) { xForwardedFor: []string{"30.30.30.30", "40.40.40.40"}, expected: 200, }, + { + desc: "authorized with only one X-Forwarded-For", + whiteList: []string{"30.30.30.30"}, + useXForwardedFor: true, + xForwardedFor: []string{"30.30.30.30"}, + expected: 200, + }, { desc: "non authorized with X-Forwarded-For", whiteList: []string{"30.30.30.30"}, diff --git a/middlewares/tracing/tracing.go b/middlewares/tracing/tracing.go index eeca54877..487581149 100644 --- a/middlewares/tracing/tracing.go +++ b/middlewares/tracing/tracing.go @@ -73,7 +73,10 @@ func (t *Tracing) IsEnabled() bool { // Close tracer func (t *Tracing) Close() { if t.closer != nil { - t.closer.Close() + err := t.closer.Close() + if err != nil { + log.Warn(err) + } } } @@ -104,10 +107,13 @@ func GetSpan(r *http.Request) opentracing.Span { // InjectRequestHeaders used to inject OpenTracing headers into the request func InjectRequestHeaders(r *http.Request) { if span := GetSpan(r); span != nil { - opentracing.GlobalTracer().Inject( + err := opentracing.GlobalTracer().Inject( span.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header)) + if err != nil { + log.Error(err) + } } } diff --git a/provider/acme/account.go b/provider/acme/account.go index e57607229..4af3ad0b8 100644 --- a/provider/acme/account.go +++ b/provider/acme/account.go @@ -19,7 +19,7 @@ type Account struct { const ( // RegistrationURLPathV1Regexp is a regexp which match ACME registration URL in the V1 format - RegistrationURLPathV1Regexp string = `^.*/acme/reg/\d+$` + RegistrationURLPathV1Regexp = `^.*/acme/reg/\d+$` ) // NewAccount creates an account diff --git a/provider/acme/provider.go b/provider/acme/provider.go index fe57ce173..c6c4825a2 100644 --- a/provider/acme/provider.go +++ b/provider/acme/provider.go @@ -330,7 +330,6 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s safe.Go(func() { if _, err := p.resolveCertificate(domain, true); err != nil { log.Errorf("Unable to obtain ACME certificate for domains %q : %v", strings.Join(domain.ToStrArray(), ","), err) - } else { } }) } @@ -384,15 +383,6 @@ func (p *Provider) watchCertificate() { }) } -func (p *Provider) deleteCertificateForDomain(domain types.Domain) { - for k, cert := range p.certificates { - if reflect.DeepEqual(cert.Domain, domain) { - p.certificates = append(p.certificates[:k], p.certificates[k+1:]...) - } - } - p.saveCertificates() -} - func (p *Provider) saveCertificates() { err := p.Store.SaveCertificates(p.certificates) if err != nil { diff --git a/provider/consulcatalog/config.go b/provider/consulcatalog/config.go index ce5e1bee9..e2daf1f38 100644 --- a/provider/consulcatalog/config.go +++ b/provider/consulcatalog/config.go @@ -237,19 +237,6 @@ func hasTag(name string, tags []string) bool { return false } -func hasTagPrefix(name string, tags []string) bool { - lowerName := strings.ToLower(name) - - for _, tag := range tags { - lowerTag := strings.ToLower(tag) - - if strings.HasPrefix(lowerTag, lowerName) { - return true - } - } - return false -} - func getTag(name string, tags []string, defaultValue string) string { lowerName := strings.ToLower(name) diff --git a/provider/consulcatalog/deprecated_config.go b/provider/consulcatalog/deprecated_config.go index bec2918bc..618089d30 100644 --- a/provider/consulcatalog/deprecated_config.go +++ b/provider/consulcatalog/deprecated_config.go @@ -156,17 +156,6 @@ func (p *Provider) getFuncSliceAttribute(name string) func(tags []string) []stri } } -// Deprecated -func (p *Provider) getMapAttribute(name string, tags []string) map[string]string { - rawValue := getTag(p.getPrefixedName(name), tags, "") - - if len(rawValue) == 0 { - return nil - } - - return label.ParseMapValue(p.getPrefixedName(name), rawValue) -} - // Deprecated func (p *Provider) getFuncIntAttribute(name string, defaultValue int) func(tags []string) int { return func(tags []string) int { @@ -180,13 +169,6 @@ func (p *Provider) getFuncBoolAttribute(name string, defaultValue bool) func(tag } } -// Deprecated -func (p *Provider) getFuncHasAttributePrefix(name string) func(tags []string) bool { - return func(tags []string) bool { - return p.hasAttributePrefix(name, tags) - } -} - // Deprecated func (p *Provider) getInt64Attribute(name string, tags []string, defaultValue int64) int64 { rawValue := getTag(p.getPrefixedName(name), tags, "") @@ -244,7 +226,3 @@ func (p *Provider) getBoolAttribute(name string, tags []string, defaultValue boo } return value } - -func (p *Provider) hasAttributePrefix(name string, tags []string) bool { - return hasTagPrefix(p.getPrefixedName(name), tags) -} diff --git a/provider/docker/config.go b/provider/docker/config.go index 030ac2cef..6e3421812 100644 --- a/provider/docker/config.go +++ b/provider/docker/config.go @@ -182,19 +182,20 @@ func (p *Provider) getFrontendRule(container dockerData, segmentLabels map[strin return value } + domain := label.GetStringValue(segmentLabels, label.TraefikDomain, p.Domain) + if values, err := label.GetStringMultipleStrict(container.Labels, labelDockerComposeProject, labelDockerComposeService); err == nil { - return "Host:" + getSubDomain(values[labelDockerComposeService]+"."+values[labelDockerComposeProject]) + "." + p.Domain + return "Host:" + getSubDomain(values[labelDockerComposeService]+"."+values[labelDockerComposeProject]) + "." + domain } - if len(p.Domain) > 0 { - return "Host:" + getSubDomain(container.ServiceName) + "." + p.Domain + if len(domain) > 0 { + return "Host:" + getSubDomain(container.ServiceName) + "." + domain } return "" } func (p Provider) getIPAddress(container dockerData) string { - if value := label.GetStringValue(container.Labels, labelDockerNetwork, ""); value != "" { networkSettings := container.NetworkSettings if networkSettings.Networks != nil { @@ -246,6 +247,8 @@ func (p Provider) getIPAddress(container dockerData) string { for _, network := range container.NetworkSettings.Networks { return network.Addr } + + log.Warnf("Unable to find the IP address for the container %q.", container.Name) return "" } @@ -314,12 +317,17 @@ func (p *Provider) getServers(containers []dockerData) map[string]types.Server { var servers map[string]types.Server for i, container := range containers { + ip := p.getIPAddress(container) + if len(ip) == 0 { + log.Warnf("Unable to find the IP address for the container %q: the server is ignored.", container.Name) + continue + } + if servers == nil { servers = make(map[string]types.Server) } protocol := label.GetStringValue(container.SegmentLabels, label.TraefikProtocol, label.DefaultProtocol) - ip := p.getIPAddress(container) port := getPort(container) serverName := "server-" + container.SegmentName + "-" + container.Name diff --git a/provider/docker/config_container_docker_test.go b/provider/docker/config_container_docker_test.go index 4f0bfe7ef..ed87dbb09 100644 --- a/provider/docker/config_container_docker_test.go +++ b/provider/docker/config_container_docker_test.go @@ -406,6 +406,7 @@ func TestDockerBuildConfiguration(t *testing.T) { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() + var dockerDataList []dockerData for _, cont := range test.containers { dData := parseContainer(cont) @@ -809,15 +810,19 @@ func TestDockerGetFrontendRule(t *testing.T) { expected: "Host:foo.docker.localhost", }, { - container: containerJSON(name("bar")), - expected: "Host:bar.docker.localhost", + container: containerJSON(name("foo"), + labels(map[string]string{ + label.TraefikDomain: "traefik.localhost", + })), + expected: "Host:foo.traefik.localhost", }, { container: containerJSON(labels(map[string]string{ label.TraefikFrontendRule: "Host:foo.bar", })), expected: "Host:foo.bar", - }, { + }, + { container: containerJSON(labels(map[string]string{ "com.docker.compose.project": "foo", "com.docker.compose.service": "bar", @@ -1022,3 +1027,122 @@ func TestDockerGetPort(t *testing.T) { }) } } + +func TestDockerGetServers(t *testing.T) { + p := &Provider{} + + testCases := []struct { + desc string + containers []docker.ContainerJSON + expected map[string]types.Server + }{ + { + desc: "no container", + expected: nil, + }, + { + desc: "with a simple container", + containers: []docker.ContainerJSON{ + containerJSON( + name("test1"), + withNetwork("testnet", ipv4("10.10.10.10")), + ports(nat.PortMap{ + "80/tcp": {}, + })), + }, + expected: map[string]types.Server{ + "server-test1": { + URL: "http://10.10.10.10:80", + Weight: 1, + }, + }, + }, + { + desc: "with several containers", + containers: []docker.ContainerJSON{ + containerJSON( + name("test1"), + withNetwork("testnet", ipv4("10.10.10.11")), + ports(nat.PortMap{ + "80/tcp": {}, + })), + containerJSON( + name("test2"), + withNetwork("testnet", ipv4("10.10.10.12")), + ports(nat.PortMap{ + "81/tcp": {}, + })), + containerJSON( + name("test3"), + withNetwork("testnet", ipv4("10.10.10.13")), + ports(nat.PortMap{ + "82/tcp": {}, + })), + }, + expected: map[string]types.Server{ + "server-test1": { + URL: "http://10.10.10.11:80", + Weight: 1, + }, + "server-test2": { + URL: "http://10.10.10.12:81", + Weight: 1, + }, + "server-test3": { + URL: "http://10.10.10.13:82", + Weight: 1, + }, + }, + }, + { + desc: "ignore one container because no ip address", + containers: []docker.ContainerJSON{ + containerJSON( + name("test1"), + withNetwork("testnet", ipv4("")), + ports(nat.PortMap{ + "80/tcp": {}, + })), + containerJSON( + name("test2"), + withNetwork("testnet", ipv4("10.10.10.12")), + ports(nat.PortMap{ + "81/tcp": {}, + })), + containerJSON( + name("test3"), + withNetwork("testnet", ipv4("10.10.10.13")), + ports(nat.PortMap{ + "82/tcp": {}, + })), + }, + expected: map[string]types.Server{ + "server-test2": { + URL: "http://10.10.10.12:81", + Weight: 1, + }, + "server-test3": { + URL: "http://10.10.10.13:82", + Weight: 1, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + var dockerDataList []dockerData + for _, cont := range test.containers { + dData := parseContainer(cont) + dockerDataList = append(dockerDataList, dData) + } + + servers := p.getServers(dockerDataList) + + assert.Equal(t, test.expected, servers) + }) + } +} diff --git a/provider/docker/config_container_swarm_test.go b/provider/docker/config_container_swarm_test.go index 8b37b95c3..0a2206df0 100644 --- a/provider/docker/config_container_swarm_test.go +++ b/provider/docker/config_container_swarm_test.go @@ -561,8 +561,11 @@ func TestSwarmGetFrontendRule(t *testing.T) { networks: map[string]*docker.NetworkResource{}, }, { - service: swarmService(serviceName("bar")), - expected: "Host:bar.docker.localhost", + service: swarmService(serviceName("foo"), + serviceLabels(map[string]string{ + label.TraefikDomain: "traefik.localhost", + })), + expected: "Host:foo.traefik.localhost", networks: map[string]*docker.NetworkResource{}, }, { diff --git a/provider/docker/deprecated_container.go b/provider/docker/deprecated_container.go index ce05082b2..a05af3d60 100644 --- a/provider/docker/deprecated_container.go +++ b/provider/docker/deprecated_container.go @@ -27,12 +27,14 @@ func (p Provider) getFrontendRuleV1(container dockerData) string { return value } + domain := label.GetStringValue(container.Labels, label.TraefikDomain, p.Domain) + if values, err := label.GetStringMultipleStrict(container.Labels, labelDockerComposeProject, labelDockerComposeService); err == nil { - return "Host:" + getSubDomain(values[labelDockerComposeService]+"."+values[labelDockerComposeProject]) + "." + p.Domain + return "Host:" + getSubDomain(values[labelDockerComposeService]+"."+values[labelDockerComposeProject]) + "." + domain } - if len(p.Domain) > 0 { - return "Host:" + getSubDomain(container.ServiceName) + "." + p.Domain + if len(domain) > 0 { + return "Host:" + getSubDomain(container.ServiceName) + "." + domain } return "" diff --git a/provider/docker/deprecated_container_docker_test.go b/provider/docker/deprecated_container_docker_test.go index db275f42d..67771473e 100644 --- a/provider/docker/deprecated_container_docker_test.go +++ b/provider/docker/deprecated_container_docker_test.go @@ -752,15 +752,19 @@ func TestDockerGetFrontendRuleV1(t *testing.T) { expected: "Host:foo.docker.localhost", }, { - container: containerJSON(name("bar")), - expected: "Host:bar.docker.localhost", + container: containerJSON(name("foo"), + labels(map[string]string{ + label.TraefikDomain: "traefik.localhost", + })), + expected: "Host:foo.traefik.localhost", }, { container: containerJSON(labels(map[string]string{ label.TraefikFrontendRule: "Host:foo.bar", })), expected: "Host:foo.bar", - }, { + }, + { container: containerJSON(labels(map[string]string{ "com.docker.compose.project": "foo", "com.docker.compose.service": "bar", diff --git a/provider/docker/deprecated_container_swarm_test.go b/provider/docker/deprecated_container_swarm_test.go index 151e4b10e..7803e91b0 100644 --- a/provider/docker/deprecated_container_swarm_test.go +++ b/provider/docker/deprecated_container_swarm_test.go @@ -527,8 +527,11 @@ func TestSwarmGetFrontendRuleV1(t *testing.T) { networks: map[string]*docker.NetworkResource{}, }, { - service: swarmService(serviceName("bar")), - expected: "Host:bar.docker.localhost", + service: swarmService(serviceName("foo"), + serviceLabels(map[string]string{ + label.TraefikDomain: "traefik.localhost", + })), + expected: "Host:foo.traefik.localhost", networks: map[string]*docker.NetworkResource{}, }, { diff --git a/provider/docker/deprecated_service.go b/provider/docker/deprecated_service.go index ab8098f7a..d6180d445 100644 --- a/provider/docker/deprecated_service.go +++ b/provider/docker/deprecated_service.go @@ -136,12 +136,6 @@ func getFuncServiceIntLabelV1(labelSuffix string, defaultValue int) func(contain } } -// Deprecated -func hasStrictServiceLabelV1(serviceLabels map[string]string, labelSuffix string) bool { - value, ok := serviceLabels[labelSuffix] - return ok && len(value) > 0 -} - // Deprecated func getServiceStringValueV1(container dockerData, serviceLabels map[string]string, labelSuffix string, defaultValue string) string { if value, ok := serviceLabels[labelSuffix]; ok { @@ -150,23 +144,6 @@ func getServiceStringValueV1(container dockerData, serviceLabels map[string]stri return label.GetStringValue(container.Labels, label.Prefix+labelSuffix, defaultValue) } -// Deprecated -func getStrictServiceStringValueV1(serviceLabels map[string]string, labelSuffix string, defaultValue string) string { - if value, ok := serviceLabels[labelSuffix]; ok { - return value - } - return defaultValue -} - -// Deprecated -func getServiceMapValueV1(container dockerData, serviceLabels map[string]string, serviceName string, labelSuffix string) map[string]string { - if value, ok := serviceLabels[labelSuffix]; ok { - lblName := label.GetServiceLabel(labelSuffix, serviceName) - return label.ParseMapValue(lblName, value) - } - return label.GetMapValue(container.Labels, label.Prefix+labelSuffix) -} - // Deprecated func getServiceSliceValueV1(container dockerData, serviceLabels map[string]string, labelSuffix string) []string { if value, ok := serviceLabels[labelSuffix]; ok { @@ -197,17 +174,6 @@ func getServiceIntLabelV1(container dockerData, serviceName string, labelSuffix return label.GetIntValue(container.Labels, label.Prefix+labelSuffix, defaultValue) } -// Deprecated -func getServiceInt64ValueV1(container dockerData, serviceLabels map[string]string, labelSuffix string, defaultValue int64) int64 { - if rawValue, ok := serviceLabels[labelSuffix]; ok { - value, err := strconv.ParseInt(rawValue, 10, 64) - if err == nil { - return value - } - } - return label.GetInt64Value(container.Labels, label.Prefix+labelSuffix, defaultValue) -} - // Deprecated func getServiceLabelsV1(container dockerData, serviceName string) label.SegmentPropertyValues { return label.ExtractServiceProperties(container.Labels)[serviceName] diff --git a/provider/docker/deprecated_service_test.go b/provider/docker/deprecated_service_test.go index 83b903506..9d955b1dc 100644 --- a/provider/docker/deprecated_service_test.go +++ b/provider/docker/deprecated_service_test.go @@ -405,154 +405,6 @@ func TestDockerGetServiceStringValueV1(t *testing.T) { } } -func TestDockerHasStrictServiceLabelV1(t *testing.T) { - testCases := []struct { - desc string - serviceLabels map[string]string - labelSuffix string - expected bool - }{ - { - desc: "should return false when service don't have label", - serviceLabels: map[string]string{}, - labelSuffix: "", - expected: false, - }, - { - desc: "should return true when service have label", - serviceLabels: map[string]string{ - "foo": "bar", - }, - labelSuffix: "foo", - expected: true, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := hasStrictServiceLabelV1(test.serviceLabels, test.labelSuffix) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestDockerGetStrictServiceStringValueV1(t *testing.T) { - testCases := []struct { - desc string - serviceLabels map[string]string - labelSuffix string - defaultValue string - expected string - }{ - { - desc: "should return a string when the label exists", - serviceLabels: map[string]string{ - "foo": "bar", - }, - labelSuffix: "foo", - expected: "bar", - }, - { - desc: "should return a string when the label exists and value empty", - serviceLabels: map[string]string{ - "foo": "", - }, - labelSuffix: "foo", - defaultValue: "cube", - expected: "", - }, - { - desc: "should return the default value when the label doesn't exist", - serviceLabels: map[string]string{}, - labelSuffix: "foo", - defaultValue: "cube", - expected: "cube", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := getStrictServiceStringValueV1(test.serviceLabels, test.labelSuffix, test.defaultValue) - - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestDockerGetServiceMapValueV1(t *testing.T) { - testCases := []struct { - desc string - container docker.ContainerJSON - serviceLabels map[string]string - serviceName string - labelSuffix string - expected map[string]string - }{ - { - desc: "should return when no labels", - container: containerJSON( - name("test1"), - labels(map[string]string{})), - serviceLabels: map[string]string{}, - serviceName: "soo", - labelSuffix: "foo", - expected: nil, - }, - { - desc: "should return a map when label exists", - container: containerJSON( - name("test1"), - labels(map[string]string{ - "traefik.foo": "bir:fii", - })), - serviceLabels: map[string]string{ - "foo": "bar:foo", - }, - serviceName: "soo", - labelSuffix: "foo", - expected: map[string]string{ - "Bar": "foo", - }, - }, - { - desc: "should return a map when label exists (fallback to container labels)", - container: containerJSON( - name("test1"), - labels(map[string]string{ - "traefik.foo": "bir:fii", - })), - serviceLabels: map[string]string{ - "fo": "bar:foo", - }, - serviceName: "soo", - labelSuffix: "foo", - expected: map[string]string{ - "Bir": "fii", - }, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - dData := parseContainer(test.container) - - actual := getServiceMapValueV1(dData, test.serviceLabels, test.serviceName, test.labelSuffix) - - assert.Equal(t, test.expected, actual) - }) - } -} - func TestDockerGetServiceSliceValueV1(t *testing.T) { testCases := []struct { desc string @@ -672,67 +524,6 @@ func TestDockerGetServiceBoolValueV1(t *testing.T) { } } -func TestDockerGetServiceInt64ValueV1(t *testing.T) { - testCases := []struct { - desc string - container docker.ContainerJSON - serviceLabels map[string]string - labelSuffix string - defaultValue int64 - expected int64 - }{ - { - desc: "should return default value when no label", - container: containerJSON( - name("test1"), - labels(map[string]string{})), - serviceLabels: map[string]string{}, - labelSuffix: "foo", - defaultValue: 666, - expected: 666, - }, - { - desc: "should return a int64 when label", - container: containerJSON( - name("test1"), - labels(map[string]string{ - "traefik.foo": "20", - })), - serviceLabels: map[string]string{ - "foo": "10", - }, - labelSuffix: "foo", - expected: 10, - }, - { - desc: "should return a int64 when label (fallback to container labels)", - container: containerJSON( - name("test1"), - labels(map[string]string{ - "traefik.foo": "20", - })), - serviceLabels: map[string]string{ - "fo": "10", - }, - labelSuffix: "foo", - expected: 20, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - dData := parseContainer(test.container) - - actual := getServiceInt64ValueV1(dData, test.serviceLabels, test.labelSuffix, test.defaultValue) - - assert.Equal(t, test.expected, actual) - }) - } -} - func TestDockerCheckPortLabelsV1(t *testing.T) { testCases := []struct { container docker.ContainerJSON diff --git a/provider/ecs/config.go b/provider/ecs/config.go index cdb1cef35..0fc8b8792 100644 --- a/provider/ecs/config.go +++ b/provider/ecs/config.go @@ -91,7 +91,9 @@ func (p *Provider) filterInstance(i ecsInstance) bool { } func (p *Provider) getFrontendRule(i ecsInstance) string { - defaultRule := "Host:" + strings.ToLower(strings.Replace(i.Name, "_", "-", -1)) + "." + p.Domain + domain := label.GetStringValue(i.TraefikLabels, label.TraefikDomain, p.Domain) + defaultRule := "Host:" + strings.ToLower(strings.Replace(i.Name, "_", "-", -1)) + "." + domain + return label.GetStringValue(i.TraefikLabels, label.TraefikFrontendRule, defaultRule) } diff --git a/provider/marathon/builder_test.go b/provider/marathon/builder_test.go index 45f6e5dbd..4fb77d8fb 100644 --- a/provider/marathon/builder_test.go +++ b/provider/marathon/builder_test.go @@ -10,6 +10,16 @@ import ( const testTaskName = "taskID" +func withAppData(app marathon.Application, segmentName string) appData { + segmentProperties := label.ExtractTraefikLabels(stringValueMap(app.Labels)) + return appData{ + Application: app, + SegmentLabels: segmentProperties[segmentName], + SegmentName: segmentName, + LinkedApps: nil, + } +} + // Functions related to building applications. func withApplications(apps ...marathon.Application) *marathon.Applications { diff --git a/provider/marathon/config.go b/provider/marathon/config.go index 128d26768..4b90de740 100644 --- a/provider/marathon/config.go +++ b/provider/marathon/config.go @@ -210,10 +210,12 @@ func (p *Provider) getFrontendRule(app appData) string { } } + domain := label.GetStringValue(app.SegmentLabels, label.TraefikDomain, p.Domain) + if len(app.SegmentName) > 0 { - return "Host:" + strings.ToLower(provider.Normalize(app.SegmentName)) + "." + p.getSubDomain(app.ID) + "." + p.Domain + return "Host:" + strings.ToLower(provider.Normalize(app.SegmentName)) + "." + p.getSubDomain(app.ID) + "." + domain } - return "Host:" + p.getSubDomain(app.ID) + "." + p.Domain + return "Host:" + p.getSubDomain(app.ID) + "." + domain } func getPort(task marathon.Task, app appData) string { @@ -345,6 +347,9 @@ func (p *Provider) getServer(app appData, task marathon.Task) (string, *types.Se func (p *Provider) getServerHost(task marathon.Task, app appData) (string, error) { if app.IPAddressPerTask == nil || p.ForceTaskHostname { + if len(task.Host) == 0 { + return "", fmt.Errorf("host is undefined for task %q app %q", task.ID, app.ID) + } return task.Host, nil } diff --git a/provider/marathon/config_test.go b/provider/marathon/config_test.go index f063c5670..df5c8f1de 100644 --- a/provider/marathon/config_test.go +++ b/provider/marathon/config_test.go @@ -1034,7 +1034,7 @@ func TestGetPort(t *testing.T) { desc string application marathon.Application task marathon.Task - serviceName string + segmentName string expected string }{ { @@ -1116,23 +1116,23 @@ func TestGetPort(t *testing.T) { }, { desc: "multiple task ports with service index available", - application: application(withLabel(label.Prefix+"http.portIndex", "0")), + application: application(withSegmentLabel(label.TraefikPortIndex, "0", "http")), task: task(taskPorts(80, 443)), - serviceName: "http", + segmentName: "http", expected: "80", }, { desc: "multiple task ports with service port available", - application: application(withLabel(label.Prefix+"https.port", "443")), + application: application(withSegmentLabel(label.TraefikPort, "443", "https")), task: task(taskPorts(80, 443)), - serviceName: "https", + segmentName: "https", expected: "443", }, { desc: "multiple task ports with services but default port available", - application: application(withLabel(label.Prefix+"http.weight", "100")), + application: application(withSegmentLabel(label.TraefikWeight, "100", "http")), task: task(taskPorts(80, 443)), - serviceName: "http", + segmentName: "http", expected: "80", }, } @@ -1142,7 +1142,7 @@ func TestGetPort(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - actual := getPortV1(test.task, test.application, test.serviceName) + actual := getPort(test.task, withAppData(test.application, test.segmentName)) assert.Equal(t, test.expected, actual) }) @@ -1153,7 +1153,7 @@ func TestGetFrontendRule(t *testing.T) { testCases := []struct { desc string application marathon.Application - serviceName string + segmentName string expected string marathonLBCompatibility bool }{ @@ -1163,6 +1163,15 @@ func TestGetFrontendRule(t *testing.T) { marathonLBCompatibility: true, expected: "Host:test.marathon.localhost", }, + { + desc: "label domain", + application: application( + appID("test"), + withLabel(label.TraefikDomain, "traefik.localhost"), + ), + marathonLBCompatibility: true, + expected: "Host:test.traefik.localhost", + }, { desc: "HAProxy vhost available and LB compat disabled", application: application( @@ -1180,7 +1189,6 @@ func TestGetFrontendRule(t *testing.T) { }, { desc: "frontend rule available", - application: application( withLabel(label.TraefikFrontendRule, "Host:foo.bar"), withLabel("HAPROXY_0_VHOST", "unused"), @@ -1189,9 +1197,9 @@ func TestGetFrontendRule(t *testing.T) { expected: "Host:foo.bar", }, { - desc: "service label existing", + desc: "segment label frontend rule", application: application(withSegmentLabel(label.TraefikFrontendRule, "Host:foo.bar", "app")), - serviceName: "app", + segmentName: "app", marathonLBCompatibility: true, expected: "Host:foo.bar", }, @@ -1206,7 +1214,7 @@ func TestGetFrontendRule(t *testing.T) { MarathonLBCompatibility: test.marathonLBCompatibility, } - actual := p.getFrontendRuleV1(test.application, test.serviceName) + actual := p.getFrontendRule(withAppData(test.application, test.segmentName)) assert.Equal(t, test.expected, actual) }) @@ -1217,7 +1225,7 @@ func TestGetBackendName(t *testing.T) { testCases := []struct { desc string application marathon.Application - serviceName string + segmentName string expected string }{ { @@ -1231,9 +1239,9 @@ func TestGetBackendName(t *testing.T) { expected: "backendbar", }, { - desc: "service label existing", + desc: "segment label existing", application: application(withSegmentLabel(label.TraefikBackend, "bar", "app")), - serviceName: "app", + segmentName: "app", expected: "backendbar", }, } @@ -1245,7 +1253,7 @@ func TestGetBackendName(t *testing.T) { p := &Provider{} - actual := p.getBackendNameV1(test.application, test.serviceName) + actual := p.getBackendName(withAppData(test.application, test.segmentName)) assert.Equal(t, test.expected, actual) }) @@ -1256,7 +1264,7 @@ func TestGetServers(t *testing.T) { testCases := []struct { desc string application marathon.Application - serviceName string + segmentName string expected map[string]types.Server }{ { @@ -1304,12 +1312,14 @@ func TestGetServers(t *testing.T) { for _, test := range testCases { test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() + if test.desc == "should return nil when all hosts are empty" { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() - actual := p.getServersV1(test.application, test.serviceName) + actual := p.getServers(withAppData(test.application, test.segmentName)) - assert.Equal(t, test.expected, actual) - }) + assert.Equal(t, test.expected, actual) + }) + } } } diff --git a/provider/marathon/deprecated_config.go b/provider/marathon/deprecated_config.go index 2e5b0a3a0..5f4b50bca 100644 --- a/provider/marathon/deprecated_config.go +++ b/provider/marathon/deprecated_config.go @@ -138,10 +138,11 @@ func (p *Provider) getFrontendRuleV1(application marathon.Application, serviceNa } } + domain := label.GetStringValue(labels, label.SuffixDomain, p.Domain) if len(serviceName) > 0 { - return "Host:" + strings.ToLower(provider.Normalize(serviceName)) + "." + p.getSubDomain(application.ID) + "." + p.Domain + return "Host:" + strings.ToLower(provider.Normalize(serviceName)) + "." + p.getSubDomain(application.ID) + "." + domain } - return "Host:" + p.getSubDomain(application.ID) + "." + p.Domain + return "Host:" + p.getSubDomain(application.ID) + "." + domain } // Deprecated diff --git a/provider/marathon/deprecated_config_test.go b/provider/marathon/deprecated_config_test.go index d6328e3b9..3c5df44f6 100644 --- a/provider/marathon/deprecated_config_test.go +++ b/provider/marathon/deprecated_config_test.go @@ -760,3 +760,67 @@ func TestGetStickyV1(t *testing.T) { }) } } + +func TestGetServersV1(t *testing.T) { + testCases := []struct { + desc string + application marathon.Application + segmentName string + expected map[string]types.Server + }{ + { + desc: "should return nil when no task", + application: application(ipAddrPerTask(80)), + expected: nil, + }, + { + desc: "should return nil when all hosts are empty", + application: application( + withTasks( + task(ipAddresses("1.1.1.1"), withTaskID("A"), taskPorts(80)), + task(ipAddresses("1.1.1.2"), withTaskID("B"), taskPorts(80)), + task(ipAddresses("1.1.1.3"), withTaskID("C"), taskPorts(80))), + ), + expected: nil, + }, + { + desc: "with 3 tasks", + application: application( + ipAddrPerTask(80), + withTasks( + task(ipAddresses("1.1.1.1"), withTaskID("A"), taskPorts(80)), + task(ipAddresses("1.1.1.2"), withTaskID("B"), taskPorts(80)), + task(ipAddresses("1.1.1.3"), withTaskID("C"), taskPorts(80))), + ), + expected: map[string]types.Server{ + "server-A": { + URL: "http://1.1.1.1:80", + Weight: label.DefaultWeight, + }, + "server-B": { + URL: "http://1.1.1.2:80", + Weight: label.DefaultWeight, + }, + "server-C": { + URL: "http://1.1.1.3:80", + Weight: label.DefaultWeight, + }, + }, + }, + } + + p := &Provider{} + + for _, test := range testCases { + test := test + if test.desc == "should return nil when all hosts are empty" { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := p.getServersV1(test.application, test.segmentName) + + assert.Equal(t, test.expected, actual) + }) + } + } +} diff --git a/provider/mesos/config.go b/provider/mesos/config.go index 7f825e505..a5d874c5f 100644 --- a/provider/mesos/config.go +++ b/provider/mesos/config.go @@ -166,7 +166,9 @@ func (p *Provider) getFrontendRule(task taskData) string { if v := label.GetStringValue(task.TraefikLabels, label.TraefikFrontendRule, ""); len(v) > 0 { return v } - return "Host:" + strings.ToLower(strings.Replace(p.getSubDomain(task.DiscoveryInfo.Name), "_", "-", -1)) + "." + p.Domain + + domain := label.GetStringValue(task.TraefikLabels, label.TraefikDomain, p.Domain) + return "Host:" + strings.ToLower(strings.Replace(p.getSubDomain(task.DiscoveryInfo.Name), "_", "-", -1)) + "." + domain } func (p *Provider) getServers(tasks []taskData) map[string]types.Server { diff --git a/provider/mesos/config_test.go b/provider/mesos/config_test.go index e686c847d..c8a8d7845 100644 --- a/provider/mesos/config_test.go +++ b/provider/mesos/config_test.go @@ -660,3 +660,50 @@ func TestGetServers(t *testing.T) { }) } } + +func TestGetFrontendRule(t *testing.T) { + p := Provider{ + Domain: "mesos.localhost", + } + + testCases := []struct { + desc string + mesosTask taskData + expected string + }{ + { + desc: "label missing", + mesosTask: aTaskData("test", + withInfo("foo"), + ), + expected: "Host:foo.mesos.localhost", + }, + { + desc: "label domain", + mesosTask: aTaskData("test", + withInfo("foo"), + withLabel(label.TraefikDomain, "traefik.localhost"), + ), + expected: "Host:foo.traefik.localhost", + }, + { + desc: "frontend rule available", + mesosTask: aTaskData("test", + withInfo("foo"), + withLabel(label.TraefikFrontendRule, "Host:foo.bar"), + ), + expected: "Host:foo.bar", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + rule := p.getFrontendRule(test.mesosTask) + + assert.Equal(t, test.expected, rule) + }) + } +} diff --git a/provider/mesos/deprecated_config.go b/provider/mesos/deprecated_config.go index cbd18ee41..71c1fb2b6 100644 --- a/provider/mesos/deprecated_config.go +++ b/provider/mesos/deprecated_config.go @@ -196,7 +196,9 @@ func (p *Provider) getFrontendRuleV1(task state.Task) string { if v := getStringValueV1(task, label.TraefikFrontendRule, ""); len(v) > 0 { return v } - return "Host:" + strings.ToLower(strings.Replace(p.getSubDomain(task.DiscoveryInfo.Name), "_", "-", -1)) + "." + p.Domain + + domain := getStringValueV1(task, label.TraefikDomain, p.Domain) + return "Host:" + strings.ToLower(strings.Replace(p.getSubDomain(task.DiscoveryInfo.Name), "_", "-", -1)) + "." + domain } // Deprecated diff --git a/provider/rancher/config.go b/provider/rancher/config.go index 494637d10..6b59ca9fd 100644 --- a/provider/rancher/config.go +++ b/provider/rancher/config.go @@ -124,7 +124,9 @@ func (p *Provider) serviceFilter(service rancherData) bool { } func (p *Provider) getFrontendRule(serviceName string, labels map[string]string) string { - defaultRule := "Host:" + strings.ToLower(strings.Replace(serviceName, "/", ".", -1)) + "." + p.Domain + domain := label.GetStringValue(labels, label.TraefikDomain, p.Domain) + defaultRule := "Host:" + strings.ToLower(strings.Replace(serviceName, "/", ".", -1)) + "." + domain + return label.GetStringValue(labels, label.TraefikFrontendRule, defaultRule) } @@ -164,6 +166,11 @@ func getServers(service rancherData) map[string]types.Server { var servers map[string]types.Server for index, ip := range service.Containers { + if len(ip) == 0 { + log.Warnf("Unable to find the IP address for a container in the service %q: this container is ignored.", service.Name) + continue + } + if servers == nil { servers = make(map[string]types.Server) } diff --git a/provider/rancher/config_test.go b/provider/rancher/config_test.go index f0cf55fd2..a62a49349 100644 --- a/provider/rancher/config_test.go +++ b/provider/rancher/config_test.go @@ -729,6 +729,16 @@ func TestProviderGetFrontendRule(t *testing.T) { }, expected: "Host:foo.rancher.localhost", }, + { + desc: "with domain label", + service: rancherData{ + Name: "test-service", + Labels: map[string]string{ + label.TraefikDomain: "traefik.localhost", + }, + }, + expected: "Host:test-service.traefik.localhost", + }, { desc: "host with /", service: rancherData{ @@ -746,26 +756,6 @@ func TestProviderGetFrontendRule(t *testing.T) { }, expected: "Host:foo.bar.com", }, - { - desc: "with Path label", - service: rancherData{ - Name: "test-service", - Labels: map[string]string{ - label.TraefikFrontendRule: "Path:/test", - }, - }, - expected: "Path:/test", - }, - { - desc: "with PathPrefix label", - service: rancherData{ - Name: "test-service", - Labels: map[string]string{ - label.TraefikFrontendRule: "PathPrefix:/test2", - }, - }, - expected: "PathPrefix:/test2", - }, } for _, test := range testCases { @@ -849,6 +839,18 @@ func TestGetServers(t *testing.T) { }, expected: nil, }, + { + desc: "should return nil when no server IPs", + service: rancherData{ + Labels: map[string]string{ + label.TraefikWeight: "7", + }, + Containers: []string{""}, + Health: "healthy", + State: "active", + }, + expected: nil, + }, { desc: "should use default weight when invalid weight value", service: rancherData{ diff --git a/server/header_rewriter.go b/server/header_rewriter.go index e4531bc64..193220385 100644 --- a/server/header_rewriter.go +++ b/server/header_rewriter.go @@ -11,20 +11,20 @@ import ( // NewHeaderRewriter Create a header rewriter func NewHeaderRewriter(trustedIPs []string, insecure bool) (forward.ReqRewriter, error) { - IPs, err := whitelist.NewIP(trustedIPs, insecure, true) + ips, err := whitelist.NewIP(trustedIPs, insecure, true) if err != nil { return nil, err } - h, err := os.Hostname() + hostname, err := os.Hostname() if err != nil { - h = "localhost" + hostname = "localhost" } return &headerRewriter{ - secureRewriter: &forward.HeaderRewriter{TrustForwardHeader: true, Hostname: h}, - insecureRewriter: &forward.HeaderRewriter{TrustForwardHeader: false, Hostname: h}, - ips: IPs, + secureRewriter: &forward.HeaderRewriter{TrustForwardHeader: false, Hostname: hostname}, + insecureRewriter: &forward.HeaderRewriter{TrustForwardHeader: true, Hostname: hostname}, + ips: ips, insecure: insecure, }, nil } @@ -37,16 +37,17 @@ type headerRewriter struct { } func (h *headerRewriter) Rewrite(req *http.Request) { - authorized, _, err := h.ips.IsAuthorized(req) + if h.insecure { + h.insecureRewriter.Rewrite(req) + return + } + + err := h.ips.IsAuthorized(req) if err != nil { log.Error(err) h.secureRewriter.Rewrite(req) return } - if h.insecure || authorized { - h.secureRewriter.Rewrite(req) - } else { - h.insecureRewriter.Rewrite(req) - } + h.insecureRewriter.Rewrite(req) } diff --git a/server/header_rewriter_test.go b/server/header_rewriter_test.go new file mode 100644 index 000000000..7e5df3bbf --- /dev/null +++ b/server/header_rewriter_test.go @@ -0,0 +1,104 @@ +package server + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHeaderRewriter_Rewrite(t *testing.T) { + testCases := []struct { + desc string + remoteAddr string + trustedIPs []string + insecure bool + expected map[string]string + }{ + { + desc: "Secure & authorized", + remoteAddr: "10.10.10.10:80", + trustedIPs: []string{"10.10.10.10"}, + insecure: false, + expected: map[string]string{ + "X-Foo": "bar", + "X-Forwarded-For": "30.30.30.30", + }, + }, + { + desc: "Secure & unauthorized", + remoteAddr: "50.50.50.50:80", + trustedIPs: []string{"10.10.10.10"}, + insecure: false, + expected: map[string]string{ + "X-Foo": "bar", + "X-Forwarded-For": "", + }, + }, + { + desc: "Secure & authorized error", + remoteAddr: "10.10.10.10", + trustedIPs: []string{"10.10.10.10"}, + insecure: false, + expected: map[string]string{ + "X-Foo": "bar", + "X-Forwarded-For": "", + }, + }, + { + desc: "insecure & authorized", + remoteAddr: "10.10.10.10:80", + trustedIPs: []string{"10.10.10.10"}, + insecure: true, + expected: map[string]string{ + "X-Foo": "bar", + "X-Forwarded-For": "30.30.30.30", + }, + }, + { + desc: "insecure & unauthorized", + remoteAddr: "50.50.50.50:80", + trustedIPs: []string{"10.10.10.10"}, + insecure: true, + expected: map[string]string{ + "X-Foo": "bar", + "X-Forwarded-For": "30.30.30.30", + }, + }, + { + desc: "insecure & authorized error", + remoteAddr: "10.10.10.10", + trustedIPs: []string{"10.10.10.10"}, + insecure: true, + expected: map[string]string{ + "X-Foo": "bar", + "X-Forwarded-For": "30.30.30.30", + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + rewriter, err := NewHeaderRewriter(test.trustedIPs, test.insecure) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodGet, "http://20.20.20.20/foo", nil) + require.NoError(t, err) + req.RemoteAddr = test.remoteAddr + + req.Header.Set("X-Foo", "bar") + req.Header.Set("X-Forwarded-For", "30.30.30.30") + + rewriter.Rewrite(req) + + for key, value := range test.expected { + assert.Equal(t, value, req.Header.Get(key)) + } + }) + } +} diff --git a/server/server.go b/server/server.go index 569d2445d..ba96c842f 100644 --- a/server/server.go +++ b/server/server.go @@ -767,7 +767,8 @@ func (s *Server) prepareServer(entryPointName string, entryPoint *configuration. if !ok { return false, fmt.Errorf("type error %v", addr) } - return IPs.ContainsIP(ip.IP) + + return IPs.ContainsIP(ip.IP), nil }, } } diff --git a/tls/certificate.go b/tls/certificate.go index 30013208c..626810760 100644 --- a/tls/certificate.go +++ b/tls/certificate.go @@ -90,12 +90,15 @@ func (f FileOrContent) Read() ([]byte, error) { func (c *Certificates) CreateTLSConfig(entryPointName string) (*tls.Config, error) { config := &tls.Config{} domainsCertificates := make(map[string]map[string]*tls.Certificate) + if c.isEmpty() { config.Certificates = []tls.Certificate{} + cert, err := generate.DefaultCertificate() if err != nil { return nil, err } + config.Certificates = append(config.Certificates, *cert) } else { for _, certificate := range *c { @@ -104,8 +107,9 @@ func (c *Certificates) CreateTLSConfig(entryPointName string) (*tls.Config, erro log.Errorf("Unable to add a certificate to the entryPoint %q : %v", entryPointName, err) continue } + for _, certDom := range domainsCertificates { - for _, cert := range map[string]*tls.Certificate(certDom) { + for _, cert := range certDom { config.Certificates = append(config.Certificates, *cert) } } diff --git a/types/logs.go b/types/logs.go index 4ad2a3902..610bc1e23 100644 --- a/types/logs.go +++ b/types/logs.go @@ -28,34 +28,21 @@ type AccessLog struct { Fields *AccessLogFields `json:"fields,omitempty" description:"AccessLogFields" export:"true"` } -// StatusCodes holds status codes ranges to filter access log -type StatusCodes []string - // AccessLogFilters holds filters configuration type AccessLogFilters struct { StatusCodes StatusCodes `json:"statusCodes,omitempty" description:"Keep access logs with status codes in the specified range" export:"true"` RetryAttempts bool `json:"retryAttempts,omitempty" description:"Keep access logs when at least one retry happened" export:"true"` } -// FieldNames holds maps of fields with specific mode -type FieldNames map[string]string - -// AccessLogFields holds configuration for access log fields -type AccessLogFields struct { - DefaultMode string `json:"defaultMode,omitempty" description:"Default mode for fields: keep | drop" export:"true"` - Names FieldNames `json:"names,omitempty" description:"Override mode for fields" export:"true"` - Headers *FieldHeaders `json:"headers,omitempty" description:"Headers to keep, drop or redact" export:"true"` -} - -// FieldHeaderNames holds maps of fields with specific mode -type FieldHeaderNames map[string]string - // FieldHeaders holds configuration for access log headers type FieldHeaders struct { DefaultMode string `json:"defaultMode,omitempty" description:"Default mode for fields: keep | drop | redact" export:"true"` Names FieldHeaderNames `json:"names,omitempty" description:"Override mode for headers" export:"true"` } +// StatusCodes holds status codes ranges to filter access log +type StatusCodes []string + // Set adds strings elem into the the parser // it splits str on , and ; func (s *StatusCodes) Set(str string) error { @@ -79,6 +66,9 @@ func (s *StatusCodes) SetValue(val interface{}) { *s = val.(StatusCodes) } +// FieldNames holds maps of fields with specific mode +type FieldNames map[string]string + // String is the method to format the flag's value, part of the flag.Value interface. // The String method's output will be used in diagnostics. func (f *FieldNames) String() string { @@ -111,6 +101,9 @@ func (f *FieldNames) SetValue(val interface{}) { *f = val.(FieldNames) } +// FieldHeaderNames holds maps of fields with specific mode +type FieldHeaderNames map[string]string + // String is the method to format the flag's value, part of the flag.Value interface. // The String method's output will be used in diagnostics. func (f *FieldHeaderNames) String() string { @@ -141,6 +134,13 @@ func (f *FieldHeaderNames) SetValue(val interface{}) { *f = val.(FieldHeaderNames) } +// AccessLogFields holds configuration for access log fields +type AccessLogFields struct { + DefaultMode string `json:"defaultMode,omitempty" description:"Default mode for fields: keep | drop" export:"true"` + Names FieldNames `json:"names,omitempty" description:"Override mode for fields" export:"true"` + Headers *FieldHeaders `json:"headers,omitempty" description:"Headers to keep, drop or redact" export:"true"` +} + // Keep check if the field need to be kept or dropped func (f *AccessLogFields) Keep(field string) bool { defaultKeep := true @@ -154,17 +154,6 @@ func (f *AccessLogFields) Keep(field string) bool { return defaultKeep } -func checkFieldValue(value string, defaultKeep bool) bool { - switch value { - case AccessLogKeep: - return true - case AccessLogDrop: - return false - default: - return defaultKeep - } -} - // KeepHeader checks if the headers need to be kept, dropped or redacted and returns the status func (f *AccessLogFields) KeepHeader(header string) string { defaultValue := AccessLogKeep @@ -178,6 +167,17 @@ func (f *AccessLogFields) KeepHeader(header string) string { return defaultValue } +func checkFieldValue(value string, defaultKeep bool) bool { + switch value { + case AccessLogKeep: + return true + case AccessLogDrop: + return false + default: + return defaultKeep + } +} + func checkFieldHeaderValue(value string, defaultValue string) string { if value == AccessLogKeep || value == AccessLogDrop || value == AccessLogRedact { return value diff --git a/whitelist/ip.go b/whitelist/ip.go index 3b1c4a0b7..33a988eba 100644 --- a/whitelist/ip.go +++ b/whitelist/ip.go @@ -1,11 +1,11 @@ package whitelist import ( + "errors" "fmt" "net" "net/http" - - "github.com/pkg/errors" + "strings" ) const ( @@ -50,64 +50,78 @@ func NewIP(whiteList []string, insecure bool, useXForwardedFor bool) (*IP, error } // IsAuthorized checks if provided request is authorized by the white list -func (ip *IP) IsAuthorized(req *http.Request) (bool, net.IP, error) { +func (ip *IP) IsAuthorized(req *http.Request) error { if ip.insecure { - return true, nil, nil + return nil } + var invalidMatches []string + if ip.useXForwardedFor { xFFs := req.Header[XForwardedFor] - if len(xFFs) > 1 { + if len(xFFs) > 0 { for _, xFF := range xFFs { - ok, i, err := ip.contains(parseHost(xFF)) + ok, err := ip.contains(parseHost(xFF)) if err != nil { - return false, nil, err + return err } if ok { - return ok, i, nil + return nil } + + invalidMatches = append(invalidMatches, xFF) } } } host, _, err := net.SplitHostPort(req.RemoteAddr) if err != nil { - return false, nil, err + return err } - return ip.contains(host) + + ok, err := ip.contains(host) + if err != nil { + return err + } + + if !ok { + invalidMatches = append(invalidMatches, req.RemoteAddr) + return fmt.Errorf("%q matched none of the white list", strings.Join(invalidMatches, ", ")) + } + + return nil } // contains checks if provided address is in the white list -func (ip *IP) contains(addr string) (bool, net.IP, error) { +func (ip *IP) contains(addr string) (bool, error) { ipAddr, err := parseIP(addr) if err != nil { - return false, nil, fmt.Errorf("unable to parse address: %s: %s", addr, err) + return false, fmt.Errorf("unable to parse address: %s: %s", addr, err) } - contains, err := ip.ContainsIP(ipAddr) - return contains, ipAddr, err + return ip.ContainsIP(ipAddr), nil } // ContainsIP checks if provided address is in the white list -func (ip *IP) ContainsIP(addr net.IP) (bool, error) { +func (ip *IP) ContainsIP(addr net.IP) bool { if ip.insecure { - return true, nil + return true } for _, whiteListIP := range ip.whiteListsIPs { if whiteListIP.Equal(addr) { - return true, nil + return true } } for _, whiteListNet := range ip.whiteListsNet { if whiteListNet.Contains(addr) { - return true, nil + return true } } - return false, nil + return false } func parseIP(addr string) (net.IP, error) { diff --git a/whitelist/ip_test.go b/whitelist/ip_test.go index f4dc0f022..f29c09335 100644 --- a/whitelist/ip_test.go +++ b/whitelist/ip_test.go @@ -17,7 +17,7 @@ func TestIsAuthorized(t *testing.T) { allowXForwardedFor bool remoteAddr string xForwardedForValues []string - expected bool + authorized bool }{ { desc: "allow UseXForwardedFor, remoteAddr not in range, UseXForwardedFor in range", @@ -25,7 +25,7 @@ func TestIsAuthorized(t *testing.T) { allowXForwardedFor: true, remoteAddr: "10.2.3.1:123", xForwardedForValues: []string{"1.2.3.1", "10.2.3.1"}, - expected: true, + authorized: true, }, { desc: "allow UseXForwardedFor, remoteAddr in range, UseXForwardedFor in range", @@ -33,7 +33,7 @@ func TestIsAuthorized(t *testing.T) { allowXForwardedFor: true, remoteAddr: "1.2.3.1:123", xForwardedForValues: []string{"1.2.3.1", "10.2.3.1"}, - expected: true, + authorized: true, }, { desc: "allow UseXForwardedFor, remoteAddr in range, UseXForwardedFor not in range", @@ -41,7 +41,7 @@ func TestIsAuthorized(t *testing.T) { allowXForwardedFor: true, remoteAddr: "1.2.3.1:123", xForwardedForValues: []string{"10.2.3.1", "10.2.3.1"}, - expected: true, + authorized: true, }, { desc: "allow UseXForwardedFor, remoteAddr not in range, UseXForwardedFor not in range", @@ -49,7 +49,7 @@ func TestIsAuthorized(t *testing.T) { allowXForwardedFor: true, remoteAddr: "10.2.3.1:123", xForwardedForValues: []string{"10.2.3.1", "10.2.3.1"}, - expected: false, + authorized: false, }, { desc: "don't allow UseXForwardedFor, remoteAddr not in range, UseXForwardedFor in range", @@ -57,7 +57,7 @@ func TestIsAuthorized(t *testing.T) { allowXForwardedFor: false, remoteAddr: "10.2.3.1:123", xForwardedForValues: []string{"1.2.3.1", "10.2.3.1"}, - expected: false, + authorized: false, }, { desc: "don't allow UseXForwardedFor, remoteAddr in range, UseXForwardedFor in range", @@ -65,7 +65,7 @@ func TestIsAuthorized(t *testing.T) { allowXForwardedFor: false, remoteAddr: "1.2.3.1:123", xForwardedForValues: []string{"1.2.3.1", "10.2.3.1"}, - expected: true, + authorized: true, }, { desc: "don't allow UseXForwardedFor, remoteAddr in range, UseXForwardedFor not in range", @@ -73,7 +73,7 @@ func TestIsAuthorized(t *testing.T) { allowXForwardedFor: false, remoteAddr: "1.2.3.1:123", xForwardedForValues: []string{"10.2.3.1", "10.2.3.1"}, - expected: true, + authorized: true, }, { desc: "don't allow UseXForwardedFor, remoteAddr not in range, UseXForwardedFor not in range", @@ -81,7 +81,7 @@ func TestIsAuthorized(t *testing.T) { allowXForwardedFor: false, remoteAddr: "10.2.3.1:123", xForwardedForValues: []string{"10.2.3.1", "10.2.3.1"}, - expected: false, + authorized: false, }, } @@ -95,11 +95,12 @@ func TestIsAuthorized(t *testing.T) { whiteLister, err := NewIP(test.whiteList, false, test.allowXForwardedFor) require.NoError(t, err) - authorized, ips, err := whiteLister.IsAuthorized(req) - require.NoError(t, err) - assert.NotNil(t, ips) - - assert.Equal(t, test.expected, authorized) + err = whiteLister.IsAuthorized(req) + if test.authorized { + require.NoError(t, err) + } else { + require.Error(t, err) + } }) } } @@ -349,16 +350,14 @@ func TestContainsIsAllowed(t *testing.T) { require.NotNil(t, whiteLister) for _, testIP := range test.passIPs { - allowed, ip, err := whiteLister.contains(testIP) + allowed, err := whiteLister.contains(testIP) require.NoError(t, err) - require.NotNil(t, ip, err) assert.Truef(t, allowed, "%s should have passed.", testIP) } for _, testIP := range test.rejectIPs { - allowed, ip, err := whiteLister.contains(testIP) + allowed, err := whiteLister.contains(testIP) require.NoError(t, err) - require.NotNil(t, ip, err) assert.Falsef(t, allowed, "%s should not have passed.", testIP) } }) @@ -405,7 +404,7 @@ func TestContainsInsecure(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - ok, _, err := test.whiteLister.contains(test.ip) + ok, err := test.whiteLister.contains(test.ip) require.NoError(t, err) assert.Equal(t, test.expected, ok) @@ -426,9 +425,8 @@ func TestContainsBrokenIPs(t *testing.T) { require.NoError(t, err) for _, testIP := range brokenIPs { - _, ip, err := whiteLister.contains(testIP) + _, err := whiteLister.contains(testIP) assert.Error(t, err) - require.Nil(t, ip, err) } }