Merge branch 'v1.5' into master

This commit is contained in:
Fernandez Ludovic 2018-02-21 16:55:57 +01:00
commit 21e28ae848
30 changed files with 451 additions and 203 deletions

View file

@ -22,7 +22,7 @@ If you intend to ask a support question: DO NOT FILE AN ISSUE.
HOW TO WRITE A GOOD ISSUE? HOW TO WRITE A GOOD ISSUE?
- Respect the issue template as more as possible. - Respect the issue template as much as possible.
- If it's possible use the command `traefik bug`. See https://www.youtube.com/watch?v=Lyz62L8m93I. - If it's possible use the command `traefik bug`. See https://www.youtube.com/watch?v=Lyz62L8m93I.
- The title must be short and descriptive. - The title must be short and descriptive.
- Explain the conditions which led you to write this issue: the context. - Explain the conditions which led you to write this issue: the context.

68
.github/ISSUE_TEMPLATE/bugs.md vendored Normal file
View file

@ -0,0 +1,68 @@
<!--
DO NOT FILE ISSUES FOR GENERAL SUPPORT QUESTIONS.
The issue tracker is for reporting bugs and feature requests only.
For end-user related support questions, refer to one of the following:
- Stack Overflow (using the "traefik" tag): https://stackoverflow.com/questions/tagged/traefik
- the Traefik community Slack channel: https://traefik.herokuapp.com
-->
### Do you want to request a *feature* or report a *bug*?
Bug
### What did you do?
<!--
HOW TO WRITE A GOOD ISSUE?
- Respect the issue template as much as possible.
- If it's possible use the command `traefik bug`. See https://www.youtube.com/watch?v=Lyz62L8m93I.
- The title must be short and descriptive.
- Explain the conditions which led you to write this issue: the context.
- The context should lead to something, an idea or a problem that youre facing.
- Remain clear and concise.
- Format your messages to help the reader focus on what matters and understand the structure of your message, use Markdown syntax https://help.github.com/articles/github-flavored-markdown
-->
### What did you expect to see?
### What did you see instead?
### Output of `traefik version`: (_What version of Traefik are you using?_)
<!--
For the Traefik Docker image:
docker run [IMAGE] version
ex: docker run traefik version
-->
```
(paste your output here)
```
### What is your environment & configuration (arguments, toml, provider, platform, ...)?
```toml
# (paste your configuration here)
```
<!--
Add more configuration information here.
-->
### If applicable, please paste the log output in debug mode (`--debug` switch)
```
(paste your output here)
```

32
.github/ISSUE_TEMPLATE/features.md vendored Normal file
View file

@ -0,0 +1,32 @@
<!--
DO NOT FILE ISSUES FOR GENERAL SUPPORT QUESTIONS.
The issue tracker is for reporting bugs and feature requests only.
For end-user related support questions, refer to one of the following:
- Stack Overflow (using the "traefik" tag): https://stackoverflow.com/questions/tagged/traefik
- the Traefik community Slack channel: https://traefik.herokuapp.com
-->
### Do you want to request a *feature* or report a *bug*?
Feature
### What did you expect to see?
<!--
HOW TO WRITE A GOOD ISSUE?
- Respect the issue template as much as possible.
- If it's possible use the command `traefik bug`. See https://www.youtube.com/watch?v=Lyz62L8m93I.
- The title must be short and descriptive.
- Explain the conditions which led you to write this issue: the context.
- The context should lead to something, an idea or a problem that youre facing.
- Remain clear and concise.
- Format your messages to help the reader focus on what matters and understand the structure of your message, use Markdown syntax https://help.github.com/articles/github-flavored-markdown
-->

View file

@ -0,0 +1,7 @@
### What does this PR do?
Merge v{{.Version}} into master
### Motivation
Be sync.

View file

@ -0,0 +1,7 @@
### What does this PR do?
Prepare release v{{.Version}}.
### Motivation
Create a new release.

View file

@ -12,7 +12,7 @@
[![Twitter](https://img.shields.io/twitter/follow/traefikproxy.svg?style=social)](https://twitter.com/intent/follow?screen_name=traefikproxy) [![Twitter](https://img.shields.io/twitter/follow/traefikproxy.svg?style=social)](https://twitter.com/intent/follow?screen_name=traefikproxy)
Træfik (pronounced like [traffic](https://speak-ipa.bearbin.net/speak.cgi?speak=%CB%88tr%C3%A6f%C9%AAk)) is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease. Træfik (pronounced like _traffic_) is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease.
It supports several backends ([Docker](https://www.docker.com/), [Swarm mode](https://docs.docker.com/engine/swarm/), [Kubernetes](https://kubernetes.io), [Marathon](https://mesosphere.github.io/marathon/), [Consul](https://www.consul.io/), [Etcd](https://coreos.com/etcd/), [Rancher](https://rancher.com), [Amazon ECS](https://aws.amazon.com/ecs), and a lot more) to manage its configuration automatically and dynamically. It supports several backends ([Docker](https://www.docker.com/), [Swarm mode](https://docs.docker.com/engine/swarm/), [Kubernetes](https://kubernetes.io), [Marathon](https://mesosphere.github.io/marathon/), [Consul](https://www.consul.io/), [Etcd](https://coreos.com/etcd/), [Rancher](https://rancher.com), [Amazon ECS](https://aws.amazon.com/ecs), and a lot more) to manage its configuration automatically and dynamically.
--- ---

View file

@ -29,11 +29,6 @@ func runHealthCheck(traefikConfiguration *TraefikConfiguration) func() error {
return func() error { return func() error {
traefikConfiguration.GlobalConfiguration.SetEffectiveConfiguration(traefikConfiguration.ConfigFile) traefikConfiguration.GlobalConfiguration.SetEffectiveConfiguration(traefikConfiguration.ConfigFile)
if traefikConfiguration.Ping == nil {
fmt.Println("Please enable `ping` to use healtcheck.")
os.Exit(1)
}
resp, errPing := healthCheck(traefikConfiguration.GlobalConfiguration) resp, errPing := healthCheck(traefikConfiguration.GlobalConfiguration)
if errPing != nil { if errPing != nil {
fmt.Printf("Error calling healthcheck: %s\n", errPing) fmt.Printf("Error calling healthcheck: %s\n", errPing)
@ -50,9 +45,13 @@ func runHealthCheck(traefikConfiguration *TraefikConfiguration) func() error {
} }
func healthCheck(globalConfiguration configuration.GlobalConfiguration) (*http.Response, error) { func healthCheck(globalConfiguration configuration.GlobalConfiguration) (*http.Response, error) {
if globalConfiguration.Ping == nil {
return nil, errors.New("please enable `ping` to use health check")
}
pingEntryPoint, ok := globalConfiguration.EntryPoints[globalConfiguration.Ping.EntryPoint] pingEntryPoint, ok := globalConfiguration.EntryPoints[globalConfiguration.Ping.EntryPoint]
if !ok { if !ok {
return nil, errors.New("missing ping entrypoint") return nil, errors.New("missing `ping` entrypoint")
} }
client := &http.Client{Timeout: 5 * time.Second} client := &http.Client{Timeout: 5 * time.Second}

View file

@ -135,6 +135,7 @@ entryPoint = "https"
# #
# delayBeforeCheck = 0 # delayBeforeCheck = 0
``` ```
!!! note !!! note
Even if `TLS-SNI-01` challenge is [disabled](https://community.letsencrypt.org/t/2018-01-11-update-regarding-acme-tls-sni-and-shared-hosting-infrastructure/50188) for the moment, it stays the _by default_ ACME Challenge in Træfik. Even if `TLS-SNI-01` challenge is [disabled](https://community.letsencrypt.org/t/2018-01-11-update-regarding-acme-tls-sni-and-shared-hosting-infrastructure/50188) for the moment, it stays the _by default_ ACME Challenge in Træfik.
If `TLS-SNI-01` challenge is not re-enabled in the future, it we will be removed from Træfik. If `TLS-SNI-01` challenge is not re-enabled in the future, it we will be removed from Træfik.
@ -149,6 +150,7 @@ entryPoint = "https"
Let's Encrypt functionality will be limited until Træfik is restarted. Let's Encrypt functionality will be limited until Træfik is restarted.
If Let's Encrypt is not reachable, these certificates will be used : If Let's Encrypt is not reachable, these certificates will be used :
- ACME certificates already generated before downtime - ACME certificates already generated before downtime
- Expired ACME certificates - Expired ACME certificates
- Provided certificates - Provided certificates
@ -168,6 +170,7 @@ storage = "acme.json"
The `storage` option sets where are stored your ACME certificates. The `storage` option sets where are stored your ACME certificates.
There are two kind of `storage` : There are two kind of `storage` :
- a JSON file, - a JSON file,
- a KV store entry. - a KV store entry.
@ -240,6 +243,8 @@ entryPoint = "https"
Specify the entryPoint to use during the challenges. Specify the entryPoint to use during the challenges.
```toml ```toml
defaultEntryPoints = ["http", "http"]
[entryPoints] [entryPoints]
[entryPoints.http] [entryPoints.http]
address = ":80" address = ":80"
@ -328,10 +333,10 @@ onDemand = true
Enable on demand certificate. Enable on demand certificate.
This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate. This will request a certificate from Let's Encrypt during the first TLS handshake for a host name that does not yet have a certificate.
!!! warning !!! warning
TLS handshakes will be slow when requesting a hostname certificate for the first time, this can lead to DoS attacks. TLS handshakes will be slow when requesting a host name certificate for the first time, this can lead to DoS attacks.
!!! warning !!! warning
Take note that Let's Encrypt have [rate limiting](https://letsencrypt.org/docs/rate-limits). Take note that Let's Encrypt have [rate limiting](https://letsencrypt.org/docs/rate-limits).
@ -345,7 +350,7 @@ onHostRule = true
# ... # ...
``` ```
Enable certificate generation on frontends Host rules. Enable certificate generation on frontends `Host` rules (for frontends wired on the `acme.entryPoint`).
This will request a certificate from Let's Encrypt for each frontend with a Host rule. This will request a certificate from Let's Encrypt for each frontend with a Host rule.

View file

@ -1,5 +1,7 @@
# API Definition # API Definition
## Configuration
```toml ```toml
# API definition # API definition
[api] [api]
@ -28,6 +30,8 @@
debug = true debug = true
``` ```
For more customization, see [entry points](/configuration/entrypoints/) documentation and [examples](/user-guide/examples/#ping-health-check).
## Web UI ## Web UI
![Web UI Providers](/img/web.frontend.png) ![Web UI Providers](/img/web.frontend.png)
@ -42,7 +46,7 @@
| `/health` | `GET` | json health metrics | | `/health` | `GET` | json health metrics |
| `/api` | `GET` | Configuration for all providers | | `/api` | `GET` | Configuration for all providers |
| `/api/providers` | `GET` | Providers | | `/api/providers` | `GET` | Providers |
| `/api/providers/{provider}` | `GET`, `PUT` | Get or update provider | | `/api/providers/{provider}` | `GET`, `PUT` | Get or update provider (1) |
| `/api/providers/{provider}/backends` | `GET` | List backends | | `/api/providers/{provider}/backends` | `GET` | List backends |
| `/api/providers/{provider}/backends/{backend}` | `GET` | Get backend | | `/api/providers/{provider}/backends/{backend}` | `GET` | Get backend |
| `/api/providers/{provider}/backends/{backend}/servers` | `GET` | List servers in backend | | `/api/providers/{provider}/backends/{backend}/servers` | `GET` | List servers in backend |
@ -52,6 +56,8 @@
| `/api/providers/{provider}/frontends/{frontend}/routes` | `GET` | List routes in a frontend | | `/api/providers/{provider}/frontends/{frontend}/routes` | `GET` | List routes in a frontend |
| `/api/providers/{provider}/frontends/{frontend}/routes/{route}` | `GET` | Get a route in a frontend | | `/api/providers/{provider}/frontends/{frontend}/routes/{route}` | `GET` | Get a route in a frontend |
<1> See [Rest](/configuration/backends/rest/#api) for more information.
!!! warning !!! warning
For compatibility reason, when you activate the rest provider, you can use `web` or `rest` as `provider` value. For compatibility reason, when you activate the rest provider, you can use `web` or `rest` as `provider` value.
But be careful, in the configuration for all providers the key is still `web`. But be careful, in the configuration for all providers the key is still `web`.
@ -185,6 +191,7 @@ curl -s "http://localhost:8080/health" | jq .
## Metrics ## Metrics
You can enable Traefik to export internal metrics to different monitoring systems. You can enable Traefik to export internal metrics to different monitoring systems.
```toml ```toml
[api] [api]
# ... # ...

View file

@ -36,7 +36,6 @@ address = ":8080"
# #
readOnly = true readOnly = true
# Set the root path for webui and API # Set the root path for webui and API
# #
# Deprecated # Deprecated
@ -55,13 +54,13 @@ readOnly = true
### Authentication ### Authentication
!!! note !!! note
The `/ping` path of the api is excluded from authentication (since 1.4). The `/ping` path of the API is excluded from authentication (since 1.4).
#### Basic Authentication #### Basic Authentication
Passwords can be encoded in MD5, SHA1 and BCrypt: you can use `htpasswd` to generate those ones. Passwords can be encoded in MD5, SHA1 and BCrypt: you can use `htpasswd` to generate those ones.
Users can be specified directly in the toml file, or indirectly by referencing an external file; Users can be specified directly in the TOML file, or indirectly by referencing an external file;
if both are provided, the two are merged, with external file contents having precedence. if both are provided, the two are merged, with external file contents having precedence.
```toml ```toml
@ -80,7 +79,7 @@ usersFile = "/path/to/.htpasswd"
You can use `htdigest` to generate those ones. You can use `htdigest` to generate those ones.
Users can be specified directly in the toml file, or indirectly by referencing an external file; Users can be specified directly in the TOML file, or indirectly by referencing an external file;
if both are provided, the two are merged, with external file contents having precedence if both are provided, the two are merged, with external file contents having precedence
```toml ```toml
@ -98,7 +97,7 @@ usersFile = "/path/to/.htdigest"
## Metrics ## Metrics
You can enable Traefik to export internal metrics to different monitoring systems. You can enable Træfik to export internal metrics to different monitoring systems.
### Prometheus ### Prometheus
@ -221,7 +220,7 @@ recentErrors = 10
|-----------------------------------------------------------------|:-------------:|----------------------------------------------------------------------------------------------------| |-----------------------------------------------------------------|:-------------:|----------------------------------------------------------------------------------------------------|
| `/` | `GET` | Provides a simple HTML frontend of Træfik | | `/` | `GET` | Provides a simple HTML frontend of Træfik |
| `/ping` | `GET`, `HEAD` | A simple endpoint to check for Træfik process liveness. Return a code `200` with the content: `OK` | | `/ping` | `GET`, `HEAD` | A simple endpoint to check for Træfik process liveness. Return a code `200` with the content: `OK` |
| `/health` | `GET` | json health metrics | | `/health` | `GET` | JSON health metrics |
| `/api` | `GET` | Configuration for all providers | | `/api` | `GET` | Configuration for all providers |
| `/api/providers` | `GET` | Providers | | `/api/providers` | `GET` | Providers |
| `/api/providers/{provider}` | `GET`, `PUT` | Get or update provider | | `/api/providers/{provider}` | `GET`, `PUT` | Get or update provider |
@ -244,7 +243,7 @@ curl -sv "http://localhost:8080/ping"
``` ```
```shell ```shell
* Trying ::1... * Trying ::1...
* Connected to localhost (::1) port 8080 (#0) * Connected to localhost (::1) port 8080 (\#0)
> GET /ping HTTP/1.1 > GET /ping HTTP/1.1
> Host: localhost:8080 > Host: localhost:8080
> User-Agent: curl/7.43.0 > User-Agent: curl/7.43.0
@ -255,7 +254,7 @@ curl -sv "http://localhost:8080/ping"
< Content-Length: 2 < Content-Length: 2
< Content-Type: text/plain; charset=utf-8 < Content-Type: text/plain; charset=utf-8
< <
* Connection #0 to host localhost left intact * Connection \#0 to host localhost left intact
OK OK
``` ```
@ -309,7 +308,7 @@ curl -s "http://localhost:8080/health" | jq .
"status": "Internal Server Error", "status": "Internal Server Error",
// request HTTP method // request HTTP method
"method": "GET", "method": "GET",
// request hostname // request host name
"host": "localhost", "host": "localhost",
// request path // request path
"path": "/path", "path": "/path",
@ -385,23 +384,28 @@ curl -s "http://localhost:8080/api" | jq .
} }
``` ```
## Path ### Deprecation compatibility
As web is deprecated, you can handle the `Path` option like this #### Path
As the web provider is deprecated, you can handle the `Path` option like this:
```toml ```toml
[entrypoints.http] defaultEntryPoints = ["http"]
address=":80"
[entrypoints.dashboard] [entryPoints]
address=":8080" [entryPoints.http]
address = ":80"
[entrypoints.api] [entryPoints.dashboard]
address=":8081" address = ":8080"
#Activate API and Dashboard [entryPoints.api]
address = ":8081"
# Activate API and Dashboard
[api] [api]
entrypoint="api" entryPoint = "api"
[file] [file]
[backends] [backends]
@ -411,8 +415,67 @@ entrypoint="api"
[frontends] [frontends]
[frontends.frontend1] [frontends.frontend1]
entrypoints=["dashboard"] entryPoints = ["dashboard"]
backend = "backend1" backend = "backend1"
[frontends.frontend1.routes.test_1] [frontends.frontend1.routes.test_1]
rule = "PathPrefixStrip:/yourprefix;PathPrefix:/yourprefix" rule = "PathPrefixStrip:/yourprefix;PathPrefix:/yourprefix"
``` ```
#### Address
As the web provider is deprecated, you can handle the `Address` option like this:
```toml
defaultEntryPoints = ["http"]
[entryPoints]
[entryPoints.http]
address = ":80"
[entryPoints.ping]
address = ":8082"
[entryPoints.api]
address = ":8083"
[ping]
entryPoint = "ping"
[api]
entryPoint = "api"
```
In the above example, you would access a regular path, administration panel, and health-check as follows:
* Regular path: `http://hostname:80/foo`
* Admin Panel: `http://hostname:8083/`
* Ping URL: `http://hostname:8082/ping`
In the above example, it is _very_ important to create a named dedicated entry point, and do **not** include it in `defaultEntryPoints`.
Otherwise, you are likely to expose _all_ services via that entry point.
#### Authentication
As the web provider is deprecated, you can handle the `auth` option like this:
```toml
defaultEntryPoints = ["http"]
[entryPoints]
[entryPoints.http]
address = ":80"
[entryPoints.api]
address=":8080"
[entryPoints.api.auth]
[entryPoints.api.auth.basic]
users = [
"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
]
[api]
entrypoint="api"
```
For more information, see [entry points](/configuration/entrypoints/) .

View file

@ -13,7 +13,10 @@
[entryPoints.http.tls] [entryPoints.http.tls]
minVersion = "VersionTLS12" minVersion = "VersionTLS12"
cipherSuites = ["TLS_RSA_WITH_AES_256_GCM_SHA384"] cipherSuites = [
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_RSA_WITH_AES_256_GCM_SHA384"
]
[[entryPoints.http.tls.certificates]] [[entryPoints.http.tls.certificates]]
certFile = "path/to/my.cert" certFile = "path/to/my.cert"
keyFile = "path/to/my.key" keyFile = "path/to/my.key"
@ -246,9 +249,9 @@ In the example below both `snitest.com` and `snitest.org` will require client ce
### Basic Authentication ### Basic Authentication
Passwords can be encoded in MD5, SHA1 and BCrypt: you can use `htpasswd` to generate those ones. Passwords can be encoded in MD5, SHA1 and BCrypt: you can use `htpasswd` to generate them.
Users can be specified directly in the toml file, or indirectly by referencing an external file; Users can be specified directly in the TOML file, or indirectly by referencing an external file;
if both are provided, the two are merged, with external file contents having precedence. if both are provided, the two are merged, with external file contents having precedence.
```toml ```toml
@ -263,9 +266,9 @@ Users can be specified directly in the toml file, or indirectly by referencing a
### Digest Authentication ### Digest Authentication
You can use `htdigest` to generate those ones. You can use `htdigest` to generate them.
Users can be specified directly in the toml file, or indirectly by referencing an external file; Users can be specified directly in the TOML file, or indirectly by referencing an external file;
if both are provided, the two are merged, with external file contents having precedence if both are provided, the two are merged, with external file contents having precedence
```toml ```toml
@ -283,7 +286,7 @@ Users can be specified directly in the toml file, or indirectly by referencing a
This configuration will first forward the request to `http://authserver.com/auth`. This configuration will first forward the request to `http://authserver.com/auth`.
If the response code is 2XX, access is granted and the original request is performed. If the response code is 2XX, access is granted and the original request is performed.
Otherwise, the response from the auth server is returned. Otherwise, the response from the authentication server is returned.
```toml ```toml
[entryPoints] [entryPoints]
@ -320,7 +323,10 @@ To specify an https entry point with a minimum TLS version, and specifying an ar
address = ":443" address = ":443"
[entryPoints.https.tls] [entryPoints.https.tls]
minVersion = "VersionTLS12" minVersion = "VersionTLS12"
cipherSuites = ["TLS_RSA_WITH_AES_256_GCM_SHA384"] cipherSuites = [
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_RSA_WITH_AES_256_GCM_SHA384"
]
[[entryPoints.https.tls.certificates]] [[entryPoints.https.tls.certificates]]
certFile = "integration/fixtures/https/snitest.com.cert" certFile = "integration/fixtures/https/snitest.com.cert"
keyFile = "integration/fixtures/https/snitest.com.key" keyFile = "integration/fixtures/https/snitest.com.key"

View file

@ -1,5 +1,7 @@
# Ping Definition # Ping Definition
## Configuration
```toml ```toml
# Ping definition # Ping definition
[ping] [ping]
@ -19,7 +21,7 @@
!!! warning !!! warning
Even if you have authentication configured on entry point, the `/ping` path of the api is excluded from authentication. Even if you have authentication configured on entry point, the `/ping` path of the api is excluded from authentication.
### Example ## Example
```shell ```shell
curl -sv "http://localhost:8080/ping" curl -sv "http://localhost:8080/ping"

View file

@ -10,7 +10,7 @@
[![Twitter](https://img.shields.io/twitter/follow/traefikproxy.svg?style=social)](https://twitter.com/intent/follow?screen_name=traefikproxy) [![Twitter](https://img.shields.io/twitter/follow/traefikproxy.svg?style=social)](https://twitter.com/intent/follow?screen_name=traefikproxy)
Træfik (pronounced like [traffic](https://speak-ipa.bearbin.net/speak.cgi?speak=%CB%88tr%C3%A6f%C9%AAk)) is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease. Træfik (pronounced like _traffic_) is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease.
It supports several backends ([Docker](https://www.docker.com/), [Swarm mode](https://docs.docker.com/engine/swarm/), [Kubernetes](https://kubernetes.io), [Marathon](https://mesosphere.github.io/marathon/), [Consul](https://www.consul.io/), [Etcd](https://coreos.com/etcd/), [Rancher](https://rancher.com), [Amazon ECS](https://aws.amazon.com/ecs), and a lot more) to manage its configuration automatically and dynamically. It supports several backends ([Docker](https://www.docker.com/), [Swarm mode](https://docs.docker.com/engine/swarm/), [Kubernetes](https://kubernetes.io), [Marathon](https://mesosphere.github.io/marathon/), [Consul](https://www.consul.io/), [Etcd](https://coreos.com/etcd/), [Rancher](https://rancher.com), [Amazon ECS](https://aws.amazon.com/ecs), and a lot more) to manage its configuration automatically and dynamically.
## Overview ## Overview

View file

@ -91,7 +91,7 @@ entryPoint = "https"
This configuration allows generating Let's Encrypt certificates (thanks to `HTTP-01` challenge) for the four domains `local[1-4].com` with described SANs. This configuration allows generating Let's Encrypt certificates (thanks to `HTTP-01` challenge) for the four domains `local[1-4].com` with described SANs.
Traefik generates these certificates when it starts and it needs to be restart if new domains are added. Træfik generates these certificates when it starts and it needs to be restart if new domains are added.
### OnHostRule option (with HTTP challenge) ### OnHostRule option (with HTTP challenge)
@ -126,9 +126,9 @@ entryPoint = "https"
This configuration allows generating Let's Encrypt certificates (thanks to `HTTP-01` challenge) for the four domains `local[1-4].com`. This configuration allows generating Let's Encrypt certificates (thanks to `HTTP-01` challenge) for the four domains `local[1-4].com`.
Traefik generates these certificates when it starts. Træfik generates these certificates when it starts.
If a backend is added with a `onHost` rule, Traefik will automatically generate the Let's Encrypt certificate for the new domain. If a backend is added with a `onHost` rule, Træfik will automatically generate the Let's Encrypt certificate for the new domain (for frontends wired on the `acme.entryPoint`).
### OnDemand option (with HTTP challenge) ### OnDemand option (with HTTP challenge)
@ -152,11 +152,10 @@ entryPoint = "https"
This configuration allows generating a Let's Encrypt certificate (thanks to `HTTP-01` challenge) during the first HTTPS request on a new domain. This configuration allows generating a Let's Encrypt certificate (thanks to `HTTP-01` challenge) during the first HTTPS request on a new domain.
!!! note !!! note
This option simplifies the configuration but : This option simplifies the configuration but :
* TLS handshakes will be slow when requesting a hostname certificate for the first time, this can leads to DDoS attacks. * TLS handshakes will be slow when requesting a host name certificate for the first time, this can leads to DDoS attacks.
* Let's Encrypt have rate limiting: https://letsencrypt.org/docs/rate-limits * Let's Encrypt have rate limiting: https://letsencrypt.org/docs/rate-limits
That's why, it's better to use the `onHostRule` option if possible. That's why, it's better to use the `onHostRule` option if possible.
@ -191,7 +190,7 @@ entryPoint = "https"
``` ```
DNS challenge needs environment variables to be executed. DNS challenge needs environment variables to be executed.
This variables have to be set on the machine/container which host Traefik. These variables have to be set on the machine/container which host Træfik.
These variables are described [in this section](/configuration/acme/#provider). These variables are described [in this section](/configuration/acme/#provider).
@ -218,7 +217,7 @@ entryPoint = "https"
entryPoint = "http" entryPoint = "http"
``` ```
Traefik will only try to generate a Let's encrypt certificate (thanks to `HTTP-01` challenge) if the domain cannot be checked by the provided certificates. Træfik will only try to generate a Let's encrypt certificate (thanks to `HTTP-01` challenge) if the domain cannot be checked by the provided certificates.
### Cluster mode ### Cluster mode
@ -292,14 +291,14 @@ The `consul` provider contains the configuration.
rule = "Path:/test" rule = "Path:/test"
``` ```
## Enable Basic authentication in an entrypoint ## Enable Basic authentication in an entry point
With two user/pass: With two user/pass:
- `test`:`test` - `test`:`test`
- `test2`:`test2` - `test2`:`test2`
Passwords are encoded in MD5: you can use htpasswd to generate those ones. Passwords are encoded in MD5: you can use `htpasswd` to generate them.
```toml ```toml
defaultEntryPoints = ["http"] defaultEntryPoints = ["http"]
@ -337,7 +336,7 @@ providersThrottleDuration = "5s"
idleTimeout = "360s" idleTimeout = "360s"
``` ```
## Securing Ping Health Check ## Ping Health Check
The `/ping` health-check URL is enabled with the command-line `--ping` or config file option `[ping]`. The `/ping` health-check URL is enabled with the command-line `--ping` or config file option `[ping]`.
Thus, if you have a regular path for `/foo` and an entrypoint on `:80`, you would access them as follows: Thus, if you have a regular path for `/foo` and an entrypoint on `:80`, you would access them as follows:
@ -346,40 +345,36 @@ Thus, if you have a regular path for `/foo` and an entrypoint on `:80`, you woul
* Admin panel: `http://hostname:8080/` * Admin panel: `http://hostname:8080/`
* Ping URL: `http://hostname:8080/ping` * Ping URL: `http://hostname:8080/ping`
However, for security reasons, you may want to be able to expose the `/ping` health-check URL to outside health-checkers, e.g. an Internet service or cloud load-balancer, _without_ exposing your admin panel's port. However, for security reasons, you may want to be able to expose the `/ping` health-check URL to outside health-checkers, e.g. an Internet service or cloud load-balancer, _without_ exposing your administration panel's port.
In many environments, the security staff may not _allow_ you to expose it. In many environments, the security staff may not _allow_ you to expose it.
You have two options: You have two options:
* Enable `/ping` on a regular entrypoint * Enable `/ping` on a regular entry point
* Enable `/ping` on a dedicated port * Enable `/ping` on a dedicated port
### Enable ping health check on a regular entrypoint ### Enable ping health check on a regular entry point
To proxy `/ping` from a regular entrypoint to the admin one without exposing the panel, do the following: To proxy `/ping` from a regular entry point to the administration one without exposing the panel, do the following:
```toml ```toml
[backends] defaultEntryPoints = ["http"]
[backends.traefik]
[backends.traefik.servers.server1] [entryPoints]
url = "http://localhost:8080" [entryPoints.http]
weight = 10 address = ":80"
[ping]
entryPoint = "http"
[frontends]
[frontends.traefikadmin]
backend = "traefik"
[frontends.traefikadmin.routes.ping]
rule = "Path:/ping"
``` ```
The above creates a new backend called `traefik`, listening on `http://localhost:8080`, i.e. the local admin port. The above link `ping` on the `http` entry point and then expose it on port `80`
We only expose the admin panel via the `frontend` named `traefikadmin`, and only expose the `/ping` Path.
Be careful with the `traefikadmin` frontend. If you do _not_ specify a `Path:` rule, you would expose the entire dashboard.
### Enable ping health check on dedicated port ### Enable ping health check on dedicated port
If you do not want to or cannot expose the health-check on a regular entrypoint - e.g. your security rules do not allow it, or you have a conflicting path - then you can enable health-check on its own entrypoint. If you do not want to or cannot expose the health-check on a regular entry point - e.g. your security rules do not allow it, or you have a conflicting path - then you can enable health-check on its own entry point.
Use the following config: Use the following configuration:
```toml ```toml
defaultEntryPoints = ["http"] defaultEntryPoints = ["http"]
@ -390,32 +385,18 @@ defaultEntryPoints = ["http"]
[entryPoints.ping] [entryPoints.ping]
address = ":8082" address = ":8082"
[backends] [ping]
[backends.traefik] entryPoint = "ping"
[backends.traefik.servers.server1]
url = "http://localhost:8080"
weight = 10
[frontends]
[frontends.traefikadmin]
backend = "traefik"
entrypoints = ["ping"]
[frontends.traefikadmin.routes.ping]
rule = "Path:/ping"
``` ```
The above is similar to the previous example, but instead of enabling `/ping` on the _default_ entrypoint, we enable it on a _dedicated_ entrypoint. The above is similar to the previous example, but instead of enabling `/ping` on the _default_ entry point, we enable it on a _dedicated_ entry point.
In the above example, you would access a regular path, admin panel and health-check as follows: In the above example, you would access a regular path and health-check as follows:
* Regular path: `http://hostname:80/foo` * Regular path: `http://hostname:80/foo`
* Admin panel: `http://hostname:8080/`
* Ping URL: `http://hostname:8082/ping` * Ping URL: `http://hostname:8082/ping`
Note the dedicated port `:8082` for `/ping`. Note the dedicated port `:8082` for `/ping`.
In the above example, it is _very_ important to create a named dedicated entrypoint, and do **not** include it in `defaultEntryPoints`. In the above example, it is _very_ important to create a named dedicated entry point, and do **not** include it in `defaultEntryPoints`.
Otherwise, you are likely to expose _all_ services via that entrypoint. Otherwise, you are likely to expose _all_ services via this entry point.
In the above example, we have two entrypoints, `http` and `ping`, but we only included `http` in `defaultEntryPoints`, while explicitly tying `frontend.traefikadmin` to the `ping` entrypoint.
This ensures that all the "normal" frontends will be exposed via entrypoint `http` and _not_ via entrypoint `ping`.

View file

@ -358,3 +358,31 @@ func (s *SimpleSuite) TestMetricsPrometheusDefaultEntrypoint(c *check.C) {
err = try.GetRequest("http://127.0.0.1:8080/metrics", 1*time.Second, try.StatusCodeIs(http.StatusOK)) err = try.GetRequest("http://127.0.0.1:8080/metrics", 1*time.Second, try.StatusCodeIs(http.StatusOK))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
} }
func (s *SimpleSuite) TestMultipleProviderSameBackendName(c *check.C) {
s.createComposeProject(c, "base")
s.composeProject.Start(c)
ipWhoami01 := s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress
ipWhoami02 := s.composeProject.Container(c, "whoami2").NetworkSettings.IPAddress
file := s.adaptFile(c, "fixtures/multiple_provider.toml", struct{ IP string }{
IP: ipWhoami02,
})
defer os.Remove(file)
cmd, output := s.traefikCmd(withConfigFile(file))
defer output(c)
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
err = try.GetRequest("http://127.0.0.1:8080/api/providers", 1*time.Second, try.BodyContains("PathPrefix"))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/whoami", 1*time.Second, try.BodyContains(ipWhoami01))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8000/file", 1*time.Second, try.BodyContains(ipWhoami02))
c.Assert(err, checker.IsNil)
}

View file

@ -91,11 +91,11 @@ func (s *ConstraintSuite) TestMatchConstraintGlobal(c *check.C) {
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
defer cmd.Process.Kill() defer cmd.Process.Kill()
nginx := s.composeProject.Container(c, "nginx") whoami := s.composeProject.Container(c, "whoami")
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{"traefik.tags=api"}) err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"traefik.tags=api"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress)
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
@ -117,11 +117,11 @@ func (s *ConstraintSuite) TestDoesNotMatchConstraintGlobal(c *check.C) {
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
defer cmd.Process.Kill() defer cmd.Process.Kill()
nginx := s.composeProject.Container(c, "nginx") whoami := s.composeProject.Container(c, "whoami")
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{}) err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress)
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
@ -143,11 +143,11 @@ func (s *ConstraintSuite) TestMatchConstraintProvider(c *check.C) {
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
defer cmd.Process.Kill() defer cmd.Process.Kill()
nginx := s.composeProject.Container(c, "nginx") whoami := s.composeProject.Container(c, "whoami")
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{"traefik.tags=api"}) err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"traefik.tags=api"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress)
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
@ -169,11 +169,11 @@ func (s *ConstraintSuite) TestDoesNotMatchConstraintProvider(c *check.C) {
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
defer cmd.Process.Kill() defer cmd.Process.Kill()
nginx := s.composeProject.Container(c, "nginx") whoami := s.composeProject.Container(c, "whoami")
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{}) err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress)
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
@ -196,11 +196,11 @@ func (s *ConstraintSuite) TestMatchMultipleConstraint(c *check.C) {
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
defer cmd.Process.Kill() defer cmd.Process.Kill()
nginx := s.composeProject.Container(c, "nginx") whoami := s.composeProject.Container(c, "whoami")
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{"traefik.tags=api", "traefik.tags=eu-1"}) err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"traefik.tags=api", "traefik.tags=eu-1"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress)
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
@ -223,11 +223,11 @@ func (s *ConstraintSuite) TestDoesNotMatchMultipleConstraint(c *check.C) {
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
defer cmd.Process.Kill() defer cmd.Process.Kill()
nginx := s.composeProject.Container(c, "nginx") whoami := s.composeProject.Container(c, "whoami")
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{"traefik.tags=api", "traefik.tags=us-1"}) err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"traefik.tags=api", "traefik.tags=us-1"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress)
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)

View file

@ -145,9 +145,9 @@ func (s *ConsulCatalogSuite) TestSingleService(c *check.C) {
err = try.GetRequest("http://127.0.0.1:8000/", 2*time.Second, try.StatusCodeIs(http.StatusNotFound)) err = try.GetRequest("http://127.0.0.1:8000/", 2*time.Second, try.StatusCodeIs(http.StatusNotFound))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
nginx := s.composeProject.Container(c, "nginx1") whoami := s.composeProject.Container(c, "whoami1")
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{}) err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
@ -157,7 +157,7 @@ func (s *ConsulCatalogSuite) TestSingleService(c *check.C) {
err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody())
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
s.deregisterService("test", nginx.NetworkSettings.IPAddress) s.deregisterService("test", whoami.NetworkSettings.IPAddress)
err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusNotFound), try.HasBody()) err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusNotFound), try.HasBody())
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
@ -175,11 +175,11 @@ func (s *ConsulCatalogSuite) TestExposedByDefaultFalseSingleService(c *check.C)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
defer cmd.Process.Kill() defer cmd.Process.Kill()
nginx := s.composeProject.Container(c, "nginx1") whoami := s.composeProject.Container(c, "whoami1")
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{}) err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress)
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
@ -201,16 +201,16 @@ func (s *ConsulCatalogSuite) TestExposedByDefaultFalseSimpleServiceMultipleNode(
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
defer cmd.Process.Kill() defer cmd.Process.Kill()
nginx := s.composeProject.Container(c, "nginx1") whoami := s.composeProject.Container(c, "whoami1")
nginx2 := s.composeProject.Container(c, "nginx2") whoami2 := s.composeProject.Container(c, "whoami2")
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{}) err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress)
err = s.registerService("test", nginx2.NetworkSettings.IPAddress, 80, []string{"traefik.enable=true"}) err = s.registerService("test", whoami2.NetworkSettings.IPAddress, 80, []string{"traefik.enable=true"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", nginx2.NetworkSettings.IPAddress) defer s.deregisterService("test", whoami2.NetworkSettings.IPAddress)
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
@ -232,16 +232,16 @@ func (s *ConsulCatalogSuite) TestExposedByDefaultTrueSimpleServiceMultipleNode(c
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
defer cmd.Process.Kill() defer cmd.Process.Kill()
nginx := s.composeProject.Container(c, "nginx1") whoami := s.composeProject.Container(c, "whoami1")
nginx2 := s.composeProject.Container(c, "nginx2") whoami2 := s.composeProject.Container(c, "whoami2")
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{"name=nginx1"}) err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress)
err = s.registerService("test", nginx2.NetworkSettings.IPAddress, 80, []string{"name=nginx2"}) err = s.registerService("test", whoami2.NetworkSettings.IPAddress, 80, []string{"name=whoami2"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", nginx2.NetworkSettings.IPAddress) defer s.deregisterService("test", whoami2.NetworkSettings.IPAddress)
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
@ -251,7 +251,7 @@ func (s *ConsulCatalogSuite) TestExposedByDefaultTrueSimpleServiceMultipleNode(c
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second,
try.BodyContains(nginx.NetworkSettings.IPAddress, nginx2.NetworkSettings.IPAddress)) try.BodyContains(whoami.NetworkSettings.IPAddress, whoami2.NetworkSettings.IPAddress))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
} }
@ -267,16 +267,16 @@ func (s *ConsulCatalogSuite) TestRefreshConfigWithMultipleNodeWithoutHealthCheck
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
defer cmd.Process.Kill() defer cmd.Process.Kill()
nginx := s.composeProject.Container(c, "nginx1") whoami := s.composeProject.Container(c, "whoami1")
nginx2 := s.composeProject.Container(c, "nginx2") whoami2 := s.composeProject.Container(c, "whoami2")
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{"name=nginx1"}) err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress)
err = s.registerAgentService("test", nginx.NetworkSettings.IPAddress, 80, []string{"name=nginx1"}) err = s.registerAgentService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering agent service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering agent service"))
defer s.deregisterAgentService(nginx.NetworkSettings.IPAddress) defer s.deregisterAgentService(whoami.NetworkSettings.IPAddress)
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
@ -286,28 +286,28 @@ func (s *ConsulCatalogSuite) TestRefreshConfigWithMultipleNodeWithoutHealthCheck
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second,
try.BodyContains(nginx.NetworkSettings.IPAddress)) try.BodyContains(whoami.NetworkSettings.IPAddress))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
err = s.registerService("test", nginx2.NetworkSettings.IPAddress, 80, []string{"name=nginx2"}) err = s.registerService("test", whoami2.NetworkSettings.IPAddress, 80, []string{"name=whoami2"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second,
try.BodyContains(nginx.NetworkSettings.IPAddress, nginx2.NetworkSettings.IPAddress)) try.BodyContains(whoami.NetworkSettings.IPAddress, whoami2.NetworkSettings.IPAddress))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
s.deregisterService("test", nginx2.NetworkSettings.IPAddress) s.deregisterService("test", whoami2.NetworkSettings.IPAddress)
err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second,
try.BodyContains(nginx.NetworkSettings.IPAddress)) try.BodyContains(whoami.NetworkSettings.IPAddress))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
err = s.registerService("test", nginx2.NetworkSettings.IPAddress, 80, []string{"name=nginx2"}) err = s.registerService("test", whoami2.NetworkSettings.IPAddress, 80, []string{"name=whoami2"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", nginx2.NetworkSettings.IPAddress) defer s.deregisterService("test", whoami2.NetworkSettings.IPAddress)
err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second,
try.BodyContains(nginx.NetworkSettings.IPAddress, nginx2.NetworkSettings.IPAddress)) try.BodyContains(whoami.NetworkSettings.IPAddress, whoami2.NetworkSettings.IPAddress))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
} }
@ -323,13 +323,13 @@ func (s *ConsulCatalogSuite) TestBasicAuthSimpleService(c *check.C) {
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
defer cmd.Process.Kill() defer cmd.Process.Kill()
nginx := s.composeProject.Container(c, "nginx1") whoami := s.composeProject.Container(c, "whoami1")
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{ err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{
"traefik.frontend.auth.basic=test:$2a$06$O5NksJPAcgrC9MuANkSoE.Xe9DSg7KcLLFYNr1Lj6hPcMmvgwxhme,test2:$2y$10$xP1SZ70QbZ4K2bTGKJOhpujkpcLxQcB3kEPF6XAV19IdcqsZTyDEe", "traefik.frontend.auth.basic=test:$2a$06$O5NksJPAcgrC9MuANkSoE.Xe9DSg7KcLLFYNr1Lj6hPcMmvgwxhme,test2:$2y$10$xP1SZ70QbZ4K2bTGKJOhpujkpcLxQcB3kEPF6XAV19IdcqsZTyDEe",
}) })
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress)
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
@ -360,17 +360,17 @@ func (s *ConsulCatalogSuite) TestRefreshConfigTagChange(c *check.C) {
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
defer cmd.Process.Kill() defer cmd.Process.Kill()
nginx := s.composeProject.Container(c, "nginx1") whoami := s.composeProject.Container(c, "whoami1")
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{"name=nginx1", "traefik.enable=false", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"}) err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1", "traefik.enable=false", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress)
err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 5*time.Second, err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 5*time.Second,
try.BodyContains(nginx.NetworkSettings.IPAddress)) try.BodyContains(whoami.NetworkSettings.IPAddress))
c.Assert(err, checker.NotNil) c.Assert(err, checker.NotNil)
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{"name=nginx1", "traefik.enable=true", "traefik.backend.circuitbreaker=ResponseCodeRatio(500, 600, 0, 600) > 0.5"}) err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1", "traefik.enable=true", "traefik.backend.circuitbreaker=ResponseCodeRatio(500, 600, 0, 600) > 0.5"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
@ -381,7 +381,7 @@ func (s *ConsulCatalogSuite) TestRefreshConfigTagChange(c *check.C) {
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second,
try.BodyContains(nginx.NetworkSettings.IPAddress)) try.BodyContains(whoami.NetworkSettings.IPAddress))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
} }
@ -402,19 +402,19 @@ func (s *ConsulCatalogSuite) TestCircuitBreaker(c *check.C) {
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
defer cmd.Process.Kill() defer cmd.Process.Kill()
nginx := s.composeProject.Container(c, "nginx1") whoami := s.composeProject.Container(c, "whoami1")
nginx2 := s.composeProject.Container(c, "nginx2") whoami2 := s.composeProject.Container(c, "whoami2")
nginx3 := s.composeProject.Container(c, "nginx3") whoami3 := s.composeProject.Container(c, "whoami3")
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{"name=nginx1", "traefik.enable=true", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"}) err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1", "traefik.enable=true", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress)
err = s.registerService("test", nginx2.NetworkSettings.IPAddress, 42, []string{"name=nginx2", "traefik.enable=true", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"}) err = s.registerService("test", whoami2.NetworkSettings.IPAddress, 42, []string{"name=whoami2", "traefik.enable=true", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", nginx2.NetworkSettings.IPAddress) defer s.deregisterService("test", whoami2.NetworkSettings.IPAddress)
err = s.registerService("test", nginx3.NetworkSettings.IPAddress, 42, []string{"name=nginx3", "traefik.enable=true", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"}) err = s.registerService("test", whoami3.NetworkSettings.IPAddress, 42, []string{"name=whoami3", "traefik.enable=true", "traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", nginx3.NetworkSettings.IPAddress) defer s.deregisterService("test", whoami3.NetworkSettings.IPAddress)
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
@ -437,9 +437,9 @@ func (s *ConsulCatalogSuite) TestRefreshConfigPortChange(c *check.C) {
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
defer cmd.Process.Kill() defer cmd.Process.Kill()
nginx := s.composeProject.Container(c, "nginx1") whoami := s.composeProject.Container(c, "whoami1")
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 81, []string{"name=nginx1", "traefik.enable=true"}) err = s.registerService("test", whoami.NetworkSettings.IPAddress, 81, []string{"name=whoami1", "traefik.enable=true"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
@ -449,15 +449,15 @@ func (s *ConsulCatalogSuite) TestRefreshConfigPortChange(c *check.C) {
err = try.Request(req, 20*time.Second, try.StatusCodeIs(http.StatusBadGateway)) err = try.Request(req, 20*time.Second, try.StatusCodeIs(http.StatusBadGateway))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 5*time.Second, try.BodyContains(nginx.NetworkSettings.IPAddress)) err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 5*time.Second, try.BodyContains(whoami.NetworkSettings.IPAddress))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{"name=nginx1", "traefik.enable=true"}) err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1", "traefik.enable=true"})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) defer s.deregisterService("test", whoami.NetworkSettings.IPAddress)
err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, try.BodyContains(nginx.NetworkSettings.IPAddress)) err = try.GetRequest("http://127.0.0.1:8080/api/providers/consul_catalog/backends", 60*time.Second, try.BodyContains(whoami.NetworkSettings.IPAddress))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
err = try.Request(req, 20*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) err = try.Request(req, 20*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody())
@ -496,9 +496,9 @@ func (s *ConsulCatalogSuite) TestRetryWithConsulServer(c *check.C) {
s.composeProject.Scale(c, "consul", 1) s.composeProject.Scale(c, "consul", 1)
s.waitToElectConsulLeader() s.waitToElectConsulLeader()
nginx := s.composeProject.Container(c, "nginx1") whoami := s.composeProject.Container(c, "whoami1")
// Register service // Register service
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{}) err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80, []string{})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
// Provider consul catalog should be present // Provider consul catalog should be present

View file

@ -14,7 +14,7 @@ import (
"github.com/docker/docker/pkg/namesgenerator" "github.com/docker/docker/pkg/namesgenerator"
"github.com/go-check/check" "github.com/go-check/check"
d "github.com/libkermit/docker" d "github.com/libkermit/docker"
docker "github.com/libkermit/docker-check" "github.com/libkermit/docker-check"
checker "github.com/vdemeester/shakers" checker "github.com/vdemeester/shakers"
) )
@ -26,7 +26,7 @@ var (
// FIXME handle this offline but loading them before build // FIXME handle this offline but loading them before build
RequiredImages = map[string]string{ RequiredImages = map[string]string{
"swarm": "1.0.0", "swarm": "1.0.0",
"nginx": "1", "emilevauge/whoami": "latest",
} }
) )

View file

@ -0,0 +1,25 @@
defaultEntryPoints = ["http"]
debug=true
[entryPoints]
[entryPoints.http]
address = ":8000"
[api]
[docker]
endpoint = "unix:///var/run/docker.sock"
watch = true
exposedbydefault = false
[file]
[frontends]
[frontends.frontend-1]
backend = "backend-test"
[frontends.frontend-1.routes.test_1]
rule = "PathPrefix:/file"
[backends]
[backends.backend-test]
[backends.backend-test.servers.website]
url = "http://{{ .IP }}"

View file

@ -19,7 +19,7 @@ func (s *RateLimitSuite) SetUpSuite(c *check.C) {
s.createComposeProject(c, "ratelimit") s.createComposeProject(c, "ratelimit")
s.composeProject.Start(c) s.composeProject.Start(c)
s.ServerIP = s.composeProject.Container(c, "nginx1").NetworkSettings.IPAddress s.ServerIP = s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress
} }
func (s *RateLimitSuite) TestSimpleConfiguration(c *check.C) { func (s *RateLimitSuite) TestSimpleConfiguration(c *check.C) {

View file

@ -3,3 +3,9 @@ whoami1:
labels: labels:
- traefik.enable=true - traefik.enable=true
- traefik.frontend.rule=PathPrefix:/whoami - traefik.frontend.rule=PathPrefix:/whoami
- traefik.backend="test"
whoami2:
image: emilevauge/whoami
labels:
- traefik.enable=false

View file

@ -11,7 +11,7 @@ consul:
- "8301/udp" - "8301/udp"
- "8302" - "8302"
- "8302/udp" - "8302/udp"
nginx: whoami:
image: nginx:alpine image: emilevauge/whoami
ports: ports:
- "8881:80" - "8881:80"

View file

@ -11,9 +11,9 @@ consul:
- "8301/udp" - "8301/udp"
- "8302" - "8302"
- "8302/udp" - "8302/udp"
nginx1: whoami1:
image: nginx:alpine image: emilevauge/whoami
nginx2: whoami2:
image: nginx:alpine image: emilevauge/whoami
nginx3: whoami3:
image: nginx:alpine image: emilevauge/whoami

View file

@ -1,4 +1,4 @@
nginx1: nginx1:
image: nginx:alpine image: nginx:1.13.8-alpine
nginx2: nginx2:
image: nginx:alpine image: nginx:1.13.8-alpine

View file

@ -1,20 +1,20 @@
nginx1: whoami1:
image: nginx:alpine image: emilevauge/whoami
ports: ports:
- "8881:80" - "8881:80"
nginx2: whoami2:
image: nginx:alpine image: emilevauge/whoami
ports: ports:
- "8882:80" - "8882:80"
nginx3: whoami3:
image: nginx:alpine image: emilevauge/whoami
ports: ports:
- "8883:80" - "8883:80"
nginx4: whoami4:
image: nginx:alpine image: emilevauge/whoami
ports: ports:
- "8884:80" - "8884:80"
nginx5: whoami5:
image: nginx:alpine image: emilevauge/whoami
ports: ports:
- "8885:80" - "8885:80"

View file

@ -1,2 +1,2 @@
nginx1: whoami1:
image: nginx:alpine image: emilevauge/whoami

View file

@ -873,7 +873,7 @@ func (s *Server) loadConfig(configurations types.Configurations, globalConfigura
backendsHealthCheck := map[string]*healthcheck.BackendHealthCheck{} backendsHealthCheck := map[string]*healthcheck.BackendHealthCheck{}
errorHandler := NewRecordingErrorHandler(middlewares.DefaultNetErrorRecorder{}) errorHandler := NewRecordingErrorHandler(middlewares.DefaultNetErrorRecorder{})
for _, config := range configurations { for providerName, config := range configurations {
frontendNames := sortedFrontendNamesForConfig(config) frontendNames := sortedFrontendNamesForConfig(config)
frontend: frontend:
for _, frontendName := range frontendNames { for _, frontendName := range frontendNames {
@ -925,7 +925,7 @@ func (s *Server) loadConfig(configurations types.Configurations, globalConfigura
redirectHandlers[entryPointName] = handlerToUse redirectHandlers[entryPointName] = handlerToUse
} }
} }
if backends[entryPointName+frontend.Backend] == nil { if backends[entryPointName+providerName+frontend.Backend] == nil {
log.Debugf("Creating backend %s", frontend.Backend) log.Debugf("Creating backend %s", frontend.Backend)
roundTripper, err := s.getRoundTripper(entryPointName, globalConfiguration, frontend.PassTLSCert, entryPoint.TLS) roundTripper, err := s.getRoundTripper(entryPointName, globalConfiguration, frontend.PassTLSCert, entryPoint.TLS)
@ -1172,14 +1172,14 @@ func (s *Server) loadConfig(configurations types.Configurations, globalConfigura
} else { } else {
n.UseHandler(lb) n.UseHandler(lb)
} }
backends[entryPointName+frontend.Backend] = n backends[entryPointName+providerName+frontend.Backend] = n
} else { } else {
log.Debugf("Reusing backend %s", frontend.Backend) log.Debugf("Reusing backend %s", frontend.Backend)
} }
if frontend.Priority > 0 { if frontend.Priority > 0 {
newServerRoute.route.Priority(frontend.Priority) newServerRoute.route.Priority(frontend.Priority)
} }
s.wireFrontendBackend(newServerRoute, backends[entryPointName+frontend.Backend]) s.wireFrontendBackend(newServerRoute, backends[entryPointName+providerName+frontend.Backend])
err := newServerRoute.route.GetError() err := newServerRoute.route.GetError()
if err != nil { if err != nil {

View file

@ -22,6 +22,7 @@
"animate.css": "^3.4.0", "animate.css": "^3.4.0",
"bootstrap": "^3.3.6", "bootstrap": "^3.3.6",
"http-status-codes": "^1.3.0", "http-status-codes": "^1.3.0",
"lodash": "^4.17.5",
"moment": "^2.14.1", "moment": "^2.14.1",
"nvd3": "^1.8.4" "nvd3": "^1.8.4"
}, },

View file

@ -1,5 +1,7 @@
'use strict'; 'use strict';
var _ = require('lodash');
/** @ngInject */ /** @ngInject */
function ProvidersController($scope, $interval, $log, Providers) { function ProvidersController($scope, $interval, $log, Providers) {
const vm = this; const vm = this;
@ -7,7 +9,12 @@ function ProvidersController($scope, $interval, $log, Providers) {
function loadProviders() { function loadProviders() {
Providers Providers
.get() .get()
.then(providers => vm.providers = providers) .then(providers => {
if (!_.isEqual(vm.previousProviders, providers)) {
vm.providers = providers;
vm.previousProviders = _.cloneDeep(providers);
}
})
.catch(error => { .catch(error => {
vm.providers = {}; vm.providers = {};
$log.error(error); $log.error(error);

View file

@ -1105,7 +1105,7 @@ camelcase@^1.0.2, camelcase@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39"
camelcase@^2.0.0, camelcase@^2.0.1: camelcase@^2.0.0:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
@ -3929,6 +3929,10 @@ lodash@^4.17.2:
version "4.17.4" version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
lodash@^4.17.5:
version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
lodash@~4.16.4: lodash@~4.16.4:
version "4.16.6" version "4.16.6"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.16.6.tgz#d22c9ac660288f3843e16ba7d2b5d06cca27d777" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.16.6.tgz#d22c9ac660288f3843e16ba7d2b5d06cca27d777"