Compare commits

..

66 Commits

Author SHA1 Message Date
Ludovic Fernandez
feeb7f81a6 Prepare Release v1.6.6 2018-08-20 14:46:02 +02:00
Damien Duportal
2beb5236d0 A tiny rewording on the documentation API's page 2018-08-20 13:34:03 +02:00
Damien Duportal
f062ee80c8 Docs: Adding warnings and solution about the configuration exposure 2018-08-20 12:02:03 +02:00
SALLEYRON Julien
a7bb768e98 Remove TLS in API 2018-08-20 11:16:02 +02:00
SALLEYRON Julien
07be89d6e9 Update oxy dependency 2018-08-20 10:38:03 +02:00
NicoMen
d81c4e6d1a Avoid duplicated ACME resolution 2018-08-20 09:40:03 +02:00
macros
60b4095c75 Set keepalive on TCP socket so idleTimeout works 2018-08-08 19:12:03 +02:00
Ludovic Fernandez
7ff6e6b66f Freeze mkdocs version 2018-08-06 15:50:03 +02:00
Daniel Tomcej
dbe720f0f1 Remove unusable --cluster flag 2018-07-13 17:32:03 +02:00
Jonathan Ballet
f0ab2721a5 Fix path to the debug pprof API 2018-07-12 17:58:02 +02:00
Michael
a7c158f0e1 Fix bad condition in ECS provider 2018-07-12 17:40:04 +02:00
Ludovic Fernandez
bdc0e3bfcf Prepare release v1.6.5 2018-07-10 17:46:04 +02:00
SALLEYRON Julien
f173ff02e3 Add a mutex on local store for HTTPChallenges 2018-07-09 23:28:02 +02:00
SALLEYRON Julien
bacd58ed7b Add logs when error is generated in error handler 2018-07-06 10:32:03 +02:00
Or Tzabary
f323df466d Split the error handling from Consul Catalog (deadlock) 2018-07-05 15:12:03 +02:00
Fabian Beuke
b1836587f2 Update keyFile first/last line comment in kv-config.md 2018-07-04 14:20:03 +02:00
John Yani
dbc3b85cd0 Minor formatting issue in user-guide 2018-06-29 17:02:03 +02:00
Jean-Baptiste Doumenjou
5eda08e9b8 Better support on same prefix at the same level in the KV 2018-06-26 16:18:05 +02:00
Ludovic Fernandez
ec6e46e2cb segment labels: multiple frontends for one backend. 2018-06-22 19:44:03 +02:00
Michael
aa705dd691 Create middleware to be able to handle HTTP pipelining correctly 2018-06-20 09:12:03 +02:00
manu5801
1c3e4124f8 The gandiv5 provider works with wildcard 2018-06-18 09:26:02 +02:00
Ludovic Fernandez
c1757372d3 Prepare release v1.6.4 2018-06-15 17:04:03 +02:00
Ludovic Fernandez
5b2b29043c Use logrus writer instead of os.Stderr 2018-06-15 16:08:03 +02:00
Ludovic Fernandez
bb3f28ffa7 Backend name with docker-compose and segments. 2018-06-14 09:20:04 +02:00
Ludovic Fernandez
6ceb2af4a7 Clean metrics documentation. 2018-06-13 17:44:04 +02:00
Ludovic Fernandez
b59276ff1c Use net.JoinHostPort for servers URL 2018-06-13 10:08:03 +02:00
herver
2e95832812 Enclose IPv6 addresses in "[]" 2018-06-13 08:06:03 +02:00
Yoan Blanc
2240bf9430 The exoscale provider works with wildcard 2018-06-12 16:16:03 +02:00
Shambarick
db036edccd Fix typo in kv user guide 2018-06-12 08:20:03 +02:00
Mayank Jethva
08e1f626c1 edit wording 2018-06-11 16:26:03 +02:00
Emile Vauge
c0d08f5e3e Temporary benchmarks removal during performance optimization process 2018-06-11 11:46:03 +02:00
Ludovic Fernandez
dec3f0798a Add missing annotation documentation. 2018-06-08 12:54:02 +02:00
SALLEYRON Julien
62ded580ce Handle buffer pool for oxy 2018-06-07 09:46:03 +02:00
Michael
446d73fcf5 Prepare release v1.6.3 2018-06-05 17:20:04 +02:00
Michael
e299775d67 Detect change when service or node are in maintenance mode 2018-06-05 16:42:03 +02:00
Marco Jantke
2c18750537 Improve Prometheus metrics removal 2018-06-05 12:32:03 +02:00
Jonas Thelemann
f317e50136 Docs: ACME Overhaul 2018-06-05 10:36:03 +02:00
Daniel Tomcej
1d84bda7ca Update Kubernetes provider to support IPv6 Backends 2018-06-05 10:28:03 +02:00
Ludovic Fernandez
ae7c947ba5 Add user agent for ACME 2018-06-05 09:54:03 +02:00
Ludovic Fernandez
6d07729c55 Add URL and Host for some access logs. 2018-06-05 09:02:03 +02:00
The Binary
1d7bf200a8 Reflect changes in catalog healthy nodes in healthCheck watch 2018-06-05 08:42:03 +02:00
Gérald Croës
6bc59f8b33 Includes the headers in the HTTP response of a websocket request 2018-06-04 14:14:03 +02:00
Ludovic Fernandez
b2cf03fa5c Use to the stable version of Lego 2018-05-31 09:30:04 +02:00
Erik Aulin
36e273714d helm installation using values 2018-05-30 17:38:04 +02:00
Ludovic Fernandez
6be77b7fb9 Webui Whitelist overflow. 2018-05-30 09:44:02 +02:00
Ludovic Fernandez
6bcf45f136 fix: whitelist and XFF. 2018-05-30 09:26:03 +02:00
Amin Cheloh
8bca8236db Update acme.md 2018-05-30 04:16:03 +02:00
Ludovic Fernandez
fb5aa4c9c1 Error pages and header merge 2018-05-28 15:00:04 +02:00
NicoMen
3f5772c62a Improve ACME account registration URI management 2018-05-28 14:40:03 +02:00
NicoMen
2d946d7ee7 Remove ACME empty certificates from KV store 2018-05-25 19:36:04 +02:00
Daniel Tomcej
10ca35dccd Update Kubernetes Port Documentation 2018-05-25 17:26:05 +02:00
SALLEYRON Julien
e3671cbb04 Prepare release 1.6.2 2018-05-22 17:06:04 +02:00
Ludovic Fernandez
a525d02cc5 Force to use ACME v02 endpoint. 2018-05-22 16:08:03 +02:00
Ludovic Fernandez
1cc1a4e6e2 WebUI typo into the buffering section. 2018-05-22 12:28:03 +02:00
SALLEYRON Julien
3f0af3fe09 No template parsing on traefik configuration file 2018-05-22 12:02:03 +02:00
Ludovic Fernandez
bc26d9f0de Enhance Load-balancing method validation log. 2018-05-22 09:20:03 +02:00
Florian Woerner
0ba28bbc8b Update caServer to letsencrypt one in examples 2018-05-18 18:16:03 +02:00
Michael Ermer
550184275a Add basicAuth example for KV 2018-05-18 17:54:03 +02:00
DeamonMV
c376083ecb Add command for basic auth with Docker Compose 2018-05-18 17:34:04 +02:00
Ludovic Fernandez
1db5fcf200 Removes ambiguity with the word 'default' 2018-05-18 17:12:03 +02:00
Ludovic Fernandez
16b2555ab3 Fix error pages content. 2018-05-18 16:38:03 +02:00
Joni Collinge
d9a5258f40 Update docs to match SF provider labels 2018-05-17 11:04:03 +02:00
Daniel Tomcej
190ebbed27 Update docs to reflect Provider wording 2018-05-16 19:48:03 +02:00
Ludovic Fernandez
a0872c9e31 Route rules overlaps in UI 2018-05-16 13:44:03 +02:00
Daniel Tomcej
f5b306e7ff Add redirect-permanent to kubernetes template 2018-05-16 10:46:03 +02:00
Ludovic Fernandez
7a1feb3c51 fix: acme errors management. 2018-05-15 17:28:02 +02:00
245 changed files with 7663 additions and 3012 deletions

View File

@@ -1,5 +1,103 @@
# Change Log
## [v1.6.6](https://github.com/containous/traefik/tree/v1.6.6) (2018-08-20)
[All Commits](https://github.com/containous/traefik/compare/v1.6.5...v1.6.6)
**Bug fixes:**
- **[acme]** Avoid duplicated ACME resolution ([#3751](https://github.com/containous/traefik/pull/3751) by [nmengin](https://github.com/nmengin))
- **[api]** Remove TLS in API ([#3788](https://github.com/containous/traefik/pull/3788) by [Juliens](https://github.com/Juliens))
- **[cluster]** Remove unusable `--cluster` flag ([#3616](https://github.com/containous/traefik/pull/3616) by [dtomcej](https://github.com/dtomcej))
- **[ecs]** Fix bad condition in ECS provider ([#3609](https://github.com/containous/traefik/pull/3609) by [mmatur](https://github.com/mmatur))
- Set keepalive on TCP socket so idleTimeout works ([#3740](https://github.com/containous/traefik/pull/3740) by [ajardan](https://github.com/ajardan))
**Documentation:**
- A tiny rewording on the documentation API's page ([#3794](https://github.com/containous/traefik/pull/3794) by [dduportal](https://github.com/dduportal))
- Adding warnings and solution about the configuration exposure ([#3790](https://github.com/containous/traefik/pull/3790) by [dduportal](https://github.com/dduportal))
- Fix path to the debug pprof API ([#3608](https://github.com/containous/traefik/pull/3608) by [multani](https://github.com/multani))
**Misc:**
- **[oxy,websocket]** Update oxy dependency ([#3777](https://github.com/containous/traefik/pull/3777) by [Juliens](https://github.com/Juliens))
## [v1.6.5](https://github.com/containous/traefik/tree/v1.6.5) (2018-07-09)
[All Commits](https://github.com/containous/traefik/compare/v1.6.4...v1.6.5)
**Bug fixes:**
- **[acme]** Add a mutex on local store for HTTPChallenges ([#3579](https://github.com/containous/traefik/pull/3579) by [Juliens](https://github.com/Juliens))
- **[consulcatalog]** Split the error handling from Consul Catalog (deadlock) ([#3560](https://github.com/containous/traefik/pull/3560) by [ortz](https://github.com/ortz))
- **[docker]** segment labels: multiple frontends for one backend. ([#3511](https://github.com/containous/traefik/pull/3511) by [ldez](https://github.com/ldez))
- **[kv]** Better support on same prefix at the same level in the KV ([#3532](https://github.com/containous/traefik/pull/3532) by [jbdoumenjou](https://github.com/jbdoumenjou))
- **[logs]** Add logs when error is generated in error handler ([#3567](https://github.com/containous/traefik/pull/3567) by [Juliens](https://github.com/Juliens))
- **[middleware]** Create middleware to be able to handle HTTP pipelining correctly ([#3513](https://github.com/containous/traefik/pull/3513) by [mmatur](https://github.com/mmatur))
**Documentation:**
- **[acme]** The gandiv5 provider works with wildcard ([#3506](https://github.com/containous/traefik/pull/3506) by [manu5801](https://github.com/manu5801))
- **[kv]** Update keyFile first/last line comment in kv-config.md ([#3558](https://github.com/containous/traefik/pull/3558) by [madnight](https://github.com/madnight))
- Minor formatting issue in user-guide ([#3546](https://github.com/containous/traefik/pull/3546) by [Vanuan](https://github.com/Vanuan))
## [v1.6.4](https://github.com/containous/traefik/tree/v1.6.4) (2018-06-15)
[All Commits](https://github.com/containous/traefik/compare/v1.6.3...v1.6.4)
**Bug fixes:**
- **[acme]** Use logrus writer instead of os.Stderr ([#3498](https://github.com/containous/traefik/pull/3498) by [ldez](https://github.com/ldez))
- **[consulcatalog]** Enclose IPv6 addresses in "[]" ([#3477](https://github.com/containous/traefik/pull/3477) by [herver](https://github.com/herver))
- **[docker,ecs,marathon,mesos,rancher]** Use net.JoinHostPort for servers URL ([#3484](https://github.com/containous/traefik/pull/3484) by [ldez](https://github.com/ldez))
- **[docker]** Backend name with docker-compose and segments. ([#3485](https://github.com/containous/traefik/pull/3485) by [ldez](https://github.com/ldez))
- **[oxy]** Handle buffer pool for oxy ([#3450](https://github.com/containous/traefik/pull/3450) by [Juliens](https://github.com/Juliens))
**Documentation:**
- **[acme]** The exoscale provider works with wildcard ([#3479](https://github.com/containous/traefik/pull/3479) by [greut](https://github.com/greut))
- **[consul,docker]** Edit wording ([#3438](https://github.com/containous/traefik/pull/3438) by [mayank23](https://github.com/mayank23))
- **[k8s]** Add missing annotation documentation. ([#3454](https://github.com/containous/traefik/pull/3454) by [ldez](https://github.com/ldez))
- **[kv]** Fix typo in kv user guide ([#3474](https://github.com/containous/traefik/pull/3474) by [shambarick](https://github.com/shambarick))
- Clean metrics documentation. ([#3488](https://github.com/containous/traefik/pull/3488) by [ldez](https://github.com/ldez))
## [v1.6.3](https://github.com/containous/traefik/tree/v1.6.3) (2018-06-05)
[All Commits](https://github.com/containous/traefik/compare/v1.6.2...v1.6.3)
**Enhancements:**
- **[acme]** Add user agent for ACME ([#3431](https://github.com/containous/traefik/pull/3431) by [ldez](https://github.com/ldez))
- **[acme]** Use to the stable version of Lego ([#3418](https://github.com/containous/traefik/pull/3418) by [ldez](https://github.com/ldez))
**Bug fixes:**
- **[acme,cluster]** Improve ACME account registration URI management ([#3398](https://github.com/containous/traefik/pull/3398) by [nmengin](https://github.com/nmengin))
- **[acme,cluster]** Remove ACME empty certificates from KV store ([#3389](https://github.com/containous/traefik/pull/3389) by [nmengin](https://github.com/nmengin))
- **[consulcatalog]** Reflect changes in catalog healthy nodes in healthCheck watch ([#3390](https://github.com/containous/traefik/pull/3390) by [thebinary](https://github.com/thebinary))
- **[consulcatalog]** Detect change when service or node are in maintenance mode ([#3434](https://github.com/containous/traefik/pull/3434) by [mmatur](https://github.com/mmatur))
- **[k8s]** Update Kubernetes provider to support IPv6 Backends ([#3432](https://github.com/containous/traefik/pull/3432) by [dtomcej](https://github.com/dtomcej))
- **[logs,middleware]** Add URL and Host for some access logs. ([#3430](https://github.com/containous/traefik/pull/3430) by [ldez](https://github.com/ldez))
- **[metrics]** Improve Prometheus metrics removal ([#3287](https://github.com/containous/traefik/pull/3287) by [marco-jantke](https://github.com/marco-jantke))
- **[middleware]** Whitelist and XFF. ([#3411](https://github.com/containous/traefik/pull/3411) by [ldez](https://github.com/ldez))
- **[middleware]** Error pages and header merge ([#3394](https://github.com/containous/traefik/pull/3394) by [ldez](https://github.com/ldez))
- **[websocket]** Includes the headers in the HTTP response of a websocket request ([#3425](https://github.com/containous/traefik/pull/3425) by [geraldcroes](https://github.com/geraldcroes))
- **[webui]** Webui Whitelist overflow. ([#3412](https://github.com/containous/traefik/pull/3412) by [ldez](https://github.com/ldez))
**Documentation:**
- **[acme]** Docs: ACME Overhaul ([#3421](https://github.com/containous/traefik/pull/3421) by [Dargmuesli](https://github.com/Dargmuesli))
- **[acme]** Minor documentation changes ([#3405](https://github.com/containous/traefik/pull/3405) by [amincheloh](https://github.com/amincheloh))
- **[k8s]** Helm installation using values ([#3392](https://github.com/containous/traefik/pull/3392) by [erikaulin](https://github.com/erikaulin))
- **[k8s]** Update Kubernetes Port Documentation ([#3368](https://github.com/containous/traefik/pull/3368) by [dtomcej](https://github.com/dtomcej))
## [v1.6.2](https://github.com/containous/traefik/tree/v1.6.2) (2018-05-22)
[All Commits](https://github.com/containous/traefik/compare/v1.6.1...v1.6.2)
**Bug fixes:**
- **[acme]** fix: acme errors management. ([#3329](https://github.com/containous/traefik/pull/3329) by [ldez](https://github.com/ldez))
- **[acme]** Force to use ACME v02 endpoint. ([#3358](https://github.com/containous/traefik/pull/3358) by [ldez](https://github.com/ldez))
- **[file]** No template parsing on traefik configuration file ([#3347](https://github.com/containous/traefik/pull/3347) by [Juliens](https://github.com/Juliens))
- **[k8s]** Add redirect-permanent to kubernetes template ([#3332](https://github.com/containous/traefik/pull/3332) by [dtomcej](https://github.com/dtomcej))
- **[logs]** Enhance Load-balancing method validation log. ([#3361](https://github.com/containous/traefik/pull/3361) by [ldez](https://github.com/ldez))
- **[middleware]** Fix error pages content. ([#3337](https://github.com/containous/traefik/pull/3337) by [ldez](https://github.com/ldez))
- **[webui]** Route rules overlaps in UI ([#3333](https://github.com/containous/traefik/pull/3333) by [ldez](https://github.com/ldez))
- **[webui]** WebUI typo into the buffering section. ([#3363](https://github.com/containous/traefik/pull/3363) by [ldez](https://github.com/ldez))
**Documentation:**
- **[acme]** Update caServer to letsencrypt one in examples ([#3339](https://github.com/containous/traefik/pull/3339) by [woernfl](https://github.com/woernfl))
- **[docker]** Add command for basic auth with Docker Compose ([#3346](https://github.com/containous/traefik/pull/3346) by [DeamonMV](https://github.com/DeamonMV))
- **[docker]** Removes ambiguity with the word 'default' ([#3344](https://github.com/containous/traefik/pull/3344) by [ldez](https://github.com/ldez))
- **[kv]** Add basicAuth example for KV ([#3274](https://github.com/containous/traefik/pull/3274) by [MichaelErmer](https://github.com/MichaelErmer))
- **[provider]** Update docs to reflect Provider wording ([#3331](https://github.com/containous/traefik/pull/3331) by [dtomcej](https://github.com/dtomcej))
- **[servicefabric]** Update docs to match SF provider labels ([#3335](https://github.com/containous/traefik/pull/3335) by [jjcollinge](https://github.com/jjcollinge))
## [v1.6.1](https://github.com/containous/traefik/tree/v1.6.1) (2018-05-14)
[All Commits](https://github.com/containous/traefik/compare/v1.6.0...v1.6.1)

33
Gopkg.lock generated
View File

@@ -257,8 +257,8 @@
[[projects]]
name = "github.com/containous/staert"
packages = ["."]
revision = "cc00c303ccbd2491ddc1dccc9eb7ccadd807557e"
version = "v3.1.0"
revision = "66717a0e0ca950c4b6dc8c87b46da0b8495c6e41"
version = "v3.1.1"
[[projects]]
name = "github.com/containous/traefik-extra-service-fabric"
@@ -320,9 +320,10 @@
version = "v3.2.0"
[[projects]]
branch = "master"
name = "github.com/dnsimple/dnsimple-go"
packages = ["dnsimple"]
revision = "f2d9b723cc9547d182e24ac2e527ae25d25fc93f"
revision = "bbe1a2c87affea187478e24d3aea3cac25f870b3"
[[projects]]
name = "github.com/docker/cli"
@@ -991,9 +992,10 @@
revision = "1f5c07e90700ae93ddcba0c7af7d9c7201646ccc"
[[projects]]
branch = "master"
name = "github.com/ovh/go-ovh"
packages = ["ovh"]
revision = "4b1fea467323b74c5f462f0947f402b428ca0626"
revision = "91b7eb631d2eced3e706932a0b36ee8b5ee22e92"
[[projects]]
branch = "master"
@@ -1215,7 +1217,7 @@
"roundrobin",
"utils"
]
revision = "6956548a7fa4272adeadf828455109c53933ea86"
revision = "885e42fe04d8e0efa6c18facad4e0fc5757cde9b"
[[projects]]
name = "github.com/vulcand/predicate"
@@ -1241,10 +1243,11 @@
revision = "0c8571ac0ce161a5feb57375a9cdf148c98c0f70"
[[projects]]
branch = "containous-fork"
branch = "master"
name = "github.com/xenolf/lego"
packages = [
"acmev2",
"acme",
"log",
"providers/dns",
"providers/dns/auroradns",
"providers/dns/azure",
@@ -1262,9 +1265,9 @@
"providers/dns/fastdns",
"providers/dns/gandi",
"providers/dns/gandiv5",
"providers/dns/gcloud",
"providers/dns/glesys",
"providers/dns/godaddy",
"providers/dns/googlecloud",
"providers/dns/lightsail",
"providers/dns/linode",
"providers/dns/namecheap",
@@ -1278,8 +1281,7 @@
"providers/dns/route53",
"providers/dns/vultr"
]
revision = "2817d2131186742bc98830c73a5d9c255b3f4537"
source = "github.com/containous/lego"
revision = "7fedfd1388f016c7ca7ed92a7f2024d06a7e20d8"
[[projects]]
branch = "master"
@@ -1319,6 +1321,7 @@
revision = "22ae77b79946ea320088417e4d50825671d82d57"
[[projects]]
branch = "master"
name = "golang.org/x/oauth2"
packages = [
".",
@@ -1327,7 +1330,7 @@
"jws",
"jwt"
]
revision = "7fdf09982454086d5570c7db3e11f360194830ca"
revision = "ec22f46f877b4505e0117eeaab541714644fdd28"
[[projects]]
branch = "master"
@@ -1366,6 +1369,7 @@
revision = "8be79e1e0910c292df4e79c241bb7e8f7e725959"
[[projects]]
branch = "master"
name = "google.golang.org/api"
packages = [
"dns/v1",
@@ -1373,7 +1377,7 @@
"googleapi",
"googleapi/internal/uritemplates"
]
revision = "1575df15c1bb8b18ad4d9bc5ca495cc85b0764fe"
revision = "de943baf05a022a8f921b544b7827bacaba1aed5"
[[projects]]
name = "google.golang.org/appengine"
@@ -1444,6 +1448,7 @@
revision = "cb884138e64c9a8bf5c7d6106d74b0fca082df0c"
[[projects]]
branch = "v2"
name = "gopkg.in/ns1/ns1-go.v2"
packages = [
"rest",
@@ -1453,7 +1458,7 @@
"rest/model/filter",
"rest/model/monitor"
]
revision = "c563826f4cbef9c11bebeb9f20a3f7afe9c1e2f4"
revision = "a5bcac82d3f637d3928d30476610891935b2d691"
[[projects]]
name = "gopkg.in/square/go-jose.v2"
@@ -1674,6 +1679,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "c7d91203842be1915ca08a31917a079489bff7ffc6f2e494330e9556b4730a06"
inputs-digest = "ad34e6336e6f19b82c52e991d22c5b43b9144ed7dc83d7b17197583ace43f346"
solver-name = "gps-cdcl"
solver-version = 1

View File

@@ -62,7 +62,7 @@
[[constraint]]
name = "github.com/containous/staert"
version = "3.1.0"
version = "3.1.1"
[[constraint]]
name = "github.com/containous/traefik-extra-service-fabric"
@@ -181,9 +181,9 @@
name = "github.com/vulcand/oxy"
[[constraint]]
branch = "containous-fork"
branch = "master"
name = "github.com/xenolf/lego"
source = "github.com/containous/lego"
# version = "1.0.0"
[[constraint]]
name = "google.golang.org/grpc"

View File

@@ -63,7 +63,7 @@ _(But if you'd rather configure some of your routes manually, Træfik supports t
- Websocket, HTTP/2, GRPC ready
- Provides metrics (Rest, Prometheus, Datadog, Statsd, InfluxDB)
- Keeps access logs (JSON, CLF)
- [Fast](https://docs.traefik.io/benchmarks) ... which is nice
- Fast
- Exposes a Rest API
- Packaged as a single binary file (made with :heart: with go) and available as a [tiny](https://microbadger.com/images/traefik) [official](https://hub.docker.com/r/_/traefik/) docker image

View File

@@ -8,14 +8,16 @@ import (
"crypto/x509"
"fmt"
"reflect"
"regexp"
"sort"
"strings"
"sync"
"time"
"github.com/containous/traefik/log"
acmeprovider "github.com/containous/traefik/provider/acme"
"github.com/containous/traefik/types"
acme "github.com/xenolf/lego/acmev2"
"github.com/xenolf/lego/acme"
)
// Account is used to store lets encrypt registration info
@@ -42,6 +44,11 @@ func (a *Account) Init() error {
return err
}
err = a.RemoveAccountV1Values()
if err != nil {
log.Errorf("Unable to remove ACME Account V1 values during account initialization: %v", err)
}
for _, cert := range a.ChallengeCerts {
if cert.certificate == nil {
certificate, err := tls.X509KeyPair(cert.Certificate, cert.PrivateKey)
@@ -103,6 +110,29 @@ func (a *Account) GetPrivateKey() crypto.PrivateKey {
return nil
}
// RemoveAccountV1Values removes ACME account V1 values
func (a *Account) RemoveAccountV1Values() error {
// Check if ACME Account is in ACME V1 format
if a.Registration != nil {
isOldRegistration, err := regexp.MatchString(acmeprovider.RegistrationURLPathV1Regexp, a.Registration.URI)
if err != nil {
return err
}
if isOldRegistration {
a.reset()
}
}
return nil
}
func (a *Account) reset() {
log.Debug("Reset ACME account object.")
a.Email = ""
a.Registration = nil
a.PrivateKey = nil
}
// Certificate is used to store certificate info
type Certificate struct {
Domain string
@@ -152,11 +182,23 @@ func (dc *DomainsCertificates) removeDuplicates() {
}
}
func (dc *DomainsCertificates) removeEmpty() {
certs := []*DomainsCertificate{}
for _, cert := range dc.Certs {
if cert.Certificate != nil && len(cert.Certificate.Certificate) > 0 && len(cert.Certificate.PrivateKey) > 0 {
certs = append(certs, cert)
}
}
dc.Certs = certs
}
// Init DomainsCertificates
func (dc *DomainsCertificates) Init() error {
dc.lock.Lock()
defer dc.lock.Unlock()
dc.removeEmpty()
for _, domainsCertificate := range dc.Certs {
tlsCert, err := tls.X509KeyPair(domainsCertificate.Certificate.Certificate, domainsCertificate.Certificate.PrivateKey)
if err != nil {

View File

@@ -9,9 +9,9 @@ import (
fmtlog "log"
"net"
"net/http"
"os"
"reflect"
"strings"
"sync"
"time"
"github.com/BurntSushi/ty/fun"
@@ -25,8 +25,11 @@ import (
"github.com/containous/traefik/safe"
"github.com/containous/traefik/tls/generate"
"github.com/containous/traefik/types"
"github.com/containous/traefik/version"
"github.com/eapache/channels"
acme "github.com/xenolf/lego/acmev2"
"github.com/sirupsen/logrus"
"github.com/xenolf/lego/acme"
legolog "github.com/xenolf/lego/log"
"github.com/xenolf/lego/providers/dns"
)
@@ -42,7 +45,7 @@ type ACME struct {
Domains []types.Domain `description:"SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='main.net,san1.net,san2.net'"`
Storage string `description:"File or key used for certificates storage."`
StorageFile string // Deprecated
OnDemand bool `description:"(Deprecated) Enable on demand certificate generation. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate."` //deprecated
OnDemand bool `description:"(Deprecated) Enable on demand certificate generation. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate."` // Deprecated
OnHostRule bool `description:"Enable certificate generation on frontends Host rules."`
CAServer string `description:"CA server to use."`
EntryPoint string `description:"Entrypoint to proxy acme challenge to."`
@@ -59,22 +62,32 @@ type ACME struct {
jobs *channels.InfiniteChannel
TLSConfig *tls.Config `description:"TLS config in case wildcard certs are used"`
dynamicCerts *safe.Safe
resolvingDomains map[string]struct{}
resolvingDomainsMutex sync.RWMutex
}
func (a *ACME) init() error {
acme.UserAgent = fmt.Sprintf("containous-traefik/%s", version.Version)
if a.ACMELogging {
acme.Logger = fmtlog.New(os.Stderr, "legolog: ", fmtlog.LstdFlags)
legolog.Logger = fmtlog.New(log.WriterLevel(logrus.DebugLevel), "legolog: ", 0)
} else {
acme.Logger = fmtlog.New(ioutil.Discard, "", 0)
legolog.Logger = fmtlog.New(ioutil.Discard, "", 0)
}
// no certificates in TLS config, so we add a default one
cert, err := generate.DefaultCertificate()
if err != nil {
return err
}
a.defaultCertificate = cert
a.jobs = channels.NewInfiniteChannel()
// Init the currently resolved domain map
a.resolvingDomains = make(map[string]struct{})
return nil
}
@@ -178,6 +191,10 @@ func (a *ACME) leadershipListener(elected bool) error {
account := object.(*Account)
account.Init()
// Reset Account values if caServer changed, thus registration URI can be updated
if account != nil && account.Registration != nil && !strings.HasPrefix(account.Registration.URI, a.CAServer) {
account.reset()
}
var needRegister bool
if account == nil || len(account.Email) == 0 {
@@ -492,6 +509,10 @@ func (a *ACME) LoadCertificateForDomains(domains []string) {
if len(uncheckedDomains) == 0 {
return
}
a.addResolvingDomains(uncheckedDomains)
defer a.removeResolvingDomains(uncheckedDomains)
certificate, err := a.getDomainsCertificates(uncheckedDomains)
if err != nil {
log.Errorf("Error getting ACME certificates %+v : %v", uncheckedDomains, err)
@@ -523,6 +544,24 @@ func (a *ACME) LoadCertificateForDomains(domains []string) {
}
}
func (a *ACME) addResolvingDomains(resolvingDomains []string) {
a.resolvingDomainsMutex.Lock()
defer a.resolvingDomainsMutex.Unlock()
for _, domain := range resolvingDomains {
a.resolvingDomains[domain] = struct{}{}
}
}
func (a *ACME) removeResolvingDomains(resolvingDomains []string) {
a.resolvingDomainsMutex.Lock()
defer a.resolvingDomainsMutex.Unlock()
for _, domain := range resolvingDomains {
delete(a.resolvingDomains, domain)
}
}
// Get provided certificate which check a domains list (Main and SANs)
// from static and dynamic provided certificates
func (a *ACME) getProvidedCertificate(domains string) *tls.Certificate {
@@ -558,6 +597,9 @@ func searchProvidedCertificateForDomains(domain string, certs map[string]*tls.Ce
// Get provided certificate which check a domains list (Main and SANs)
// from static and dynamic provided certificates
func (a *ACME) getUncheckedDomains(domains []string, account *Account) []string {
a.resolvingDomainsMutex.RLock()
defer a.resolvingDomainsMutex.RUnlock()
log.Debugf("Looking for provided certificate to validate %s...", domains)
allCerts := make(map[string]*tls.Certificate)
@@ -580,6 +622,13 @@ func (a *ACME) getUncheckedDomains(domains []string, account *Account) []string
}
}
// Get currently resolved domains
for domain := range a.resolvingDomains {
if _, ok := allCerts[domain]; !ok {
allCerts[domain] = &tls.Certificate{}
}
}
// Get Configuration Domains
for i := 0; i < len(a.Domains); i++ {
allCerts[a.Domains[i].Main] = &tls.Certificate{}
@@ -611,11 +660,13 @@ func (a *ACME) getDomainsCertificates(domains []string) (*Certificate, error) {
domains = fun.Map(types.CanonicalDomain, domains).([]string)
log.Debugf("Loading ACME certificates %s...", domains)
bundle := true
certificate, failures := a.client.ObtainCertificate(domains, bundle, nil, OSCPMustStaple)
if len(failures) > 0 {
log.Error(failures)
return nil, fmt.Errorf("cannot obtain certificates %+v", failures)
certificate, err := a.client.ObtainCertificate(domains, bundle, nil, OSCPMustStaple)
if err != nil {
log.Error(err)
return nil, fmt.Errorf("cannot obtain certificates: %+v", err)
}
log.Debugf("Loaded ACME certificates %s", domains)
return &Certificate{
Domain: certificate.Domain,

View File

@@ -6,6 +6,7 @@ import (
"net/http"
"net/http/httptest"
"reflect"
"sort"
"sync"
"testing"
"time"
@@ -14,7 +15,7 @@ import (
"github.com/containous/traefik/tls/generate"
"github.com/containous/traefik/types"
"github.com/stretchr/testify/assert"
acme "github.com/xenolf/lego/acmev2"
"github.com/xenolf/lego/acme"
)
func TestDomainsSet(t *testing.T) {
@@ -330,9 +331,12 @@ func TestAcme_getUncheckedCertificates(t *testing.T) {
mm["*.containo.us"] = &tls.Certificate{}
mm["traefik.acme.io"] = &tls.Certificate{}
a := ACME{TLSConfig: &tls.Config{NameToCertificate: mm}}
dm := make(map[string]struct{})
dm["*.traefik.wtf"] = struct{}{}
domains := []string{"traefik.containo.us", "trae.containo.us"}
a := ACME{TLSConfig: &tls.Config{NameToCertificate: mm}, resolvingDomains: dm}
domains := []string{"traefik.containo.us", "trae.containo.us", "foo.traefik.wtf"}
uncheckedDomains := a.getUncheckedDomains(domains, nil)
assert.Empty(t, uncheckedDomains)
domains = []string{"traefik.acme.io", "trae.acme.io"}
@@ -350,6 +354,9 @@ func TestAcme_getUncheckedCertificates(t *testing.T) {
account := Account{DomainsCertificate: domainsCertificates}
uncheckedDomains = a.getUncheckedDomains(domains, &account)
assert.Empty(t, uncheckedDomains)
domains = []string{"traefik.containo.us", "trae.containo.us", "traefik.wtf"}
uncheckedDomains = a.getUncheckedDomains(domains, nil)
assert.Len(t, uncheckedDomains, 1)
}
func TestAcme_getProvidedCertificate(t *testing.T) {
@@ -550,3 +557,268 @@ func TestAcme_getCertificateForDomain(t *testing.T) {
})
}
}
func TestRemoveEmptyCertificates(t *testing.T) {
now := time.Now()
fooCert, fooKey, _ := generate.KeyPair("foo.com", now)
acmeCert, acmeKey, _ := generate.KeyPair("acme.wtf", now.Add(24*time.Hour))
barCert, barKey, _ := generate.KeyPair("bar.com", now)
testCases := []struct {
desc string
dc *DomainsCertificates
expectedDc *DomainsCertificates
}{
{
desc: "No empty certificate",
dc: &DomainsCertificates{
Certs: []*DomainsCertificate{
{
Certificate: &Certificate{
Certificate: fooCert,
PrivateKey: fooKey,
},
Domains: types.Domain{
Main: "foo.com",
},
},
{
Certificate: &Certificate{
Certificate: acmeCert,
PrivateKey: acmeKey,
},
Domains: types.Domain{
Main: "acme.wtf",
},
},
{
Certificate: &Certificate{
Certificate: barCert,
PrivateKey: barKey,
},
Domains: types.Domain{
Main: "bar.com",
},
},
},
},
expectedDc: &DomainsCertificates{
Certs: []*DomainsCertificate{
{
Certificate: &Certificate{
Certificate: fooCert,
PrivateKey: fooKey,
},
Domains: types.Domain{
Main: "foo.com",
},
},
{
Certificate: &Certificate{
Certificate: acmeCert,
PrivateKey: acmeKey,
},
Domains: types.Domain{
Main: "acme.wtf",
},
},
{
Certificate: &Certificate{
Certificate: barCert,
PrivateKey: barKey,
},
Domains: types.Domain{
Main: "bar.com",
},
},
},
},
},
{
desc: "First certificate is nil",
dc: &DomainsCertificates{
Certs: []*DomainsCertificate{
{
Domains: types.Domain{
Main: "foo.com",
},
},
{
Certificate: &Certificate{
Certificate: acmeCert,
PrivateKey: acmeKey,
},
Domains: types.Domain{
Main: "acme.wtf",
},
},
{
Certificate: &Certificate{
Certificate: barCert,
PrivateKey: barKey,
},
Domains: types.Domain{
Main: "bar.com",
},
},
},
},
expectedDc: &DomainsCertificates{
Certs: []*DomainsCertificate{
{
Certificate: &Certificate{
Certificate: acmeCert,
PrivateKey: acmeKey,
},
Domains: types.Domain{
Main: "acme.wtf",
},
},
{
Certificate: &Certificate{
Certificate: nil,
PrivateKey: barKey,
},
Domains: types.Domain{
Main: "bar.com",
},
},
},
},
},
{
desc: "Last certificate is empty",
dc: &DomainsCertificates{
Certs: []*DomainsCertificate{
{
Certificate: &Certificate{
Certificate: fooCert,
PrivateKey: fooKey,
},
Domains: types.Domain{
Main: "foo.com",
},
},
{
Certificate: &Certificate{
Certificate: acmeCert,
PrivateKey: acmeKey,
},
Domains: types.Domain{
Main: "acme.wtf",
},
},
{
Certificate: &Certificate{},
Domains: types.Domain{
Main: "bar.com",
},
},
},
},
expectedDc: &DomainsCertificates{
Certs: []*DomainsCertificate{
{
Certificate: &Certificate{
Certificate: fooCert,
PrivateKey: fooKey,
},
Domains: types.Domain{
Main: "foo.com",
},
},
{
Certificate: &Certificate{
Certificate: acmeCert,
PrivateKey: acmeKey,
},
Domains: types.Domain{
Main: "acme.wtf",
},
},
},
},
},
{
desc: "First and last certificates are nil or empty",
dc: &DomainsCertificates{
Certs: []*DomainsCertificate{
{
Domains: types.Domain{
Main: "foo.com",
},
},
{
Certificate: &Certificate{
Certificate: acmeCert,
PrivateKey: acmeKey,
},
Domains: types.Domain{
Main: "acme.wtf",
},
},
{
Certificate: &Certificate{},
Domains: types.Domain{
Main: "bar.com",
},
},
},
},
expectedDc: &DomainsCertificates{
Certs: []*DomainsCertificate{
{
Certificate: &Certificate{
Certificate: acmeCert,
PrivateKey: acmeKey,
},
Domains: types.Domain{
Main: "acme.wtf",
},
},
},
},
},
{
desc: "All certificates are nil or empty",
dc: &DomainsCertificates{
Certs: []*DomainsCertificate{
{
Domains: types.Domain{
Main: "foo.com",
},
},
{
Domains: types.Domain{
Main: "foo24.com",
},
},
{
Certificate: &Certificate{},
Domains: types.Domain{
Main: "bar.com",
},
},
},
},
expectedDc: &DomainsCertificates{
Certs: []*DomainsCertificate{},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
a := &Account{DomainsCertificate: *test.dc}
a.Init()
assert.Equal(t, len(test.expectedDc.Certs), len(a.DomainsCertificate.Certs))
sort.Sort(&a.DomainsCertificate)
sort.Sort(test.expectedDc)
for key, value := range test.expectedDc.Certs {
assert.Equal(t, value.Domains.Main, a.DomainsCertificate.Certs[key].Domains.Main)
}
})
}
}

View File

@@ -9,7 +9,7 @@ import (
"github.com/containous/traefik/cluster"
"github.com/containous/traefik/log"
"github.com/containous/traefik/safe"
acme "github.com/xenolf/lego/acmev2"
"github.com/xenolf/lego/acme"
)
var _ acme.ChallengeProviderTimeout = (*challengeHTTPProvider)(nil)

View File

@@ -4,7 +4,6 @@ import (
"encoding/json"
"io/ioutil"
"os"
"regexp"
"github.com/containous/traefik/log"
"github.com/containous/traefik/provider/acme"
@@ -51,24 +50,6 @@ func (s *LocalStore) Get() (*Account, error) {
return account, nil
}
// RemoveAccountV1Values removes ACME account V1 values
func RemoveAccountV1Values(account *Account) error {
// Check if ACME Account is in ACME V1 format
if account != nil && account.Registration != nil {
isOldRegistration, err := regexp.MatchString(acme.RegistrationURLPathV1Regexp, account.Registration.URI)
if err != nil {
return err
}
if isOldRegistration {
account.Email = ""
account.Registration = nil
account.PrivateKey = nil
}
}
return nil
}
// ConvertToNewFormat converts old acme.json format to the new one and store the result into the file (used for the backward compatibility)
func ConvertToNewFormat(fileName string) {
localStore := acme.NewLocalStore(fileName)
@@ -99,13 +80,13 @@ func ConvertToNewFormat(fileName string) {
if account != nil && len(account.Email) > 0 {
err = backupACMEFile(fileName, account)
if err != nil {
log.Errorf("Unable to create a backup for the V1 formatted ACME file: %s", err.Error())
log.Errorf("Unable to create a backup for the V1 formatted ACME file: %v", err)
return
}
err = RemoveAccountV1Values(account)
err = account.RemoveAccountV1Values()
if err != nil {
log.Errorf("Unable to remove ACME Account V1 values: %s", err.Error())
log.Errorf("Unable to remove ACME Account V1 values during format conversion: %v", err)
return
}

View File

@@ -1089,6 +1089,7 @@ var _templatesKubernetesTmpl = []byte(`[backends]
entryPoint = "{{ $frontend.Redirect.EntryPoint }}"
regex = "{{ $frontend.Redirect.Regex }}"
replacement = "{{ $frontend.Redirect.Replacement }}"
permanent = {{ $frontend.Redirect.Permanent }}
{{end}}
{{if $frontend.Errors }}

View File

@@ -140,7 +140,7 @@ func migrateACMEData(fileName string) (*acme.Account, error) {
return nil, err
}
err = acme.RemoveAccountV1Values(account)
err = account.RemoveAccountV1Values()
if err != nil {
return nil, err
}

View File

@@ -175,7 +175,7 @@ func runCmd(globalConfiguration *configuration.GlobalConfiguration, configFile s
log.Debugf("Global configuration loaded %s", string(jsonConf))
if acme.IsEnabled() {
store := acme.NewLocalStore(acme.Get().Storage)
acme.Get().Store = &store
acme.Get().Store = store
}
svr := server.NewServer(*globalConfiguration, configuration.NewProviderAggregator(globalConfiguration))
if acme.IsEnabled() && acme.Get().OnHostRule {

View File

@@ -50,24 +50,27 @@ const (
// DefaultGraceTimeout controls how long Traefik serves pending requests
// prior to shutting down.
DefaultGraceTimeout = 10 * time.Second
// DefaultAcmeCAServer is the default ACME API endpoint
DefaultAcmeCAServer = "https://acme-v02.api.letsencrypt.org/directory"
)
// GlobalConfiguration holds global configuration (with providers, etc.).
// It's populated from the traefik configuration file passed as an argument to the binary.
type GlobalConfiguration struct {
LifeCycle *LifeCycle `description:"Timeouts influencing the server life cycle" export:"true"`
GraceTimeOut flaeg.Duration `short:"g" description:"(Deprecated) Duration to give active requests a chance to finish before Traefik stops" export:"true"` // Deprecated
Debug bool `short:"d" description:"Enable debug mode" export:"true"`
CheckNewVersion bool `description:"Periodically check if a new version has been released" export:"true"`
SendAnonymousUsage bool `description:"send periodically anonymous usage statistics" export:"true"`
AccessLogsFile string `description:"(Deprecated) Access logs file" export:"true"` // Deprecated
AccessLog *types.AccessLog `description:"Access log settings" export:"true"`
TraefikLogsFile string `description:"(Deprecated) Traefik logs file. Stdout is used when omitted or empty" export:"true"` // Deprecated
TraefikLog *types.TraefikLog `description:"Traefik log settings" export:"true"`
Tracing *tracing.Tracing `description:"OpenTracing configuration" export:"true"`
LogLevel string `short:"l" description:"Log level" export:"true"`
EntryPoints EntryPoints `description:"Entrypoints definition using format: --entryPoints='Name:http Address::8000 Redirect.EntryPoint:https' --entryPoints='Name:https Address::4442 TLS:tests/traefik.crt,tests/traefik.key;prod/traefik.crt,prod/traefik.key'" export:"true"`
Cluster *types.Cluster `description:"Enable clustering" export:"true"`
LifeCycle *LifeCycle `description:"Timeouts influencing the server life cycle" export:"true"`
GraceTimeOut flaeg.Duration `short:"g" description:"(Deprecated) Duration to give active requests a chance to finish before Traefik stops" export:"true"` // Deprecated
Debug bool `short:"d" description:"Enable debug mode" export:"true"`
CheckNewVersion bool `description:"Periodically check if a new version has been released" export:"true"`
SendAnonymousUsage bool `description:"send periodically anonymous usage statistics" export:"true"`
AccessLogsFile string `description:"(Deprecated) Access logs file" export:"true"` // Deprecated
AccessLog *types.AccessLog `description:"Access log settings" export:"true"`
TraefikLogsFile string `description:"(Deprecated) Traefik logs file. Stdout is used when omitted or empty" export:"true"` // Deprecated
TraefikLog *types.TraefikLog `description:"Traefik log settings" export:"true"`
Tracing *tracing.Tracing `description:"OpenTracing configuration" export:"true"`
LogLevel string `short:"l" description:"Log level" export:"true"`
EntryPoints EntryPoints `description:"Entrypoints definition using format: --entryPoints='Name:http Address::8000 Redirect.EntryPoint:https' --entryPoints='Name:https Address::4442 TLS:tests/traefik.crt,tests/traefik.key;prod/traefik.crt,prod/traefik.key'" export:"true"`
Cluster *types.Cluster
Constraints types.Constraints `description:"Filter services by constraint, matching with service tags" export:"true"`
ACME *acme.ACME `description:"Enable ACME (Let's Encrypt): automatic SSL" export:"true"`
DefaultEntryPoints DefaultEntryPoints `description:"Entrypoints to be used by frontends that do not specify any entrypoint" export:"true"`
@@ -304,14 +307,8 @@ func (gc *GlobalConfiguration) SetEffectiveConfiguration(configFile string) {
gc.Web.Path += "/"
}
// Try to fallback to traefik config file in case the file provider is enabled
// but has no file name configured and is not in a directory mode.
if gc.File != nil && len(gc.File.Filename) == 0 && len(gc.File.Directory) == 0 {
if len(configFile) > 0 {
gc.File.Filename = configFile
} else {
log.Errorln("Error using file configuration backend, no filename defined")
}
if gc.File != nil {
gc.File.TraefikFile = configFile
}
gc.initACMEProvider()
@@ -356,7 +353,14 @@ func (gc *GlobalConfiguration) initTracing() {
func (gc *GlobalConfiguration) initACMEProvider() {
if gc.ACME != nil {
// TODO: to remove in the futurs
gc.ACME.CAServer = getSafeACMECAServer(gc.ACME.CAServer)
if gc.ACME.DNSChallenge != nil && gc.ACME.HTTPChallenge != nil {
log.Warn("Unable to use DNS challenge and HTTP challenge at the same time. Fallback to DNS challenge.")
gc.ACME.HTTPChallenge = nil
}
// TODO: to remove in the future
if len(gc.ACME.StorageFile) > 0 && len(gc.ACME.Storage) == 0 {
log.Warn("ACME.StorageFile is deprecated, use ACME.Storage instead")
gc.ACME.Storage = gc.ACME.StorageFile
@@ -391,6 +395,26 @@ func (gc *GlobalConfiguration) initACMEProvider() {
}
}
func getSafeACMECAServer(caServerSrc string) string {
if len(caServerSrc) == 0 {
return DefaultAcmeCAServer
}
if strings.HasPrefix(caServerSrc, "https://acme-v01.api.letsencrypt.org") {
caServer := strings.Replace(caServerSrc, "v01", "v02", 1)
log.Warnf("The CA server %[1]q refers to a v01 endpoint of the ACME API, please change to %[2]q. Fallback to %[2]q.", caServerSrc, caServer)
return caServer
}
if strings.HasPrefix(caServerSrc, "https://acme-staging.api.letsencrypt.org") {
caServer := strings.Replace(caServerSrc, "https://acme-staging.api.letsencrypt.org", "https://acme-staging-v02.api.letsencrypt.org", 1)
log.Warnf("The CA server %[1]q refers to a v01 endpoint of the ACME API, please change to %[2]q. Fallback to %[2]q.", caServerSrc, caServer)
return caServer
}
return caServerSrc
}
// ValidateConfiguration validate that configuration is coherent
func (gc *GlobalConfiguration) ValidateConfiguration() {
if gc.ACME != nil {

View File

@@ -65,24 +65,28 @@ func TestSetEffectiveConfigurationGraceTimeout(t *testing.T) {
func TestSetEffectiveConfigurationFileProviderFilename(t *testing.T) {
testCases := []struct {
desc string
fileProvider *file.Provider
wantFileProviderFilename string
desc string
fileProvider *file.Provider
wantFileProviderFilename string
wantFileProviderTraefikFile string
}{
{
desc: "no filename for file provider given",
fileProvider: &file.Provider{},
wantFileProviderFilename: defaultConfigFile,
desc: "no filename for file provider given",
fileProvider: &file.Provider{},
wantFileProviderFilename: "",
wantFileProviderTraefikFile: defaultConfigFile,
},
{
desc: "filename for file provider given",
fileProvider: &file.Provider{BaseProvider: provider.BaseProvider{Filename: "other.toml"}},
wantFileProviderFilename: "other.toml",
desc: "filename for file provider given",
fileProvider: &file.Provider{BaseProvider: provider.BaseProvider{Filename: "other.toml"}},
wantFileProviderFilename: "other.toml",
wantFileProviderTraefikFile: defaultConfigFile,
},
{
desc: "directory for file provider given",
fileProvider: &file.Provider{Directory: "/"},
wantFileProviderFilename: "",
desc: "directory for file provider given",
fileProvider: &file.Provider{Directory: "/"},
wantFileProviderFilename: "",
wantFileProviderTraefikFile: defaultConfigFile,
},
}
@@ -98,6 +102,7 @@ func TestSetEffectiveConfigurationFileProviderFilename(t *testing.T) {
gc.SetEffectiveConfiguration(defaultConfigFile)
assert.Equal(t, test.wantFileProviderFilename, gc.File.Filename)
assert.Equal(t, test.wantFileProviderTraefikFile, gc.File.TraefikFile)
})
}
}

View File

@@ -1,6 +1,6 @@
# ACME (Let's Encrypt) configuration
# ACME (Let's Encrypt) Configuration
See also [Let's Encrypt examples](/user-guide/examples/#lets-encrypt-support) and [Docker & Let's Encrypt user guide](/user-guide/docker-and-lets-encrypt).
See [Let's Encrypt examples](/user-guide/examples/#lets-encrypt-support) and [Docker & Let's Encrypt user guide](/user-guide/docker-and-lets-encrypt) as well.
## Configuration
@@ -63,14 +63,14 @@ entryPoint = "https"
#
# acmeLogging = true
# Enable on demand certificate generation.
# Deprecated. Enable on demand certificate generation.
#
# Optional (Deprecated)
# Optional
# Default: false
#
# onDemand = true
# Enable certificate generation on frontends Host rules.
# Enable certificate generation on frontends host rules.
#
# Optional
# Default: false
@@ -78,8 +78,8 @@ entryPoint = "https"
# onHostRule = true
# CA server to use.
# - Uncomment the line to run on the staging let's encrypt server.
# - Leave comment to go to prod.
# Uncomment the line to use Let's Encrypt's staging server,
# leave commented to go to prod.
#
# Optional
# Default: "https://acme-v02.api.letsencrypt.org/directory"
@@ -94,15 +94,13 @@ entryPoint = "https"
# sans = ["test1.local1.com", "test2.local1.com"]
# [[acme.domains]]
# main = "local2.com"
# sans = ["test1.local2.com", "test2.local2.com"]
# [[acme.domains]]
# main = "local3.com"
# [[acme.domains]]
# main = "local4.com"
# main = "*.local3.com"
# sans = ["local3.com", "test1.test1.local3.com"]
# Use a HTTP-01 acme challenge.
# Use a HTTP-01 ACME challenge.
#
# Optional but recommend
# Optional (but recommended)
#
[acme.httpChallenge]
@@ -112,21 +110,21 @@ entryPoint = "https"
#
entryPoint = "http"
# Use a DNS-01/DNS-01 acme challenge rather than HTTP-01 challenge.
# Note : Mandatory for wildcard certificates generation.
# Use a DNS-01 ACME challenge rather than HTTP-01 challenge.
# Note: mandatory for wildcard certificate generation.
#
# Optional
#
# [acme.dnsChallenge]
# Provider used.
# DNS provider used.
#
# Required
#
# provider = "digitalocean"
# By default, the provider will verify the TXT DNS challenge record before letting ACME verify.
# If delayBeforeCheck is greater than zero, avoid this & instead just wait so many seconds.
# If delayBeforeCheck is greater than zero, this check is delayed for the configured duration in seconds.
# Useful if internal networks block external DNS queries.
#
# Optional
@@ -135,98 +133,134 @@ entryPoint = "https"
# delayBeforeCheck = 0
```
!!! note
If `HTTP-01` challenge is used, `acme.httpChallenge.entryPoint` has to be defined and reachable by Let's Encrypt through the port 80.
These are Let's Encrypt limitations as described on the [community forum](https://community.letsencrypt.org/t/support-for-ports-other-than-80-and-443/3419/72).
### `caServer`
!!! note
Wildcard certificates can be generated only if `acme.dnsChallenge`
option is enable.
The CA server to use.
### Let's Encrypt downtime
Let's Encrypt functionality will be limited until Træfik is restarted.
If Let's Encrypt is not reachable, these certificates will be used :
- ACME certificates already generated before downtime
- Expired ACME certificates
- Provided certificates
!!! note
Default Træfik certificate will be used instead of ACME certificates for new (sub)domains (which need Let's Encrypt challenge).
### `storage`
This example shows the usage of Let's Encrypt's staging server:
```toml
[acme]
# ...
storage = "acme.json"
caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
# ...
```
The `storage` option sets where are stored your ACME certificates.
### `dnsChallenge`
There are two kind of `storage` :
Use the `DNS-01` challenge to generate and renew ACME certificates by provisioning a DNS record.
- a JSON file,
- a KV store entry.
```toml
[acme]
# ...
[acme.dnsChallenge]
provider = "digitalocean"
delayBeforeCheck = 0
# ...
```
!!! danger "DEPRECATED"
`storage` replaces `storageFile` which is deprecated.
#### `delayBeforeCheck`
By default, the `provider` will verify the TXT DNS challenge record before letting ACME verify.
If `delayBeforeCheck` is greater than zero, this check is delayed for the configured duration in seconds.
Useful if internal networks block external DNS queries.
!!! note
During Træfik configuration migration from a configuration file to a KV store (thanks to `storeconfig` subcommand as described [here](/user-guide/kv-config/#store-configuration-in-key-value-store)), if ACME certificates have to be migrated too, use both `storageFile` and `storage`.
A `provider` is mandatory.
- `storageFile` will contain the path to the `acme.json` file to migrate.
- `storage` will contain the key where the certificates will be stored.
#### `provider`
#### Store data in a file
Here is a list of supported `provider`s, that can automate the DNS verification, along with the required environment variables and their [wildcard & root domain support](/configuration/acme/#wildcard-domains) for each. Do not hesitate to complete it.
ACME certificates can be stored in a JSON file which with the `600` right mode.
| Provider Name | Provider Code | Environment Variables | Wildcard & Root Domain Support |
|--------------------------------------------------------|----------------|-----------------------------------------------------------------------------------------------------------------------------|--------------------------------|
| [Auroradns](https://www.pcextreme.com/aurora/dns) | `auroradns` | `AURORA_USER_ID`, `AURORA_KEY`, `AURORA_ENDPOINT` | Not tested yet |
| [Azure](https://azure.microsoft.com/services/dns/) | `azure` | `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_SUBSCRIPTION_ID`, `AZURE_TENANT_ID`, `AZURE_RESOURCE_GROUP` | Not tested yet |
| [Blue Cat](https://www.bluecatnetworks.com/) | `bluecat` | `BLUECAT_SERVER_URL`, `BLUECAT_USER_NAME`, `BLUECAT_PASSWORD`, `BLUECAT_CONFIG_NAME`, `BLUECAT_DNS_VIEW` | Not tested yet |
| [Cloudflare](https://www.cloudflare.com) | `cloudflare` | `CLOUDFLARE_EMAIL`, `CLOUDFLARE_API_KEY` - The `Global API Key` needs to be used, not the `Origin CA Key` | YES |
| [CloudXNS](https://www.cloudxns.net) | `cloudxns` | `CLOUDXNS_API_KEY`, `CLOUDXNS_SECRET_KEY` | Not tested yet |
| [DigitalOcean](https://www.digitalocean.com) | `digitalocean` | `DO_AUTH_TOKEN` | YES |
| [DNSimple](https://dnsimple.com) | `dnsimple` | `DNSIMPLE_OAUTH_TOKEN`, `DNSIMPLE_BASE_URL` | Not tested yet |
| [DNS Made Easy](https://dnsmadeeasy.com) | `dnsmadeeasy` | `DNSMADEEASY_API_KEY`, `DNSMADEEASY_API_SECRET`, `DNSMADEEASY_SANDBOX` | Not tested yet |
| [DNSPod](http://www.dnspod.net/) | `dnspod` | `DNSPOD_API_KEY` | Not tested yet |
| [Duck DNS](https://www.duckdns.org/) | `duckdns` | `DUCKDNS_TOKEN` | Not tested yet |
| [Dyn](https://dyn.com) | `dyn` | `DYN_CUSTOMER_NAME`, `DYN_USER_NAME`, `DYN_PASSWORD` | Not tested yet |
| External Program | `exec` | `EXEC_PATH` | Not tested yet |
| [Exoscale](https://www.exoscale.ch) | `exoscale` | `EXOSCALE_API_KEY`, `EXOSCALE_API_SECRET`, `EXOSCALE_ENDPOINT` | YES |
| [Fast DNS](https://www.akamai.com/) | `fastdns` | `AKAMAI_CLIENT_TOKEN`, `AKAMAI_CLIENT_SECRET`, `AKAMAI_ACCESS_TOKEN` | Not tested yet |
| [Gandi](https://www.gandi.net) | `gandi` | `GANDI_API_KEY` | Not tested yet |
| [Gandi V5](http://doc.livedns.gandi.net) | `gandiv5` | `GANDIV5_API_KEY` | YES |
| [Glesys](https://glesys.com/) | `glesys` | `GLESYS_API_USER`, `GLESYS_API_KEY`, `GLESYS_DOMAIN` | Not tested yet |
| [GoDaddy](https://godaddy.com/domains) | `godaddy` | `GODADDY_API_KEY`, `GODADDY_API_SECRET` | Not tested yet |
| [Google Cloud DNS](https://cloud.google.com/dns/docs/) | `gcloud` | `GCE_PROJECT`, `GCE_SERVICE_ACCOUNT_FILE` | YES |
| [Lightsail](https://aws.amazon.com/lightsail/) | `lightsail` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `DNS_ZONE` | Not tested yet |
| [Linode](https://www.linode.com) | `linode` | `LINODE_API_KEY` | Not tested yet |
| manual | - | none, but you need to run Træfik interactively, turn on `acmeLogging` to see instructions and press <kbd>Enter</kbd>. | YES |
| [Namecheap](https://www.namecheap.com) | `namecheap` | `NAMECHEAP_API_USER`, `NAMECHEAP_API_KEY` | Not tested yet |
| [name.com](https://www.name.com/) | `namedotcom` | `NAMECOM_USERNAME`, `NAMECOM_API_TOKEN`, `NAMECOM_SERVER` | Not tested yet |
| [Ns1](https://ns1.com/) | `ns1` | `NS1_API_KEY` | Not tested yet |
| [Open Telekom Cloud](https://cloud.telekom.de/en/) | `otc` | `OTC_DOMAIN_NAME`, `OTC_USER_NAME`, `OTC_PASSWORD`, `OTC_PROJECT_NAME`, `OTC_IDENTITY_ENDPOINT` | Not tested yet |
| [OVH](https://www.ovh.com) | `ovh` | `OVH_ENDPOINT`, `OVH_APPLICATION_KEY`, `OVH_APPLICATION_SECRET`, `OVH_CONSUMER_KEY` | YES |
| [PowerDNS](https://www.powerdns.com) | `pdns` | `PDNS_API_KEY`, `PDNS_API_URL` | Not tested yet |
| [Rackspace](https://www.rackspace.com/cloud/dns) | `rackspace` | `RACKSPACE_USER`, `RACKSPACE_API_KEY` | Not tested yet |
| [RFC2136](https://tools.ietf.org/html/rfc2136) | `rfc2136` | `RFC2136_TSIG_KEY`, `RFC2136_TSIG_SECRET`, `RFC2136_TSIG_ALGORITHM`, `RFC2136_NAMESERVER` | Not tested yet |
| [Route 53](https://aws.amazon.com/route53/) | `route53` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`, `AWS_HOSTED_ZONE_ID` or a configured user/instance IAM profile. | YES |
| [VULTR](https://www.vultr.com) | `vultr` | `VULTR_API_KEY` | Not tested yet |
There are two ways to store ACME certificates in a file from Docker:
### `domains`
You can provide SANs (alternative domains) to each main domain.
All domains must have A/AAAA records pointing to Træfik.
Each domain & SAN will lead to a certificate request.
- create a file on your host and mount it as a volume:
```toml
storage = "acme.json"
```
```bash
docker run -v "/my/host/acme.json:acme.json" traefik
```
- mount the folder containing the file as a volume
```toml
storage = "/etc/traefik/acme/acme.json"
```
```bash
docker run -v "/my/host/acme:/etc/traefik/acme" traefik
[acme]
# ...
[[acme.domains]]
main = "local1.com"
sans = ["test1.local1.com", "test2.local1.com"]
[[acme.domains]]
main = "local2.com"
[[acme.domains]]
main = "*.local3.com"
sans = ["local3.com", "test1.test1.local3.com"]
# ...
```
!!! warning
This file cannot be shared per many instances of Træfik at the same time.
If you have to use Træfik cluster mode, please use [a KV Store entry](/configuration/acme/#storage-kv-entry).
#### Store data in a KV store entry
ACME certificates can be stored in a KV Store entry.
```toml
storage = "traefik/acme/account"
```
**This kind of storage is mandatory in cluster mode.**
Because KV stores (like Consul) have limited entries size, the certificates list is compressed before to be set in a KV store entry.
Take note that Let's Encrypt applies [rate limiting](https://letsencrypt.org/docs/rate-limits).
!!! note
It's possible to store up to approximately 100 ACME certificates in Consul.
Wildcard certificates can only be verified through a `DNS-01` challenge.
#### Wildcard Domains
[ACME V2](https://community.letsencrypt.org/t/acme-v2-and-wildcard-certificate-support-is-live/55579) allows wildcard certificate support.
As described in [Let's Encrypt's post](https://community.letsencrypt.org/t/staging-endpoint-for-acme-v2/49605) wildcard certificates can only be generated through a [`DNS-01` challenge](/configuration/acme/#dnschallenge).
```toml
[acme]
# ...
[[acme.domains]]
main = "*.local1.com"
sans = ["local1.com"]
# ...
```
It is not possible to request a double wildcard certificate for a domain (for example `*.*.local.com`).
Due to ACME limitation it is not possible to define wildcards in SANs (alternative domains). Thus, the wildcard domain has to be defined as a main domain.
Most likely the root domain should receive a certificate too, so it needs to be specified as SAN and 2 `DNS-01` challenges are executed.
In this case the generated DNS TXT record for both domains is the same.
Eventhough this behaviour is [DNS RFC](https://community.letsencrypt.org/t/wildcard-issuance-two-txt-records-for-the-same-name/54528/2) compliant, it can lead to problems as all DNS providers keep DNS records cached for a certain time (TTL) and this TTL can be superior to the challenge timeout making the `DNS-01` challenge fail.
The Træfik ACME client library [LEGO](https://github.com/xenolf/lego) supports some but not all DNS providers to work around this issue.
The [`provider` table](/configuration/acme/#provider) indicates if they allow generating certificates for a wildcard domain and its root domain.
### `httpChallenge`
Use `HTTP-01` challenge to generate/renew ACME certificates.
Use the `HTTP-01` challenge to generate and renew ACME certificates by provisioning a HTTP resource under a well-known URI.
The redirection is fully compatible with the HTTP-01 challenge.
You can use redirection with HTTP-01 challenge without problem.
Redirection is fully compatible with the `HTTP-01` challenge.
```toml
[acme]
@@ -236,6 +270,10 @@ entryPoint = "https"
entryPoint = "http"
```
!!! note
If the `HTTP-01` challenge is used, `acme.httpChallenge.entryPoint` has to be defined and reachable by Let's Encrypt through port 80.
This is a Let's Encrypt limitation as described on the [community forum](https://community.letsencrypt.org/t/support-for-ports-other-than-80-and-443/3419/72).
#### `entryPoint`
Specify the entryPoint to use during the challenges.
@@ -259,73 +297,7 @@ defaultEntryPoints = ["http", "https"]
```
!!! note
`acme.httpChallenge.entryPoint` has to be reachable by Let's Encrypt through the port 80.
It's a Let's Encrypt limitation as described on the [community forum](https://community.letsencrypt.org/t/support-for-ports-other-than-80-and-443/3419/72).
### `dnsChallenge`
Use `DNS-01/DNS-01` challenge to generate/renew ACME certificates.
```toml
[acme]
# ...
[acme.dnsChallenge]
provider = "digitalocean"
delayBeforeCheck = 0
# ...
```
!!! note
ACME wildcard certificates can only be generated thanks to a `DNS-01` challenge.
#### `provider`
Select the provider that matches the DNS domain that will host the challenge TXT record, and provide environment variables to enable setting it:
| Provider Name | Provider code | Configuration |
|--------------------------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------|
| [Auroradns](https://www.pcextreme.com/aurora/dns) | `auroradns` | `AURORA_USER_ID`, `AURORA_KEY`, `AURORA_ENDPOINT` |
| [Azure](https://azure.microsoft.com/services/dns/) | `azure` | `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_SUBSCRIPTION_ID`, `AZURE_TENANT_ID`, `AZURE_RESOURCE_GROUP` |
| [Blue Cat](https://www.bluecatnetworks.com/) | `bluecat` | `BLUECAT_SERVER_URL`, `BLUECAT_USER_NAME`, `BLUECAT_PASSWORD`, `BLUECAT_CONFIG_NAME`, `BLUECAT_DNS_VIEW` |
| [Cloudflare](https://www.cloudflare.com) | `cloudflare` | `CLOUDFLARE_EMAIL`, `CLOUDFLARE_API_KEY` - The Cloudflare `Global API Key` needs to be used and not the `Origin CA Key` |
| [CloudXNS](https://www.cloudxns.net) | `cloudxns` | `CLOUDXNS_API_KEY`, `CLOUDXNS_SECRET_KEY` |
| [DigitalOcean](https://www.digitalocean.com) | `digitalocean` | `DO_AUTH_TOKEN` |
| [DNSimple](https://dnsimple.com) | `dnsimple` | `DNSIMPLE_OAUTH_TOKEN`, `DNSIMPLE_BASE_URL` |
| [DNS Made Easy](https://dnsmadeeasy.com) | `dnsmadeeasy` | `DNSMADEEASY_API_KEY`, `DNSMADEEASY_API_SECRET`, `DNSMADEEASY_SANDBOX` |
| [DNSPod](http://www.dnspod.net/) | `dnspod` | `DNSPOD_API_KEY` |
| [Duck DNS](https://www.duckdns.org/) | `duckdns` | `DUCKDNS_TOKEN` |
| [Dyn](https://dyn.com) | `dyn` | `DYN_CUSTOMER_NAME`, `DYN_USER_NAME`, `DYN_PASSWORD` |
| External Program | `exec` | `EXEC_PATH` |
| [Exoscale](https://www.exoscale.ch) | `exoscale` | `EXOSCALE_API_KEY`, `EXOSCALE_API_SECRET`, `EXOSCALE_ENDPOINT` |
| [Fast DNS](https://www.akamai.com/) | `fastdns` | `AKAMAI_CLIENT_TOKEN`, `AKAMAI_CLIENT_SECRET`, `AKAMAI_ACCESS_TOKEN` |
| [Gandi](https://www.gandi.net) | `gandi` | `GANDI_API_KEY` |
| [Gandi V5](http://doc.livedns.gandi.net) | `gandiv5` | `GANDIV5_API_KEY` |
| [Glesys](https://glesys.com/) | `glesys` | `GLESYS_API_USER`, `GLESYS_API_KEY`, `GLESYS_DOMAIN` |
| [GoDaddy](https://godaddy.com/domains) | `godaddy` | `GODADDY_API_KEY`, `GODADDY_API_SECRET` |
| [Google Cloud DNS](https://cloud.google.com/dns/docs/) | `gcloud` | `GCE_PROJECT`, `GCE_SERVICE_ACCOUNT_FILE` |
| [Lightsail](https://aws.amazon.com/lightsail/) | `lightsail` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `DNS_ZONE` |
| [Linode](https://www.linode.com) | `linode` | `LINODE_API_KEY` |
| manual | - | none, but run Træfik interactively & turn on `acmeLogging` to see instructions & press <kbd>Enter</kbd>. |
| [Namecheap](https://www.namecheap.com) | `namecheap` | `NAMECHEAP_API_USER`, `NAMECHEAP_API_KEY` |
| [name.com](https://www.name.com/) | `namedotcom` | `NAMECOM_USERNAME`, `NAMECOM_API_TOKEN`, `NAMECOM_SERVER` |
| [Ns1](https://ns1.com/) | `ns1` | `NS1_API_KEY` |
| [Open Telekom Cloud](https://cloud.telekom.de/en/) | `otc` | `OTC_DOMAIN_NAME`, `OTC_USER_NAME`, `OTC_PASSWORD`, `OTC_PROJECT_NAME`, `OTC_IDENTITY_ENDPOINT` |
| [OVH](https://www.ovh.com) | `ovh` | `OVH_ENDPOINT`, `OVH_APPLICATION_KEY`, `OVH_APPLICATION_SECRET`, `OVH_CONSUMER_KEY` |
| [PowerDNS](https://www.powerdns.com) | `pdns` | `PDNS_API_KEY`, `PDNS_API_URL` |
| [Rackspace](https://www.rackspace.com/cloud/dns) | `rackspace` | `RACKSPACE_USER`, `RACKSPACE_API_KEY` |
| [RFC2136](https://tools.ietf.org/html/rfc2136) | `rfc2136` | `RFC2136_TSIG_KEY`, `RFC2136_TSIG_SECRET`, `RFC2136_TSIG_ALGORITHM`, `RFC2136_NAMESERVER` |
| [Route 53](https://aws.amazon.com/route53/) | `route53` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`, `AWS_HOSTED_ZONE_ID` or configured user/instance IAM profile. |
| [VULTR](https://www.vultr.com) | `vultr` | `VULTR_API_KEY` |
#### `delayBeforeCheck`
By default, the `provider` will verify the TXT DNS challenge record before letting ACME verify.
If `delayBeforeCheck` is greater than zero, avoid this & instead just wait so many seconds.
Useful if internal networks block external DNS queries.
!!! note
This field has no sense if a `provider` is not defined.
`acme.httpChallenge.entryPoint` has to be reachable through port 80. It's a Let's Encrypt limitation as described on the [community forum](https://community.letsencrypt.org/t/support-for-ports-other-than-80-and-443/3419/72).
### `onDemand` (Deprecated)
@@ -339,15 +311,15 @@ onDemand = true
# ...
```
Enable on demand certificate.
Enable on demand certificate generation.
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.
This will request certificates from Let's Encrypt during the first TLS handshake for host names that do not yet have certificates.
!!! warning
TLS handshakes will be slow when requesting a host name certificate for the first time, this can lead to DoS attacks.
TLS handshakes are slow when requesting a host name certificate for the first time. This can lead to DoS attacks!
!!! warning
Take note that Let's Encrypt have [rate limiting](https://letsencrypt.org/docs/rate-limits).
Take note that Let's Encrypt applies [rate limiting](https://letsencrypt.org/docs/rate-limits).
### `onHostRule`
@@ -358,199 +330,94 @@ onHostRule = true
# ...
```
Enable certificate generation on frontends `Host` rules (for frontends wired on the `acme.entryPoint`).
Enable certificate generation on frontend `Host` rules (for frontends wired to the `acme.entryPoint`).
This will request a certificate from Let's Encrypt for each frontend with a Host rule.
For example, a rule `Host:test1.traefik.io,test2.traefik.io` will request a certificate with main domain `test1.traefik.io` and SAN `test2.traefik.io`.
For example, the rule `Host:test1.traefik.io,test2.traefik.io` will request a certificate with main domain `test1.traefik.io` and SAN `test2.traefik.io`.
!!! warning
`onHostRule` option can not be used to generate wildcard certificates.
Refer to [the wildcard generation section](/configuration/acme/#wildcard-domain) for more information.
Refer to [wildcard generation](/configuration/acme/#wildcard-domain) for further information.
### `caServer`
### `storage`
The `storage` option sets the location where your ACME certificates are saved to.
```toml
[acme]
# ...
caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
storage = "acme.json"
# ...
```
CA server to use.
The value can refer to two kinds of storage:
- Uncomment the line to run on the staging Let's Encrypt server.
- Leave comment to go to prod.
- a JSON file
- a KV store entry
### `domains`
!!! danger "DEPRECATED"
`storage` replaces `storageFile` which is deprecated.
!!! note
During migration to a KV store use both `storageFile` and `storage` to migrate ACME certificates too. See [`storeconfig` subcommand](/user-guide/kv-config/#store-configuration-in-key-value-store) for further information.
#### As a File
ACME certificates can be stored in a JSON file that needs to have file mode `600`.
In Docker you can either mount the JSON file or the folder containing it:
```bash
docker run -v "/my/host/acme.json:acme.json" traefik
```
```bash
docker run -v "/my/host/acme:/etc/traefik/acme" traefik
```
!!! warning
This file cannot be shared across multiple instances of Træfik at the same time. Please use a [KV Store entry](/configuration/acme/#as-a-key-value-store-entry) instead.
#### As a Key Value Store Entry
ACME certificates can be stored in a KV Store entry. This kind of storage is **mandatory in cluster mode**.
```toml
[acme]
# ...
[[acme.domains]]
main = "local1.com"
sans = ["test1.local1.com", "test2.local1.com"]
[[acme.domains]]
main = "local2.com"
sans = ["test1.local2.com", "test2.local2.com"]
[[acme.domains]]
main = "local3.com"
[[acme.domains]]
main = "*.local4.com"
sans = ["local4.com", "test1.test1.local4.com"]
# ...
storage = "traefik/acme/account"
```
#### Wildcard domains
Because KV stores (like Consul) have limited entry size the certificates list is compressed before it is saved as KV store entry.
Wildcard domain has to be defined as a main domain.
All domains must have A/AAAA records pointing to Træfik.
!!! note
It is possible to store up to approximately 100 ACME certificates in Consul.
Due to ACME limitation, it's not possible to define a wildcard as a SAN (alternative domains).
It's neither possible to define a wildcard on a wildcard domain (for example `*.*.local.com`).
#### ACME v2 Migration
!!! warning
Note that Let's Encrypt has [rate limiting](https://letsencrypt.org/docs/rate-limits).
During migration from ACME v1 to ACME v2, using a storage file, a backup of the original file is created in the same place as the latter (with a `.bak` extension).
Each domain & SANs will lead to a certificate request.
For example: if `acme.storage`'s value is `/etc/traefik/acme/acme.json`, the backup file will be `/etc/traefik/acme/acme.json.bak`.
#### Others domains
You can provide SANs (alternative domains) to each main domain.
All domains must have A/AAAA records pointing to Træfik.
!!! warning
Take note that Let's Encrypt have [rate limiting](https://letsencrypt.org/docs/rate-limits).
Each domain & SANs will lead to a certificate request.
!!! note
When Træfik is launched in a container, the storage file's parent directory needs to be mounted to be able to access the backup file on the host.
Otherwise the backup file will be deleted when the container is stopped. Træfik will only generate it once!
### `dnsProvider` (Deprecated)
!!! danger "DEPRECATED"
This option is deprecated, use [dnsChallenge.provider](/configuration/acme/#dnschallenge) instead.
This option is deprecated. Please use [dnsChallenge.provider](/configuration/acme/#provider) instead.
### `delayDontCheckDNS` (Deprecated)
!!! danger "DEPRECATED"
This option is deprecated, use [dnsChallenge.delayBeforeCheck](/configuration/acme/#dnschallenge) instead.
This option is deprecated. Please use [dnsChallenge.delayBeforeCheck](/configuration/acme/#dnschallenge) instead.
## Wildcard certificates
## Fallbacks
[ACME V2](https://community.letsencrypt.org/t/acme-v2-and-wildcard-certificate-support-is-live/55579) allows wildcard certificate support.
However, this feature needs a specific configuration.
If Let's Encrypt is not reachable, these certificates will be used:
### DNS-01 Challenge
As described in [Let's Encrypt post](https://community.letsencrypt.org/t/staging-endpoint-for-acme-v2/49605), wildcard certificates can only be generated through a `DNS-01` Challenge.
This challenge is linked to the Træfik option `acme.dnsChallenge`.
```toml
[acme]
# ...
[acme.dnsChallenge]
provider = "digitalocean"
delayBeforeCheck = 0
# ...
```
For more information about this option, please refer to the [dnsChallenge section](/configuration/acme/#dnschallenge).
### Wildcard domain
Wildcard domains can currently be provided only by to the `acme.domains` option.
```toml
[acme]
# ...
[[acme.domains]]
main = "*.local1.com"
sans = ["local1.com"]
[[acme.domains]]
main = "*.local2.com"
# ...
```
For more information about this option, please refer to the [domains section](/configuration/acme/#domains).
### Limitations
Let's Encrypt wildcard support have some limitations to take into account :
- Wildcard domain can not be a SAN (alternative domain),
- Wildcard domain on a wildcard domain is forbidden (for example `*.*.local.com`),
- A DNS-01 Challenge is executed for each domain (CN and SANs), DNS provider can not manage correctly this behavior as explained in the [DNS provider support section](/configuration/acme/#dns-provider-support)
### DNS provider support
All DNS providers allow creating ACME wildcard certificates.
However, many troubles can appear for wildcard domains with SANs.
If a wildcard domain is defined with it root domain as SAN, as described below, 2 DNS-01 Challenges will be executed.
```toml
[acme]
# ...
[[acme.domains]]
main = "*.local1.com"
sans = ["local1.com"]
# ...
```
When a DNS-01 Challenge is done, Let's Encrypt checks if a TXT record is created with a given name and a given value.
When a certificate is generated for a wildcard domain is defined with it root domain as SAN, the requested TXT record name for both the wildcard domain and the root domain is the same.
The [DNS RFC](https://community.letsencrypt.org/t/wildcard-issuance-two-txt-records-for-the-same-name/54528/2) allows this behavior.
But all DNS providers keep TXT records values in a cache with a TTL.
In function of the parameters given by the Træfik ACME client library ([LEGO](https://github.com/xenolf/lego)), the TXT record TTL can be superior to challenge Timeout.
In that event, the DNS-01 Challenge will not work correctly.
[LEGO](https://github.com/xenolf/lego) will involve in the way to be adapted to all of DNS providers.
Meanwhile, the table described below contains all the DNS providers supported by Træfik and indicates if they allow generating certificates for a wildcard domain and its root domain.
Do not hesitate to complete it.
| Provider Name | Provider code | Wildcard and Root Domain Support |
|--------------------------------------------------------|----------------|----------------------------------|
| [Auroradns](https://www.pcextreme.com/aurora/dns) | `auroradns` | Not tested yet |
| [Azure](https://azure.microsoft.com/services/dns/) | `azure` | Not tested yet |
| [Blue Cat](https://www.bluecatnetworks.com/) | `bluecat` | Not tested yet |
| [Cloudflare](https://www.cloudflare.com) | `cloudflare` | YES |
| [CloudXNS](https://www.cloudxns.net) | `cloudxns` | Not tested yet |
| [DigitalOcean](https://www.digitalocean.com) | `digitalocean` | YES |
| [DNSimple](https://dnsimple.com) | `dnsimple` | Not tested yet |
| [DNS Made Easy](https://dnsmadeeasy.com) | `dnsmadeeasy` | Not tested yet |
| [DNSPod](http://www.dnspod.net/) | `dnspod` | Not tested yet |
| [Duck DNS](https://www.duckdns.org/) | `duckdns` | Not tested yet |
| [Dyn](https://dyn.com) | `dyn` | Not tested yet |
| External Program | `exec` | Not tested yet |
| [Exoscale](https://www.exoscale.ch) | `exoscale` | Not tested yet |
| [Fast DNS](https://www.akamai.com/) | `fastdns` | Not tested yet |
| [Gandi](https://www.gandi.net) | `gandi` | Not tested yet |
| [Gandi V5](http://doc.livedns.gandi.net) | `gandiv5` | Not tested yet |
| [Glesys](https://glesys.com/) | `glesys` | Not tested yet |
| [GoDaddy](https://godaddy.com/domains) | `godaddy` | Not tested yet |
| [Google Cloud DNS](https://cloud.google.com/dns/docs/) | `gcloud` | YES |
| [Lightsail](https://aws.amazon.com/lightsail/) | `lightsail` | Not tested yet |
| [Linode](https://www.linode.com) | `linode` | Not tested yet |
| manual | - | YES |
| [Namecheap](https://www.namecheap.com) | `namecheap` | Not tested yet |
| [name.com](https://www.name.com/) | `namedotcom` | Not tested yet |
| [Ns1](https://ns1.com/) | `ns1` | Not tested yet |
| [Open Telekom Cloud](https://cloud.telekom.de/en/) | `otc` | Not tested yet |
| [OVH](https://www.ovh.com) | `ovh` | YES |
| [PowerDNS](https://www.powerdns.com) | `pdns` | Not tested yet |
| [Rackspace](https://www.rackspace.com/cloud/dns) | `rackspace` | Not tested yet |
| [RFC2136](https://tools.ietf.org/html/rfc2136) | `rfc2136` | Not tested yet |
| [Route 53](https://aws.amazon.com/route53/) | `route53` | YES |
| [VULTR](https://www.vultr.com) | `vultr` | Not tested yet |
## ACME V2 migration
During migration from ACME V1 to ACME V2 with a storage file, a backup is created with the content of the ACME V1 file.
To obtain the name of the backup file, Træfik concatenates the option `acme.storage` and the suffix `.bak`.
For example : if `acme.storage` value is `/etc/traefik/acme/acme.json`, the backup file will be named `/etc/traefik/acme/acme.json.bak`.
1. ACME certificates already generated before downtime
1. Expired ACME certificates
1. Provided certificates
!!! note
When Træfik is launched in a container, do not forget to create a volume of the parent folder to get the backup file on the host.
Otherwise, the backup file will be deleted when the container will be stopped and Træfik will not generate it again.
For new (sub)domains which need Let's Encrypt authentification, the default Træfik certificate will be used until Træfik is restarted.

View File

@@ -4,6 +4,9 @@
```toml
# API definition
# Warning: Enabling API will expose Træfik's configuration.
# It is not recommended in production,
# unless secured by authentication and authorizations
[api]
# Name of the related entry point
#
@@ -12,7 +15,7 @@
#
entryPoint = "traefik"
# Enabled Dashboard
# Enable Dashboard
#
# Optional
# Default: true
@@ -21,7 +24,7 @@
# Enable debug mode.
# This will install HTTP handlers to expose Go expvars under /debug/vars and
# pprof profiling data under /debug/pprof.
# pprof profiling data under /debug/pprof/.
# Additionally, the log level will be set to DEBUG.
#
# Optional
@@ -38,6 +41,22 @@ For more customization, see [entry points](/configuration/entrypoints/) document
![Web UI Health](/img/traefik-health.png)
## Security
Enabling the API will expose all configuration elements,
including sensitive data.
It is not recommended in production,
unless secured by authentication and authorizations.
A good sane default (but not exhaustive) set of recommendations
would be to apply the following protection mechanism:
* _At application level:_ enabling HTTP [Basic Authentication](#authentication)
* _At transport level:_ NOT exposing publicly the API's port,
keeping it restricted over internal networks
(restricted networks as in https://en.wikipedia.org/wiki/Principle_of_least_privilege).
## API
| Path | Method | Description |

View File

@@ -1,13 +1,13 @@
# BoltDB Backend
# BoltDB Provider
Træfik can be configured to use BoltDB as a backend configuration.
Træfik can be configured to use BoltDB as a provider.
```toml
################################################################
# BoltDB configuration backend
# BoltDB Provider
################################################################
# Enable BoltDB configuration backend.
# Enable BoltDB Provider.
[boltdb]
# BoltDB file.
@@ -56,4 +56,4 @@ filename = "boltdb.tmpl"
# insecureSkipVerify = true
```
To enable constraints see [backend-specific constraints section](/configuration/commons/#backend-specific).
To enable constraints see [provider-specific constraints section](/configuration/commons/#provider-specific).

View File

@@ -1,13 +1,13 @@
# Consul Key-Value Backend
# Consul Key-Value Provider
Træfik can be configured to use Consul as a backend configuration.
Træfik can be configured to use Consul as a provider.
```toml
################################################################
# Consul KV configuration backend
# Consul KV Provider
################################################################
# Enable Consul KV configuration backend.
# Enable Consul KV Provider.
[consul]
# Consul server endpoint.
@@ -56,6 +56,6 @@ prefix = "traefik"
# insecureSkipVerify = true
```
To enable constraints see [backend-specific constraints section](/configuration/commons/#backend-specific).
To enable constraints see [provider-specific constraints section](/configuration/commons/#provider-specific).
Please refer to the [Key Value storage structure](/user-guide/kv-config/#key-value-storage-structure) section to get documentation on Traefik KV structure.

View File

@@ -1,13 +1,13 @@
# Consul Catalog backend
# Consul Catalog Provider
Træfik can be configured to use service discovery catalog of Consul as a backend configuration.
Træfik can be configured to use service discovery catalog of Consul as a provider.
```toml
################################################################
# Consul Catalog configuration backend
# Consul Catalog Provider
################################################################
# Enable Consul Catalog configuration backend.
# Enable Consul Catalog Provider.
[consulCatalog]
# Consul server endpoint.
@@ -76,9 +76,9 @@ prefix = "traefik"
# templateVersion = 2
```
This backend will create routes matching on hostname based on the service name used in Consul.
This provider will create routes matching on hostname based on the service name used in Consul.
To enable constraints see [backend-specific constraints section](/configuration/commons/#backend-specific).
To enable constraints see [provider-specific constraints section](/configuration/commons/#provider-specific).
## Tags

View File

@@ -1,16 +1,16 @@
# Docker Backend
# Docker Provider
Træfik can be configured to use Docker as a backend configuration.
Træfik can be configured to use Docker as a provider.
## Docker
```toml
################################################################
# Docker configuration backend
# Docker Provider
################################################################
# Enable Docker configuration backend.
# Enable Docker Provider.
[docker]
# Docker server endpoint. Can be a tcp or a unix socket endpoint.
@@ -82,17 +82,17 @@ swarmMode = false
# insecureSkipVerify = true
```
To enable constraints see [backend-specific constraints section](/configuration/commons/#backend-specific).
To enable constraints see [provider-specific constraints section](/configuration/commons/#provider-specific).
## Docker Swarm Mode
```toml
################################################################
# Docker Swarm Mode configuration backend
# Docker Swarm Mode Provider
################################################################
# Enable Docker configuration backend.
# Enable Docker Provider.
[docker]
# Docker server endpoint.
@@ -159,7 +159,7 @@ exposedByDefault = false
# insecureSkipVerify = true
```
To enable constraints see [backend-specific constraints section](/configuration/commons/#backend-specific).
To enable constraints see [provider-specific constraints section](/configuration/commons/#provider-specific).
## Labels: overriding default behavior
@@ -218,7 +218,7 @@ Labels can be used on containers to override default behavior.
| `traefik.backend.loadbalancer.swarm=true` | Use Swarm's inbuilt load balancer (only relevant under Swarm Mode). |
| `traefik.backend.maxconn.amount=10` | Set a maximum number of connections to the backend.<br>Must be used in conjunction with the below label to take effect. |
| `traefik.backend.maxconn.extractorfunc=client.ip` | Set the function to be used against the request to determine what to limit maximum connections to the backend by.<br>Must be used in conjunction with the above label to take effect. |
| `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash` |
| `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash` [2] |
| `traefik.frontend.entryPoints=http,https` | Assign this frontend to entry points `http` and `https`.<br>Overrides `defaultEntryPoints` |
| `traefik.frontend.errors.<name>.backend=NAME` | See [custom error pages](/configuration/commons/#custom-error-pages) section. |
| `traefik.frontend.errors.<name>.query=PATH` | See [custom error pages](/configuration/commons/#custom-error-pages) section. |
@@ -243,6 +243,10 @@ If a container is linked to several networks, be sure to set the proper network
For instance when deploying docker `stack` from compose files, the compose defined networks will be prefixed with the `stack` name.
Or if your service references external network use it's name instead.
[2] `traefik.frontend.auth.basic=EXPR`:
To create `user:password` pair, it's possible to use this command `echo $(htpasswd -nb user password) | sed -e s/\\$/\\$\\$/g`.
The result will be `user:$$apr1$$9Cv/OMGj$$ZomWQzuQbL.3TRCS81A1g/`, note additional symbol `$` makes escaping.
#### Custom Headers
| Label | Description |

View File

@@ -1,15 +1,15 @@
# DynamoDB Backend
# DynamoDB Provider
Træfik can be configured to use Amazon DynamoDB as a backend configuration.
Træfik can be configured to use Amazon DynamoDB as a provider.
## Configuration
```toml
################################################################
# DynamoDB configuration backend
# DynamoDB Provider
################################################################
# Enable DynamoDB configuration backend.
# Enable DynamoDB Provider.
[dynamodb]
# Region to use when connecting to AWS.
@@ -68,4 +68,3 @@ Items in the `dynamodb` table must have three attributes:
See `types/types.go` for details.
The presence or absence of this attribute determines its type.
So an item should never have both a `frontend` and a `backend` attribute.

View File

@@ -1,15 +1,15 @@
# ECS Backend
# ECS Provider
Træfik can be configured to use Amazon ECS as a backend configuration.
Træfik can be configured to use Amazon ECS as a provider.
## Configuration
```toml
################################################################
# ECS configuration backend
# ECS Provider
################################################################
# Enable ECS configuration backend.
# Enable ECS Provider.
[ecs]
# ECS Cluster Name.

View File

@@ -1,13 +1,13 @@
# Etcd Backend
# Etcd Provider
Træfik can be configured to use Etcd as a backend configuration.
Træfik can be configured to use Etcd as a provider.
```toml
################################################################
# Etcd configuration backend
# Etcd Provider
################################################################
# Enable Etcd configuration backend.
# Enable Etcd Provider.
[etcd]
# Etcd server endpoint.
@@ -66,7 +66,7 @@ useAPIV3 = true
# insecureSkipVerify = true
```
To enable constraints see [backend-specific constraints section](/configuration/commons/#backend-specific).
To enable constraints see [provider-specific constraints section](/configuration/commons/#provider-specific).
Please refer to the [Key Value storage structure](/user-guide/kv-config/#key-value-storage-structure) section to get documentation on Traefik KV structure.

View File

@@ -1,13 +1,13 @@
# Eureka Backend
# Eureka Provider
Træfik can be configured to use Eureka as a backend configuration.
Træfik can be configured to use Eureka as a provider.
```toml
################################################################
# Eureka configuration backend
# Eureka Provider
################################################################
# Enable Eureka configuration backend.
# Enable Eureka Provider.
[eureka]
# Eureka server endpoint.

View File

@@ -1,4 +1,4 @@
# File Backends
# File Provider
Træfik can be configured with a file.

View File

@@ -1,6 +1,6 @@
# Kubernetes Ingress Backend
# Kubernetes Ingress Provider
Træfik can be configured to use Kubernetes Ingress as a backend configuration.
Træfik can be configured to use Kubernetes Ingress as a provider.
See also [Kubernetes user guide](/user-guide/kubernetes).
@@ -8,10 +8,10 @@ See also [Kubernetes user guide](/user-guide/kubernetes).
```toml
################################################################
# Kubernetes Ingress configuration backend
# Kubernetes Ingress Provider
################################################################
# Enable Kubernetes Ingress configuration backend.
# Enable Kubernetes Ingress Provider.
[kubernetes]
# Kubernetes server endpoint.
@@ -138,6 +138,7 @@ The following general annotations are applicable on the Ingress object:
| `traefik.ingress.kubernetes.io/rewrite-target: /users` | Replaces each matched Ingress path with the specified one, and adds the old path to the `X-Replaced-Path` header. |
| `traefik.ingress.kubernetes.io/rule-type: PathPrefixStrip` | Override the default frontend rule type. Default: `PathPrefix`. |
| `traefik.ingress.kubernetes.io/whitelist-source-range: "1.2.3.0/24, fe80::/16"` | A comma-separated list of IP ranges permitted for access. all source IPs are permitted if the list is empty or a single range is ill-formatted. Please note, you may have to set `service.spec.externalTrafficPolicy` to the value `Local` to preserve the source IP of the request for filtering. Please see [this link](https://kubernetes.io/docs/tutorials/services/source-ip/) for more information.|
| `ingress.kubernetes.io/whitelist-x-forwarded-for: "true"` | Use `X-Forwarded-For` header as valid source of IP for the white list. |
| `traefik.ingress.kubernetes.io/app-root: "/index.html"` | Redirects all requests for `/` to the defined path. (4) |
<1> `traefik.ingress.kubernetes.io/error-pages` example:

View File

@@ -1,6 +1,6 @@
# Marathon Backend
# Marathon Provider
Træfik can be configured to use Marathon as a backend configuration.
Træfik can be configured to use Marathon as a provider.
See also [Marathon user guide](/user-guide/marathon).
@@ -9,10 +9,10 @@ See also [Marathon user guide](/user-guide/marathon).
```toml
################################################################
# Mesos/Marathon configuration backend
# Mesos/Marathon Provider
################################################################
# Enable Marathon configuration backend.
# Enable Marathon Provider.
[marathon]
# Marathon server endpoint.
@@ -157,7 +157,7 @@ domain = "marathon.localhost"
# respectReadinessChecks = true
```
To enable constraints see [backend-specific constraints section](/configuration/commons/#backend-specific).
To enable constraints see [provider-specific constraints section](/configuration/commons/#provider-specific).
## Labels: overriding default behavior

View File

@@ -1,13 +1,13 @@
# Mesos Generic Backend
# Mesos Generic Provider
Træfik can be configured to use Mesos as a backend configuration.
Træfik can be configured to use Mesos as a provider.
```toml
################################################################
# Mesos configuration backend
# Mesos Provider
################################################################
# Enable Mesos configuration backend.
# Enable Mesos Provider.
[mesos]
# Mesos server endpoint.

View File

@@ -1,15 +1,15 @@
# Rancher Backend
# Rancher Provider
Træfik can be configured to use Rancher as a backend configuration.
Træfik can be configured to use Rancher as a provider.
## Global Configuration
```toml
################################################################
# Rancher configuration backend
# Rancher Provider
################################################################
# Enable Rancher configuration backend.
# Enable Rancher Provider.
[rancher]
# Default domain used.
@@ -64,13 +64,13 @@ enableServiceHealthFilter = true
# templateVersion = 2
```
To enable constraints see [backend-specific constraints section](/configuration/commons/#backend-specific).
To enable constraints see [provider-specific constraints section](/configuration/commons/#provider-specific).
## Rancher Metadata Service
```toml
# Enable Rancher metadata service configuration backend instead of the API
# configuration backend.
# Enable Rancher metadata service provider instead of the API
# provider.
#
# Optional
# Default: false
@@ -97,7 +97,7 @@ prefix = "/2016-07-29"
## Rancher API
```toml
# Enable Rancher API configuration backend.
# Enable Rancher API provider.
#
# Optional
# Default: true

View File

@@ -1,4 +1,4 @@
# Rest Backend
# Rest Provider
Træfik can be configured:
@@ -7,7 +7,7 @@ Træfik can be configured:
## Configuration
```toml
# Enable rest backend.
# Enable REST Provider.
[rest]
# Name of the related entry point
#

View File

@@ -1,6 +1,6 @@
# Azure Service Fabric Backend
# Azure Service Fabric Provider
Træfik can be configured to use Azure Service Fabric as a backend configuration.
Træfik can be configured to use Azure Service Fabric as a provider.
See [this repository for an example deployment package and further documentation.](https://aka.ms/traefikonsf)
@@ -8,10 +8,10 @@ See [this repository for an example deployment package and further documentation
```toml
################################################################
# Azure Service Fabric provider
# Azure Service Fabric Provider
################################################################
# Enable Azure Service Fabric configuration backend
# Enable Azure Service Fabric Provider
[serviceFabric]
# Azure Service Fabric Management Endpoint
@@ -61,7 +61,7 @@ Here is an example of an extension setting Træfik labels:
<Extension Name="Traefik">
<Labels xmlns="http://schemas.microsoft.com/2015/03/fabact-no-schema">
<Label Key="traefik.frontend.rule.example2">PathPrefixStrip: /a/path/to/strip</Label>
<Label Key="traefik.enable">true</Label>
<Label Key="traefik.enable">true</Label>
<Label Key="traefik.frontend.passHostHeader">true</Label>
</Labels>
</Extension>
@@ -98,8 +98,9 @@ Labels, set through extensions or the property manager, can be used on services
|------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `traefik.enable=false` | Disable this container in Træfik |
| `traefik.backend.circuitbreaker.expression=EXPR` | Create a [circuit breaker](/basics/#backends) to be used against the backend |
| `traefik.backend.group.name` | Group all services with the same name into a single backend in Træfik |
| `traefik.backend.group.weight` | Set the weighting of the current services nodes in the backend group |
| `traefik.servicefabric.groupname` | Group all services with the same name into a single backend in Træfik |
| `traefik.servicefabric.groupweight` | Set the weighting of the current services nodes in the backend group |
| `traefik.servicefabric.enablelabeloverrides` | Toggle whether labels can be overridden using the Service Fabric Property Manager API |
| `traefik.backend.healthcheck.path=/health` | Enable health check for the backend, hitting the container at `path`. |
| `traefik.backend.healthcheck.port=8080` | Allow to use a different port for the health check. |
| `traefik.backend.healthcheck.interval=1s` | Define the health check interval. |

View File

@@ -1,4 +1,4 @@
# Web Backend
# Web Provider
!!! danger "DEPRECATED"
The web provider is deprecated, please use the [api](/configuration/api.md), the [ping](/configuration/ping.md), the [metrics](/configuration/metrics) and the [rest](/configuration/backends/rest.md) provider.
@@ -12,7 +12,7 @@ Træfik can be configured:
## Configuration
```toml
# Enable web backend.
# Enable Web Provider.
[web]
# Web administration port.

View File

@@ -1,13 +1,13 @@
# Zookeeper Backend
# Zookeeper Provider
Træfik can be configured to use Zookeeper as a backend configuration.
Træfik can be configured to use Zookeeper as a provider.
```toml
################################################################
# Zookeeper configuration backend
# Zookeeper Provider
################################################################
# Enable Zookeeperconfiguration backend.
# Enable Zookeeper Provider.
[zookeeper]
# Zookeeper server endpoint.
@@ -56,6 +56,6 @@ prefix = "traefik"
# insecureSkipVerify = true
```
To enable constraints see [backend-specific constraints section](/configuration/commons/#backend-specific).
To enable constraints see [provider-specific constraints section](/configuration/commons/#provider-specific).
Please refer to the [Key Value storage structure](/user-guide/kv-config/#key-value-storage-structure) section to get documentation on Traefik KV structure.

View File

@@ -18,7 +18,7 @@
# Enable debug mode.
# This will install HTTP handlers to expose Go expvars under /debug/vars and
# pprof profiling data under /debug/pprof.
# pprof profiling data under /debug/pprof/.
# The log level will be set to DEBUG unless `logLevel` is specified.
#
# Optional
@@ -33,7 +33,7 @@
#
# checkNewVersion = false
# Backends throttle duration.
# Providers throttle duration.
#
# Optional
# Default: "2s"
@@ -85,7 +85,7 @@ Can be provided in a format supported by [time.ParseDuration](https://golang.org
If no units are provided, the value is parsed assuming seconds.
**Note:** in this time frame no new requests are accepted.
- `providersThrottleDuration`: Backends throttle duration: minimum duration in seconds between 2 events from providers before applying a new configuration.
- `providersThrottleDuration`: Providers throttle duration: minimum duration in seconds between 2 events from providers before applying a new configuration.
It avoids unnecessary reloads if multiples events are sent in a short amount of time.
Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw values (digits).
If no units are provided, the value is parsed assuming seconds.
@@ -108,7 +108,7 @@ Each frontend can specify its own entrypoints.
In a micro-service architecture, with a central service discovery, setting constraints limits Træfik scope to a smaller number of routes.
Træfik filters services according to service attributes/tags set in your configuration backends.
Træfik filters services according to service attributes/tags set in your providers.
Supported filters:
@@ -136,9 +136,9 @@ constraints = ["tag==us-*"]
constraints = ["tag!=us-*", "tag!=asia-*"]
```
### Backend-specific
### provider-specific
Supported backends:
Supported Providers:
- Docker
- Consul K/V
@@ -151,12 +151,12 @@ Supported backends:
- Kubernetes (using a provider-specific mechanism based on label selectors)
```toml
# Backend-specific constraint
# Provider-specific constraint
[consulCatalog]
# ...
constraints = ["tag==api"]
# Backend-specific constraint
# Provider-specific constraint
[marathon]
# ...
constraints = ["tag==api", "tag!=v*-beta"]
@@ -421,12 +421,12 @@ idleTimeout = "360s"
!!! warning
For advanced users only.
Supported by all backends except: File backend, Web backend and DynamoDB backend.
Supported by all providers except: File Provider, Web Provider and DynamoDB Provider.
```toml
[backend_name]
[provider_name]
# Override default configuration template. For advanced users :)
# Override default provider configuration template. For advanced users :)
#
# Optional
# Default: ""

View File

@@ -80,7 +80,8 @@
# ...
```
### InfluxDB
## InfluxDB
```toml
[metrics]
@@ -105,22 +106,3 @@
# ...
```
## Statistics
```toml
# Metrics definition
[metrics]
# ...
# Enable more detailed statistics.
[metrics.statistics]
# Number of recent errors logged.
#
# Default: 10
#
recentErrors = 10
# ...
```

View File

@@ -54,7 +54,7 @@ Træfik supports two backends: Jaeger and Zipkin.
```
!!! warning
Træfik is only able to send data over compact thrift protocol to the [Jaeger agent](https://www.jaegertracing.io/docs/deployment/#agent).
Træfik is only able to send data over compact thrift protocol to the [Jaeger agent](https://www.jaegertracing.io/docs/deployment/#agent).
## Zipkin

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 274 KiB

View File

@@ -42,12 +42,12 @@ _(But if you'd rather configure some of your routes manually, Træfik supports t
- Websocket, HTTP/2, GRPC ready
- Provides metrics (Rest, Prometheus, Datadog, Statsd, InfluxDB)
- Keeps access logs (JSON, CLF)
- [Fast](/benchmarks) ... which is nice
- Fast
- Exposes a Rest API
- Packaged as a single binary file (made with :heart: with go) and available as a [tiny](https://microbadger.com/images/traefik) [official](https://hub.docker.com/r/_/traefik/) docker image
## Supported backends
## Supported Providers
- [Docker](/configuration/backends/docker/) / [Swarm mode](/configuration/backends/docker/#docker-swarm-mode)
- [Kubernetes](/configuration/backends/kubernetes/)
@@ -86,6 +86,10 @@ services:
- /var/run/docker.sock:/var/run/docker.sock # So that Traefik can listen to the Docker events
```
!!! warning
Enabling the Web UI with the `--api` flag might exposes configuration elements. You can read more about this on the [API/Dashboard's Security section](/configuration/api#security).
**That's it. Now you can launch Træfik!**
Start your `reverse-proxy` with the following command:
@@ -166,7 +170,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](/) 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.
Whatever your infrastructure is, there is probably [an available Træfik provider](/#supported-providers) 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](/user-guide/examples/#lets-encrypt-support) using the dedicated [user guide](/user-guide/docker-and-lets-encrypt/).

View File

@@ -9,9 +9,9 @@ If you want to use Let's Encrypt with Træfik, sharing configuration or TLS cert
Ok, could we mount a shared volume used by all my instances? Yes, you can, but it will not work.
When you use Let's Encrypt, you need to store certificates, but not only.
When Træfik generates a new certificate, it configures a challenge and once Let's Encrypt will verify the ownership of the domain, it will ping back the challenge.
If the challenge is not knowing by other Træfik instances, the validation will fail.
If the challenge is not known by other Træfik instances, the validation will fail.
For more information about challenge: [Automatic Certificate Management Environment (ACME)](https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#http-challenge)
For more information about the challenge: [Automatic Certificate Management Environment (ACME)](https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#http-challenge)
## Prerequisites

View File

@@ -113,7 +113,7 @@ This is the minimum configuration required to do the following:
- Log `ERROR`-level messages (or more severe) to the console, but silence `DEBUG`-level messages
- Check for new versions of Træfik periodically
- Create two entry points, namely an `HTTP` endpoint on port `80`, and an `HTTPS` endpoint on port `443` where all incoming traffic on port `80` will immediately get redirected to `HTTPS`.
- Enable the Docker configuration backend and listen for container events on the Docker unix socket we've mounted earlier. However, **new containers will not be exposed by Træfik by default, we'll get into this in a bit!**
- Enable the Docker provider and listen for container events on the Docker unix socket we've mounted earlier. However, **new containers will not be exposed by Træfik by default, we'll get into this in a bit!**
- Enable automatic request and configuration of SSL certificates using Let's Encrypt.
These certificates will be stored in the `acme.json` file, which you can back-up yourself and store off-premises.
@@ -123,7 +123,7 @@ Alright, let's boot the container. From the `/opt/traefik` directory, run `docke
Now that we've fully configured and started Træfik, it's time to get our applications running!
Let's take a simple example of a micro-service project consisting of various services, where some will be exposed to the outside world and some will not.
Let's take a simple example of a micro-service project consisting of various services, where some will be exposed to the outside world and some will not.
The `docker-compose.yml` of our project looks like this:
@@ -145,12 +145,11 @@ services:
expose:
- "9000"
labels:
- "traefik.backend=my-awesome-app-app"
- "traefik.docker.network=web"
- "traefik.frontend.rule=Host:app.my-awesome-app.org"
- "traefik.enable=true"
- "traefik.port=9000"
- "traefik.default.protocol=http"
- "traefik.basic.frontend.rule=Host:app.my-awesome-app.org"
- "traefik.basic.port=9000"
- "traefik.basic.protocol=http"
- "traefik.admin.frontend.rule=Host:admin-app.my-awesome-app.org"
- "traefik.admin.protocol=https"
- "traefik.admin.port=9443"
@@ -204,12 +203,11 @@ Thanks to Docker labels, we can tell Træfik how to create its internal routing
Let's take a look at the labels themselves for the `app` service, which is a HTTP webservice listing on port 9000:
```yaml
- "traefik.backend=my-awesome-app-app"
- "traefik.docker.network=web"
- "traefik.frontend.rule=Host:app.my-awesome-app.org"
- "traefik.enable=true"
- "traefik.port=9000"
- "traefik.default.protocol=http"
- "traefik.basic.frontend.rule=Host:app.my-awesome-app.org"
- "traefik.basic.port=9000"
- "traefik.basic.protocol=http"
- "traefik.admin.frontend.rule=Host:admin-app.my-awesome-app.org"
- "traefik.admin.protocol=https"
- "traefik.admin.port=9443"
@@ -221,11 +219,11 @@ We use both `container labels` and `service labels`.
First, we specify the `backend` name which corresponds to the actual service we're routing **to**.
We also tell Træfik to use the `web` network to route HTTP traffic to this container.
We also tell Træfik to use the `web` network to route HTTP traffic to this container.
With the `traefik.enable` label, we tell Træfik to include this container in its internal configuration.
With the `frontend.rule` label, we tell Træfik that we want to route to this container if the incoming HTTP request contains the `Host` `app.my-awesome-app.org`.
Essentially, this is the actual rule used for Layer-7 load balancing.
Essentially, this is the actual rule used for Layer-7 load balancing.
Finally but not unimportantly, we tell Træfik to route **to** port `9000`, since that is the actual TCP/IP port the container actually listens on.
@@ -236,11 +234,11 @@ Finally but not unimportantly, we tell Træfik to route **to** port `9000`, sinc
When both `container labels` and `service labels` are defined, `container labels` are just used as default values for missing `service labels` but no frontend/backend are going to be defined only with these labels.
Obviously, labels `traefik.frontend.rule` and `traefik.port` described above, will only be used to complete information set in `service labels` during the container frontends/bakends creation.
In the example, two service names are defined : `default` and `admin`.
In the example, two service names are defined : `basic` and `admin`.
They allow creating two frontends and two backends.
- `default` has only one `service label` : `traefik.default.protocol`.
Træfik will use values set in `traefik.frontend.rule` and `traefik.port` to create the `default` frontend and backend.
- `basic` has only one `service label` : `traefik.basic.protocol`.
Træfik will use values set in `traefik.frontend.rule` and `traefik.port` to create the `basic` frontend and backend.
The frontend listens to incoming HTTP requests which contain the `Host` `app.my-awesome-app.org` and redirect them in `HTTP` to the port `9000` of the backend.
- `admin` has all the `services labels` needed to create the `admin` frontend and backend (`traefik.admin.frontend.rule`, `traefik.admin.protocol`, `traefik.admin.port`).
Træfik will create a frontend to listen to incoming HTTP requests which contain the `Host` `admin-app.my-awesome-app.org` and redirect them in `HTTPS` to the port `9443` of the backend.

View File

@@ -68,7 +68,7 @@ defaultEntryPoints = ["http", "https"]
[acme]
email = "test@traefik.io"
storage = "acme.json"
caServer = "http://172.18.0.1:4000/directory"
caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
entryPoint = "https"
[acme.httpChallenge]
entryPoint = "http"
@@ -103,7 +103,7 @@ Træfik generates these certificates when it starts and it needs to be restart i
email = "test@traefik.io"
storage = "acme.json"
onHostRule = true
caServer = "http://172.18.0.1:4000/directory"
caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
entryPoint = "https"
[acme.httpChallenge]
entryPoint = "http"
@@ -140,7 +140,7 @@ If a backend is added with a `onHost` rule, Træfik will automatically generate
email = "test@traefik.io"
storage = "acme.json"
onDemand = true
caServer = "http://172.18.0.1:4000/directory"
caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
entryPoint = "https"
[acme.httpChallenge]
entryPoint = "http"
@@ -167,7 +167,7 @@ This configuration allows generating a Let's Encrypt certificate (thanks to `HTT
[acme]
email = "test@traefik.io"
storage = "acme.json"
caServer = "http://172.18.0.1:4000/directory"
caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
entryPoint = "https"
[acme.dnsChallenge]
provider = "digitalocean" # DNS Provider name (cloudflare, OVH, gandi...)

View File

@@ -81,9 +81,11 @@ For namespaced restrictions, one RoleBinding is required per watched namespace a
It is possible to use Træfik with a [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) or a [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/) object,
whereas both options have their own pros and cons:
- The scalability is much better when using a Deployment, because you will have a Single-Pod-per-Node model when using the DaemonSet.
- It is possible to exclusively run a Service on a dedicated set of machines using taints and tolerations with a DaemonSet.
- On the other hand the DaemonSet allows you to access any Node directly on Port 80 and 443, where you have to setup a [Service](https://kubernetes.io/docs/concepts/services-networking/service/) object with a Deployment.
- The scalability can be much better when using a Deployment, because you will have a Single-Pod-per-Node model when using a DaemonSet, whereas you may need less replicas based on your environment when using a Deployment.
- DaemonSets automatically scale to new nodes, when the nodes join the cluster, whereas Deployment pods are only scheduled on new nodes if required.
- DaemonSets ensure that only one replica of pods run on any single node. Deployments require affinity settings if you want to ensure that two pods don't end up on the same node.
- DaemonSets can be run with the `NET_BIND_SERVICE` capability, which will allow it to bind to port 80/443/etc on each host. This will allow bypassing the kube-proxy, and reduce traffic hops. Note that this is against the Kubernetes Best Practices [Guidelines](https://kubernetes.io/docs/concepts/configuration/overview/#services), and raises the potential for scheduling/scaling issues. Despite potential issues, this remains the choice for most ingress controllers.
- If you are unsure which to choose, start with the Daemonset.
The Deployment objects looks like this:
@@ -118,6 +120,11 @@ spec:
containers:
- image: traefik
name: traefik-ingress-lb
ports:
- name: http
containerPort: 80
- name: admin
containerPort: 8080
args:
- --api
- --kubernetes
@@ -172,7 +179,6 @@ spec:
spec:
serviceAccountName: traefik-ingress-controller
terminationGracePeriodSeconds: 60
hostNetwork: true
containers:
- image: traefik
name: traefik-ingress-lb
@@ -208,11 +214,13 @@ spec:
- protocol: TCP
port: 8080
name: admin
type: NodePort
```
[examples/k8s/traefik-ds.yaml](https://github.com/containous/traefik/tree/master/examples/k8s/traefik-ds.yaml)
!!! note
This will create a Daemonset that uses privileged ports 80/8080 on the host. This may not work on all providers, but illustrates the static (non-NodePort) hostPort binding. The `traefik-ingress-service` can still be used inside the cluster to access the DaemonSet pods.
To deploy Træfik to your cluster start by submitting one of the YAML files to the cluster with `kubectl`:
```shell
@@ -293,7 +301,21 @@ Install the Træfik chart by:
```shell
helm install stable/traefik
```
Install the Træfik chart using a values.yaml file.
```shell
helm install --values values.yaml stable/traefik
```
```yaml
dashboard:
enabled: true
domain: traefik-ui.minikube
kubernetes:
namespaces:
- default
- kube-system
```
For more information, check out [the documentation](https://github.com/kubernetes/charts/tree/master/stable/traefik).
## Submitting an Ingress to the Cluster

View File

@@ -76,7 +76,7 @@ defaultEntryPoints = ["http", "https"]
address = ":80"
[entryPoints.https]
address = ":443"
[entryPoints.https.tls]
[[entryPoints.https.tls.certificates]]
certFile = "integration/fixtures/https/snitest.com.cert"
@@ -85,9 +85,9 @@ defaultEntryPoints = ["http", "https"]
certFile = """-----BEGIN CERTIFICATE-----
<cert file content>
-----END CERTIFICATE-----"""
keyFile = """-----BEGIN CERTIFICATE-----
keyFile = """-----BEGIN PRIVATE KEY-----
<key file content>
-----END CERTIFICATE-----"""
-----END PRIVATE KEY-----"""
[entryPoints.other-https]
address = ":4443"
[entryPoints.other-https.tls]
@@ -164,7 +164,7 @@ If a Consul ACL is used to restrict Træfik read/write access, one of the follow
key "traefik" {
policy = "write"
},
session "" {
policy = "write"
}
@@ -266,6 +266,10 @@ Here is the toml configuration we would like to store in the store :
backend = "backend1"
passHostHeader = true
priority = 10
basicAuth = [
"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
]
entrypoints = ["https"] # overrides defaultEntryPoints
[frontends.frontend2.routes.test_1]
rule = "Host:{subdomain:[a-z]+}.localhost"
@@ -325,13 +329,15 @@ And there, the same dynamic configuration in a KV Store (using `prefix = "traefi
- frontend 2
| Key | Value |
|----------------------------------------------------|--------------------|
| `/traefik/frontends/frontend2/backend` | `backend1` |
| `/traefik/frontends/frontend2/passhostheader` | `true` |
| `/traefik/frontends/frontend2/priority` | `10` |
| `/traefik/frontends/frontend2/entrypoints` | `http,https` |
| `/traefik/frontends/frontend2/routes/test_2/rule` | `PathPrefix:/test` |
| Key | Value |
|----------------------------------------------------|-----------------------------------------------|
| `/traefik/frontends/frontend2/backend` | `backend1` |
| `/traefik/frontends/frontend2/passhostheader` | `true` |
| `/traefik/frontends/frontend2/priority` | `10` |
| `/traefik/frontends/frontend2/basicauth/0` | `test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/` |
| `/traefik/frontends/frontend2/basicauth/1` | `test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0` |
| `/traefik/frontends/frontend2/entrypoints` | `http,https` |
| `/traefik/frontends/frontend2/routes/test_2/rule` | `PathPrefix:/test` |
- certificate 1
@@ -349,7 +355,7 @@ And there, the same dynamic configuration in a KV Store (using `prefix = "traefi
|---------------------------------------|-----------------------|
| `/traefik/tls/2/entrypoints` | `https,other-https` |
| `/traefik/tls/2/certificate/certfile` | `<cert file content>` |
| `/traefik/tls/2/certificate/certfile` | `<key file content>` |
| `/traefik/tls/2/certificate/keyfile` | `<key file content>` |
### Atomic configuration changes
@@ -422,7 +428,7 @@ Træfik will not start but the [static configuration](/basics/#static-trfik-conf
If you configured ACME (Let's Encrypt), your registration account and your certificates will also be uploaded.
If you configured a file backend `[file]`, all your dynamic configuration (backends, frontends...) will be uploaded to the Key-value store.
If you configured a file provider `[file]`, all your dynamic configuration (backends, frontends...) will be uploaded to the Key-value store.
To upload your ACME certificates to the KV store, get your Traefik TOML file and add the new `storage` option in the `acme` section:

View File

@@ -101,8 +101,8 @@ Let's explain this command:
| `--constraint=node.role==manager` | we ask docker to schedule Træfik on a manager node. |
| `--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock` | we bind mount the docker socket where Træfik is scheduled to be able to speak to the daemon. |
| `--network traefik-net` | we attach the Træfik service (and thus the underlying container) to the `traefik-net` network. |
| `--docker` | enable docker backend, and `--docker.swarmMode` to enable the swarm mode on Træfik. |
| `--api | activate the webUI on port 8080 |
| `--docker` | enable docker provider, and `--docker.swarmMode` to enable the swarm mode on Træfik. |
| `--api` | activate the webUI on port 8080 |
## Deploy your apps

View File

@@ -104,7 +104,7 @@ Let's explain this command:
| `--net=my-net` | run the container on the network my-net |
| `-v /var/lib/boot2docker/:/ssl` | mount the ssl keys generated by docker-machine |
| `-c /dev/null` | empty config file |
| `--docker` | enable docker backend |
| `--docker` | enable docker provider |
| `--docker.endpoint=tcp://172.18.0.1:2376` | connect to the swarm master using the docker_gwbridge network |
| `--docker.tls` | enable TLS using the docker-machine keys |
| `--api` | activate the webUI on port 8080 |

View File

@@ -50,7 +50,7 @@ start_boulder() {
# Script usage
show_usage() {
echo
echo "USAGE : manage_acme_docker_environment.sh [--start|--stop|--restart]"
echo "USAGE : manage_acme_docker_environment.sh [--dev|--start|--stop|--restart]"
echo
}

View File

@@ -28,6 +28,11 @@ spec:
containers:
- image: traefik
name: traefik-ingress-lb
ports:
- name: http
containerPort: 80
- name: admin
containerPort: 8080
args:
- --api
- --kubernetes

View File

@@ -21,7 +21,6 @@ spec:
spec:
serviceAccountName: traefik-ingress-controller
terminationGracePeriodSeconds: 60
hostNetwork: true
containers:
- image: traefik
name: traefik-ingress-lb
@@ -31,6 +30,7 @@ spec:
hostPort: 80
- name: admin
containerPort: 8080
hostport: 8080
securityContext:
capabilities:
drop:
@@ -57,4 +57,3 @@ spec:
- protocol: TCP
port: 8080
name: admin
type: NodePort

View File

@@ -30,7 +30,7 @@ type accessLogValue struct {
code string
user string
frontendName string
backendName string
backendURL string
}
func (s *AccessLogSuite) SetUpSuite(c *check.C) {
@@ -103,7 +103,7 @@ func (s *AccessLogSuite) TestAccessLogAuthFrontend(c *check.C) {
code: "401",
user: "-",
frontendName: "Auth for frontend-Host-frontend-auth-docker-local",
backendName: "-",
backendURL: "/",
},
}
@@ -151,7 +151,7 @@ func (s *AccessLogSuite) TestAccessLogAuthEntrypoint(c *check.C) {
code: "401",
user: "-",
frontendName: "Auth for entrypoint",
backendName: "-",
backendURL: "/",
},
}
@@ -199,7 +199,7 @@ func (s *AccessLogSuite) TestAccessLogAuthEntrypointSuccess(c *check.C) {
code: "200",
user: "test",
frontendName: "Host-entrypoint-auth-docker",
backendName: "http://172.17.0",
backendURL: "http://172.17.0",
},
}
@@ -247,14 +247,14 @@ func (s *AccessLogSuite) TestAccessLogDigestAuthEntrypoint(c *check.C) {
code: "401",
user: "-",
frontendName: "Auth for entrypoint",
backendName: "-",
backendURL: "/",
},
{
formatOnly: false,
code: "200",
user: "test",
frontendName: "Host-entrypoint-digest-auth-docker",
backendName: "http://172.17.0",
backendURL: "http://172.17.0",
},
}
@@ -355,7 +355,7 @@ func (s *AccessLogSuite) TestAccessLogEntrypointRedirect(c *check.C) {
code: "302",
user: "-",
frontendName: "entrypoint redirect for frontend-",
backendName: "-",
backendURL: "/",
},
{
formatOnly: true,
@@ -405,7 +405,7 @@ func (s *AccessLogSuite) TestAccessLogFrontendRedirect(c *check.C) {
code: "302",
user: "-",
frontendName: "frontend redirect for frontend-Path-",
backendName: "-",
backendURL: "/",
},
{
formatOnly: true,
@@ -461,7 +461,7 @@ func (s *AccessLogSuite) TestAccessLogRateLimit(c *check.C) {
code: "429",
user: "-",
frontendName: "rate limit for frontend-Host-ratelimit",
backendName: "/",
backendURL: "/",
},
}
@@ -512,7 +512,7 @@ func (s *AccessLogSuite) TestAccessLogBackendNotFound(c *check.C) {
code: "404",
user: "-",
frontendName: "backend not found",
backendName: "/",
backendURL: "/",
},
}
@@ -557,7 +557,7 @@ func (s *AccessLogSuite) TestAccessLogEntrypointWhitelist(c *check.C) {
code: "403",
user: "-",
frontendName: "ipwhitelister for entrypoint httpWhitelistReject",
backendName: "-",
backendURL: "/",
},
}
@@ -604,7 +604,7 @@ func (s *AccessLogSuite) TestAccessLogFrontendWhitelist(c *check.C) {
code: "403",
user: "-",
frontendName: "ipwhitelister for frontend-Host-frontend-whitelist",
backendName: "-",
backendURL: "/",
},
}
@@ -734,7 +734,7 @@ func checkAccessLogExactValues(c *check.C, line string, i int, v accessLogValue)
c.Assert(results[accesslog.OriginStatus], checker.Equals, v.code)
c.Assert(results[accesslog.RequestCount], checker.Equals, fmt.Sprintf("%d", i+1))
c.Assert(results[accesslog.FrontendName], checker.Matches, `^"?`+v.frontendName+`.*$`)
c.Assert(results[accesslog.BackendURL], checker.Matches, `^"?`+v.backendName+`.*$`)
c.Assert(results[accesslog.BackendURL], checker.Matches, `^"?`+v.backendURL+`.*$`)
c.Assert(results[accesslog.Duration], checker.Matches, `^\d+ms$`)
}

View File

@@ -1,6 +1,7 @@
package integration
import (
"bytes"
"fmt"
"net/http"
"time"
@@ -41,7 +42,7 @@ func (s *ConsulCatalogSuite) waitToElectConsulLeader() error {
leader, err := s.consulClient.Status().Leader()
if err != nil || len(leader) == 0 {
return fmt.Errorf("Leader not found. %v", err)
return fmt.Errorf("leader not found. %v", err)
}
return nil
@@ -55,9 +56,6 @@ func (s *ConsulCatalogSuite) createConsulClient(config *api.Config, c *check.C)
s.consulClient = consulClient
return consulClient
}
func (s *ConsulCatalogSuite) startConsulService(c *check.C) {
}
func (s *ConsulCatalogSuite) registerService(name string, address string, port int, tags []string) error {
catalog := s.consulClient.Catalog()
@@ -78,28 +76,45 @@ func (s *ConsulCatalogSuite) registerService(name string, address string, port i
return err
}
func (s *ConsulCatalogSuite) registerAgentService(name string, address string, port int, tags []string) error {
func (s *ConsulCatalogSuite) registerAgentService(name string, address string, port int, tags []string, withHealthCheck bool) error {
agent := s.consulClient.Agent()
err := agent.ServiceRegister(
var healthCheck *api.AgentServiceCheck
if withHealthCheck {
healthCheck = &api.AgentServiceCheck{
HTTP: "http://" + address,
Interval: "10s",
}
} else {
healthCheck = nil
}
return agent.ServiceRegister(
&api.AgentServiceRegistration{
ID: address,
Tags: tags,
Name: name,
Address: address,
Port: port,
Check: &api.AgentServiceCheck{
HTTP: "http://" + address,
Interval: "10s",
},
Check: healthCheck,
},
)
return err
}
func (s *ConsulCatalogSuite) registerCheck(name string, address string, port int) error {
agent := s.consulClient.Agent()
checkRegistration := &api.AgentCheckRegistration{
ID: fmt.Sprintf("%s-%s", name, address),
Name: name,
ServiceID: address,
}
checkRegistration.HTTP = fmt.Sprintf("http://%s:%d/health", address, port)
checkRegistration.Interval = "2s"
checkRegistration.CheckID = address
return agent.CheckRegister(checkRegistration)
}
func (s *ConsulCatalogSuite) deregisterAgentService(address string) error {
agent := s.consulClient.Agent()
err := agent.ServiceDeregister(address)
return err
return agent.ServiceDeregister(address)
}
func (s *ConsulCatalogSuite) deregisterService(name string, address string) error {
@@ -115,6 +130,22 @@ func (s *ConsulCatalogSuite) deregisterService(name string, address string) erro
return err
}
func (s *ConsulCatalogSuite) consulEnableServiceMaintenance(name string) error {
return s.consulClient.Agent().EnableServiceMaintenance(name, fmt.Sprintf("Maintenance mode for service %s", name))
}
func (s *ConsulCatalogSuite) consulDisableServiceMaintenance(name string) error {
return s.consulClient.Agent().DisableServiceMaintenance(name)
}
func (s *ConsulCatalogSuite) consulEnableNodeMaintenance() error {
return s.consulClient.Agent().EnableNodeMaintenance("Maintenance mode for node")
}
func (s *ConsulCatalogSuite) consulDisableNodeMaintenance() error {
return s.consulClient.Agent().DisableNodeMaintenance()
}
func (s *ConsulCatalogSuite) TestSimpleConfiguration(c *check.C) {
cmd, display := s.traefikCmd(
withConfigFile("fixtures/consul_catalog/simple.toml"),
@@ -273,7 +304,7 @@ func (s *ConsulCatalogSuite) TestRefreshConfigWithMultipleNodeWithoutHealthCheck
c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", whoami.NetworkSettings.IPAddress)
err = s.registerAgentService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1"})
err = s.registerAgentService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1"}, true)
c.Assert(err, checker.IsNil, check.Commentf("Error registering agent service"))
defer s.deregisterAgentService(whoami.NetworkSettings.IPAddress)
@@ -514,3 +545,132 @@ func (s *ConsulCatalogSuite) TestRetryWithConsulServer(c *check.C) {
err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody())
c.Assert(err, checker.IsNil)
}
func (s *ConsulCatalogSuite) TestServiceWithMultipleHealthCheck(c *check.C) {
//Scale consul to 0 to be able to start traefik before and test retry
s.composeProject.Scale(c, "consul", 0)
cmd, display := s.traefikCmd(
withConfigFile("fixtures/consul_catalog/simple.toml"),
"--consulCatalog",
"--consulCatalog.watch=false",
"--consulCatalog.exposedByDefault=true",
"--consulCatalog.endpoint="+s.consulIP+":8500",
"--consulCatalog.domain=consul.localhost")
defer display(c)
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
// Wait for Traefik to turn ready.
err = try.GetRequest("http://127.0.0.1:8000/", 2*time.Second, 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 = "test.consul.localhost"
// Request should fail
err = try.Request(req, 2*time.Second, try.StatusCodeIs(http.StatusNotFound), try.HasBody())
c.Assert(err, checker.IsNil)
// Scale consul to 1
s.composeProject.Scale(c, "consul", 1)
s.waitToElectConsulLeader()
whoami := s.composeProject.Container(c, "whoami1")
// Register service
err = s.registerAgentService("test", whoami.NetworkSettings.IPAddress, 80, []string{"name=whoami1"}, true)
c.Assert(err, checker.IsNil, check.Commentf("Error registering agent service"))
defer s.deregisterAgentService(whoami.NetworkSettings.IPAddress)
// Register one healthcheck
err = s.registerCheck("test", whoami.NetworkSettings.IPAddress, 80)
c.Assert(err, checker.IsNil, check.Commentf("Error registering check"))
// Provider consul catalog should be present
err = try.GetRequest("http://127.0.0.1:8080/api/providers", 10*time.Second, try.BodyContains("consul_catalog"))
c.Assert(err, checker.IsNil)
// Should be ok
err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody())
c.Assert(err, checker.IsNil)
// Change health value of service to critical
reqHealth, err := http.NewRequest(http.MethodPost, fmt.Sprintf("http://%s:80/health", whoami.NetworkSettings.IPAddress), bytes.NewBuffer([]byte("500")))
c.Assert(err, checker.IsNil)
reqHealth.Host = "test.consul.localhost"
err = try.Request(reqHealth, 10*time.Second, try.StatusCodeIs(http.StatusOK))
c.Assert(err, checker.IsNil)
// Should be a 404
err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusNotFound), try.HasBody())
c.Assert(err, checker.IsNil)
// Change health value of service to passing
reqHealth, err = http.NewRequest(http.MethodPost, fmt.Sprintf("http://%s:80/health", whoami.NetworkSettings.IPAddress), bytes.NewBuffer([]byte("200")))
c.Assert(err, checker.IsNil)
err = try.Request(reqHealth, 10*time.Second, try.StatusCodeIs(http.StatusOK))
c.Assert(err, checker.IsNil)
// Should be a 200
err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody())
c.Assert(err, checker.IsNil)
}
func (s *ConsulCatalogSuite) TestMaintenanceMode(c *check.C) {
cmd, display := s.traefikCmd(
withConfigFile("fixtures/consul_catalog/simple.toml"),
"--consulCatalog",
"--consulCatalog.endpoint="+s.consulIP+":8500",
"--consulCatalog.domain=consul.localhost")
defer display(c)
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
// Wait for Traefik to turn ready.
err = try.GetRequest("http://127.0.0.1:8000/", 2*time.Second, try.StatusCodeIs(http.StatusNotFound))
c.Assert(err, checker.IsNil)
whoami := s.composeProject.Container(c, "whoami1")
err = s.registerAgentService("test", whoami.NetworkSettings.IPAddress, 80, []string{}, false)
c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
c.Assert(err, checker.IsNil)
req.Host = "test.consul.localhost"
err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody())
c.Assert(err, checker.IsNil)
// Enable service maintenance mode
err = s.consulEnableServiceMaintenance(whoami.NetworkSettings.IPAddress)
c.Assert(err, checker.IsNil)
err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusNotFound), try.HasBody())
c.Assert(err, checker.IsNil)
// Disable service maintenance mode
err = s.consulDisableServiceMaintenance(whoami.NetworkSettings.IPAddress)
c.Assert(err, checker.IsNil)
err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody())
c.Assert(err, checker.IsNil)
// Enable node maintenance mode
err = s.consulEnableNodeMaintenance()
c.Assert(err, checker.IsNil)
err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusNotFound), try.HasBody())
c.Assert(err, checker.IsNil)
// Disable node maintenance mode
err = s.consulDisableNodeMaintenance()
c.Assert(err, checker.IsNil)
err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody())
c.Assert(err, checker.IsNil)
}

View File

@@ -585,21 +585,14 @@ func (s *ConsulSuite) TestSNIDynamicTlsConfig(c *check.C) {
})
c.Assert(err, checker.IsNil)
// wait for traefik
err = try.GetRequest("http://127.0.0.1:8081/api/providers", 60*time.Second, try.BodyContains("MIIEpQIBAAKCAQEA1RducBK6EiFDv3TYB8ZcrfKWRVaSfHzWicO3J5WdST9oS7hG"))
c.Assert(err, checker.IsNil)
req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
c.Assert(err, checker.IsNil)
client := &http.Client{Transport: tr1}
req.Host = tr1.TLSClientConfig.ServerName
req.Header.Set("Host", tr1.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*")
var resp *http.Response
resp, err = client.Do(req)
err = try.RequestWithTransport(req, 30*time.Second, tr1, try.HasCn("snitest.com"))
c.Assert(err, checker.IsNil)
cn := resp.TLS.PeerCertificates[0].Subject.CommonName
c.Assert(cn, checker.Equals, "snitest.com")
// now we configure the second keypair in consul and the request for host "snitest.org" will use the second keypair
for key, value := range tlsconfigure2 {
@@ -614,18 +607,12 @@ func (s *ConsulSuite) TestSNIDynamicTlsConfig(c *check.C) {
})
c.Assert(err, checker.IsNil)
// waiting for traefik to pull configuration
err = try.GetRequest("http://127.0.0.1:8081/api/providers", 30*time.Second, try.BodyContains("MIIEogIBAAKCAQEAvG9kL+vF57+MICehzbqcQAUlAOSl5r"))
c.Assert(err, checker.IsNil)
req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
c.Assert(err, checker.IsNil)
client = &http.Client{Transport: tr2}
req.Host = tr2.TLSClientConfig.ServerName
req.Header.Set("Host", tr2.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*")
resp, err = client.Do(req)
err = try.RequestWithTransport(req, 30*time.Second, tr2, try.HasCn("snitest.org"))
c.Assert(err, checker.IsNil)
cn = resp.TLS.PeerCertificates[0].Subject.CommonName
c.Assert(cn, checker.Equals, "snitest.org")
}

View File

@@ -532,21 +532,14 @@ func (s *Etcd3Suite) TestSNIDynamicTlsConfig(c *check.C) {
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
// wait for Træfik
err = try.GetRequest("http://127.0.0.1:8081/api/providers", 60*time.Second, try.BodyContains(string("MIIEpQIBAAKCAQEA1RducBK6EiFDv3TYB8ZcrfKWRVaSfHzWicO3J5WdST9oS7h")))
c.Assert(err, checker.IsNil)
req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
c.Assert(err, checker.IsNil)
client := &http.Client{Transport: tr1}
req.Host = tr1.TLSClientConfig.ServerName
req.Header.Set("Host", tr1.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*")
var resp *http.Response
resp, err = client.Do(req)
err = try.RequestWithTransport(req, 30*time.Second, tr1, try.HasCn("snitest.com"))
c.Assert(err, checker.IsNil)
cn := resp.TLS.PeerCertificates[0].Subject.CommonName
c.Assert(cn, checker.Equals, "snitest.com")
// now we configure the second keypair in etcd and the request for host "snitest.org" will use the second keypair
@@ -562,20 +555,14 @@ func (s *Etcd3Suite) TestSNIDynamicTlsConfig(c *check.C) {
})
c.Assert(err, checker.IsNil)
// waiting for Træfik to pull configuration
err = try.GetRequest("http://127.0.0.1:8081/api/providers", 30*time.Second, try.BodyContains("MIIEogIBAAKCAQEAvG9kL+vF57+MICehzbqcQAUlAOSl5r"))
c.Assert(err, checker.IsNil)
req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
c.Assert(err, checker.IsNil)
client = &http.Client{Transport: tr2}
req.Host = tr2.TLSClientConfig.ServerName
req.Header.Set("Host", tr2.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*")
resp, err = client.Do(req)
err = try.RequestWithTransport(req, 30*time.Second, tr2, try.HasCn("snitest.org"))
c.Assert(err, checker.IsNil)
cn = resp.TLS.PeerCertificates[0].Subject.CommonName
c.Assert(cn, checker.Equals, "snitest.org")
}
func (s *Etcd3Suite) TestDeleteSNIDynamicTlsConfig(c *check.C) {
@@ -646,21 +633,14 @@ func (s *Etcd3Suite) TestDeleteSNIDynamicTlsConfig(c *check.C) {
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
// wait for Træfik
err = try.GetRequest(traefikWebEtcdURL+"api/providers", 60*time.Second, try.BodyContains(string("MIIEpQIBAAKCAQEA1RducBK6EiFDv3TYB8ZcrfKWRVaSfHzWicO3J5WdST9oS7h")))
c.Assert(err, checker.IsNil)
req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
c.Assert(err, checker.IsNil)
client := &http.Client{Transport: tr1}
req.Host = tr1.TLSClientConfig.ServerName
req.Header.Set("Host", tr1.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*")
var resp *http.Response
resp, err = client.Do(req)
err = try.RequestWithTransport(req, 30*time.Second, tr1, try.HasCn("snitest.com"))
c.Assert(err, checker.IsNil)
cn := resp.TLS.PeerCertificates[0].Subject.CommonName
c.Assert(cn, checker.Equals, "snitest.com")
// now we delete the tls cert/key pairs,so the endpoint show use default cert/key pair
for key := range tlsconfigure1 {
@@ -668,18 +648,12 @@ func (s *Etcd3Suite) TestDeleteSNIDynamicTlsConfig(c *check.C) {
c.Assert(err, checker.IsNil)
}
// waiting for Træfik to pull configuration
err = try.GetRequest(traefikWebEtcdURL+"api/providers", 30*time.Second, try.BodyNotContains("MIIEpQIBAAKCAQEA1RducBK6EiFDv3TYB8ZcrfKWRVaSfHzWicO3J5WdST9oS7h"))
c.Assert(err, checker.IsNil)
req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
c.Assert(err, checker.IsNil)
client = &http.Client{Transport: tr1}
req.Host = tr1.TLSClientConfig.ServerName
req.Header.Set("Host", tr1.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*")
resp, err = client.Do(req)
err = try.RequestWithTransport(req, 30*time.Second, tr1, try.HasCn("TRAEFIK DEFAULT CERT"))
c.Assert(err, checker.IsNil)
cn = resp.TLS.PeerCertificates[0].Subject.CommonName
c.Assert(cn, checker.Equals, "TRAEFIK DEFAULT CERT")
}

View File

@@ -548,21 +548,14 @@ func (s *EtcdSuite) TestSNIDynamicTlsConfig(c *check.C) {
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
// wait for Træfik
err = try.GetRequest("http://127.0.0.1:8081/api/providers", 60*time.Second, try.BodyContains(string("MIIEpQIBAAKCAQEA1RducBK6EiFDv3TYB8ZcrfKWRVaSfHzWicO3J5WdST9oS7h")))
c.Assert(err, checker.IsNil)
req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
c.Assert(err, checker.IsNil)
client := &http.Client{Transport: tr1}
req.Host = tr1.TLSClientConfig.ServerName
req.Header.Set("Host", tr1.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*")
var resp *http.Response
resp, err = client.Do(req)
err = try.RequestWithTransport(req, 30*time.Second, tr1, try.HasCn("snitest.com"))
c.Assert(err, checker.IsNil)
cn := resp.TLS.PeerCertificates[0].Subject.CommonName
c.Assert(cn, checker.Equals, "snitest.com")
// now we configure the second keypair in etcd and the request for host "snitest.org" will use the second keypair
@@ -578,18 +571,12 @@ func (s *EtcdSuite) TestSNIDynamicTlsConfig(c *check.C) {
})
c.Assert(err, checker.IsNil)
// waiting for Træfik to pull configuration
err = try.GetRequest("http://127.0.0.1:8081/api/providers", 30*time.Second, try.BodyContains("MIIEogIBAAKCAQEAvG9kL+vF57+MICehzbqcQAUlAOSl5r"))
c.Assert(err, checker.IsNil)
req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
c.Assert(err, checker.IsNil)
client = &http.Client{Transport: tr2}
req.Host = tr2.TLSClientConfig.ServerName
req.Header.Set("Host", tr2.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*")
resp, err = client.Do(req)
err = try.RequestWithTransport(req, 30*time.Second, tr2, try.HasCn("snitest.org"))
c.Assert(err, checker.IsNil)
cn = resp.TLS.PeerCertificates[0].Subject.CommonName
c.Assert(cn, checker.Equals, "snitest.org")
}

View File

@@ -14,6 +14,7 @@ defaultEntryPoints = ["http", "https"]
email = "test@traefik.io"
storage = "/tmp/acme.json"
entryPoint = "https"
acmeLogging = true
onDemand = {{.OnDemand}}
onHostRule = {{.OnHostRule}}
caServer = "http://{{.BoulderHost}}:4001/directory"

View File

@@ -13,6 +13,7 @@ defaultEntryPoints = ["http", "https"]
email = "test@traefik.io"
storage = "/tmp/acme.json"
entryPoint = "https"
acmeLogging = true
onDemand = {{.OnDemand}}
onHostRule = {{.OnHostRule}}
caServer = "http://{{.BoulderHost}}:4001/directory"

View File

@@ -16,6 +16,7 @@ defaultEntryPoints = ["http", "https"]
email = "test@traefik.io"
storage = "/tmp/acme.json"
entryPoint = "https"
acmeLogging = true
onDemand = {{.OnDemand}}
onHostRule = {{.OnHostRule}}
caServer = "http://{{.BoulderHost}}:4001/directory"

View File

@@ -14,6 +14,7 @@ defaultEntryPoints = ["http", "https"]
email = "test@traefik.io"
storage = "/tmp/acme.json"
entryPoint = "https"
acmeLogging = true
onDemand = {{.OnDemand}}
onHostRule = {{.OnHostRule}}
caServer = "http://{{.BoulderHost}}:4001/directory"

View File

@@ -17,6 +17,7 @@ email = "test@traefik.io"
storage = "/tmp/acme.json"
entryPoint = "https"
onHostRule = true
acmeLogging = true
caServer = "http://{{.BoulderHost}}:4001/directory"
# No challenge defined

View File

@@ -16,6 +16,7 @@ defaultEntryPoints = ["http", "https"]
email = "test@traefik.io"
storage = "/tmp/acme.json"
entryPoint = "https"
acmeLogging = true
onHostRule = true
caServer = "http://wrongurl:4001/directory"

View File

@@ -2,7 +2,7 @@
[backends]
[backends.backend2]
[backends.backend2.servers.server1]
url = "http://172.17.0.2:80"
url = "http://172.17.0.123:80"
weight = 1
[frontends]

View File

@@ -14,6 +14,7 @@ defaultEntryPoints = ["http", "https"]
email = "test@traefik.io"
storage = "/tmp/acme.json"
entryPoint = "https"
acmeLogging = true
onDemand = {{.OnDemand}}
onHostRule = {{.OnHostRule}}
caServer = "http://{{.BoulderHost}}:4001/directory"

View File

@@ -14,6 +14,7 @@ defaultEntryPoints = ["http", "https"]
email = "test@traefik.io"
storage = "/tmp/acme.json"
entryPoint = "https"
acmeLogging = true
onDemand = false
onHostRule = false
caServer = "http://{{.BoulderHost}}:4001/directory"

View File

@@ -14,6 +14,7 @@ defaultEntryPoints = ["http", "https"]
email = "test@traefik.io"
storage = "/tmp/acme.jsonl"
entryPoint = "https"
acmeLogging = true
onDemand = {{.OnDemand}}
onHostRule = {{.OnHostRule}}
caServer = "http://{{.BoulderHost}}:4001/directory"

View File

@@ -88,6 +88,31 @@ func HasBody() ResponseCondition {
}
}
// HasCn returns a retry condition function.
// The condition returns an error if the cn is not correct.
func HasCn(cn string) ResponseCondition {
return func(res *http.Response) error {
if res.TLS == nil {
return errors.New("response doesn't have TLS")
}
if len(res.TLS.PeerCertificates) == 0 {
return errors.New("response TLS doesn't have peer certificates")
}
if res.TLS.PeerCertificates[0] == nil {
return errors.New("first peer certificate is nil")
}
commonName := res.TLS.PeerCertificates[0].Subject.CommonName
if cn != commonName {
return fmt.Errorf("common name don't match: %s != %s", cn, commonName)
}
return nil
}
}
// StatusCodeIs returns a retry condition function.
// The condition returns an error if the given response's status code is not the
// given HTTP status code.

View File

@@ -31,7 +31,7 @@ func Sleep(d time.Duration) {
// response body needs to be closed or not. Callers are expected to close on
// their own if the function returns a nil error.
func Response(req *http.Request, timeout time.Duration) (*http.Response, error) {
return doTryRequest(req, timeout)
return doTryRequest(req, timeout, nil)
}
// ResponseUntilStatusCode is like Request, but returns the response for further
@@ -40,7 +40,7 @@ func Response(req *http.Request, timeout time.Duration) (*http.Response, error)
// response body needs to be closed or not. Callers are expected to close on
// their own if the function returns a nil error.
func ResponseUntilStatusCode(req *http.Request, timeout time.Duration, statusCode int) (*http.Response, error) {
return doTryRequest(req, timeout, StatusCodeIs(statusCode))
return doTryRequest(req, timeout, nil, StatusCodeIs(statusCode))
}
// GetRequest is like Do, but runs a request against the given URL and applies
@@ -48,7 +48,7 @@ func ResponseUntilStatusCode(req *http.Request, timeout time.Duration, statusCod
// ResponseCondition may be nil, in which case only the request against the URL must
// succeed.
func GetRequest(url string, timeout time.Duration, conditions ...ResponseCondition) error {
resp, err := doTryGet(url, timeout, conditions...)
resp, err := doTryGet(url, timeout, nil, conditions...)
if resp != nil && resp.Body != nil {
defer resp.Body.Close()
@@ -62,7 +62,21 @@ func GetRequest(url string, timeout time.Duration, conditions ...ResponseConditi
// ResponseCondition may be nil, in which case only the request against the URL must
// succeed.
func Request(req *http.Request, timeout time.Duration, conditions ...ResponseCondition) error {
resp, err := doTryRequest(req, timeout, conditions...)
resp, err := doTryRequest(req, timeout, nil, conditions...)
if resp != nil && resp.Body != nil {
defer resp.Body.Close()
}
return err
}
// RequestWithTransport is like Do, but runs a request against the given URL and applies
// the condition on the response.
// ResponseCondition may be nil, in which case only the request against the URL must
// succeed.
func RequestWithTransport(req *http.Request, timeout time.Duration, transport *http.Transport, conditions ...ResponseCondition) error {
resp, err := doTryRequest(req, timeout, transport, conditions...)
if resp != nil && resp.Body != nil {
defer resp.Body.Close()
@@ -112,24 +126,27 @@ func Do(timeout time.Duration, operation DoCondition) error {
}
}
func doTryGet(url string, timeout time.Duration, conditions ...ResponseCondition) (*http.Response, error) {
func doTryGet(url string, timeout time.Duration, transport *http.Transport, conditions ...ResponseCondition) (*http.Response, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
return doTryRequest(req, timeout, conditions...)
return doTryRequest(req, timeout, transport, conditions...)
}
func doTryRequest(request *http.Request, timeout time.Duration, conditions ...ResponseCondition) (*http.Response, error) {
return doRequest(Do, timeout, request, conditions...)
func doTryRequest(request *http.Request, timeout time.Duration, transport *http.Transport, conditions ...ResponseCondition) (*http.Response, error) {
return doRequest(Do, timeout, request, transport, conditions...)
}
func doRequest(action timedAction, timeout time.Duration, request *http.Request, conditions ...ResponseCondition) (*http.Response, error) {
func doRequest(action timedAction, timeout time.Duration, request *http.Request, transport *http.Transport, conditions ...ResponseCondition) (*http.Response, error) {
var resp *http.Response
return resp, action(timeout, func() error {
var err error
client := http.DefaultClient
if transport != nil {
client.Transport = transport
}
resp, err = client.Do(request)
if err != nil {

View File

@@ -36,25 +36,18 @@ const (
backendServerUpName = metricNamePrefix + "backend_server_up"
)
const (
// generationAgeForever indicates that a metric never gets outdated.
generationAgeForever = 0
// generationAgeDefault is the default age of three generations.
generationAgeDefault = 3
)
// promState holds all metric state internally and acts as the only Collector we register for Prometheus.
//
// This enables control to remove metrics that belong to outdated configuration.
// As an example why this is required, consider Traefik learns about a new service.
// It populates the 'traefik_server_backend_up' metric for it with a value of 1 (alive).
// When the backend is undeployed now the metric is still there in the client library
// and will be until Traefik would be restarted.
// and will be returned on the metrics endpoint until Traefik would be restarted.
//
// To solve this problem promState keeps track of configuration generations.
// Every time a new configuration is loaded, the generation is increased by one.
// Metrics that "belong" to a dynamic configuration part of Traefik (e.g. backend, entrypoint)
// are removed, given they were tracked more than 3 generations ago.
// To solve this problem promState keeps track of Traefik's dynamic configuration.
// Metrics that "belong" to a dynamic configuration part like backends or entrypoints
// are removed after they were scraped at least once when the corresponding object
// doesn't exist anymore.
var promState = newPrometheusState()
// PrometheusHandler exposes Prometheus routes.
@@ -163,40 +156,66 @@ func RegisterPrometheus(config *types.Prometheus) Registry {
}
}
// OnConfigurationUpdate increases the current generation of the prometheus state.
func OnConfigurationUpdate() {
promState.IncGeneration()
// OnConfigurationUpdate receives the current configuration from Traefik.
// It then converts the configuration to the optimized package internal format
// and sets it to the promState.
func OnConfigurationUpdate(configurations types.Configurations) {
dynamicConfig := newDynamicConfig()
for _, config := range configurations {
for _, frontend := range config.Frontends {
for _, entrypointName := range frontend.EntryPoints {
dynamicConfig.entrypoints[entrypointName] = true
}
}
for backendName, backend := range config.Backends {
dynamicConfig.backends[backendName] = make(map[string]bool)
for _, server := range backend.Servers {
dynamicConfig.backends[backendName][server.URL] = true
}
}
}
promState.SetDynamicConfig(dynamicConfig)
}
func newPrometheusState() *prometheusState {
collectors := make(chan *collector)
state := make(map[string]*collector)
return &prometheusState{
collectors: collectors,
state: state,
collectors: make(chan *collector),
dynamicConfig: newDynamicConfig(),
state: make(map[string]*collector),
}
}
type prometheusState struct {
currentGeneration int
collectors chan *collector
describers []func(ch chan<- *stdprometheus.Desc)
collectors chan *collector
describers []func(ch chan<- *stdprometheus.Desc)
mtx sync.Mutex
state map[string]*collector
mtx sync.Mutex
dynamicConfig *dynamicConfig
state map[string]*collector
}
func (ps *prometheusState) IncGeneration() {
// reset is a utility method for unit testing. It should be called after each
// test run that changes promState internally in order to avoid dependencies
// between unit tests.
func (ps *prometheusState) reset() {
ps.collectors = make(chan *collector)
ps.describers = []func(ch chan<- *stdprometheus.Desc){}
ps.dynamicConfig = newDynamicConfig()
ps.state = make(map[string]*collector)
}
func (ps *prometheusState) SetDynamicConfig(dynamicConfig *dynamicConfig) {
ps.mtx.Lock()
defer ps.mtx.Unlock()
ps.currentGeneration++
ps.dynamicConfig = dynamicConfig
}
func (ps *prometheusState) ListenValueUpdates() {
for collector := range ps.collectors {
ps.mtx.Lock()
collector.lastTrackedGeneration = ps.currentGeneration
ps.state[collector.id] = collector
ps.mtx.Unlock()
}
@@ -212,42 +231,89 @@ func (ps *prometheusState) Describe(ch chan<- *stdprometheus.Desc) {
// Collect implements prometheus.Collector. It calls the Collect
// method of all metrics it received on the collectors channel.
// It's also responsible to remove metrics that were tracked
// at least three generations ago. Those metrics are cleaned up
// after the Collect of them were called.
// It's also responsible to remove metrics that belong to an outdated configuration.
// The removal happens only after their Collect method was called to ensure that
// also those metrics will be exported on the current scrape.
func (ps *prometheusState) Collect(ch chan<- stdprometheus.Metric) {
ps.mtx.Lock()
defer ps.mtx.Unlock()
outdatedKeys := []string{}
var outdatedKeys []string
for key, cs := range ps.state {
cs.collector.Collect(ch)
if cs.maxAge == generationAgeForever {
continue
}
if ps.currentGeneration-cs.lastTrackedGeneration >= cs.maxAge {
if ps.isOutdated(cs) {
outdatedKeys = append(outdatedKeys, key)
}
}
for _, key := range outdatedKeys {
ps.state[key].delete()
delete(ps.state, key)
}
}
func newCollector(metricName string, lnvs labelNamesValues, c stdprometheus.Collector) *collector {
maxAge := generationAgeDefault
// isOutdated checks whether the passed collector has labels that mark
// it as belonging to an outdated configuration of Traefik.
func (ps *prometheusState) isOutdated(collector *collector) bool {
labels := collector.labels
// metrics without labels should never become outdated
if len(lnvs) == 0 {
maxAge = generationAgeForever
if entrypointName, ok := labels["entrypoint"]; ok && !ps.dynamicConfig.hasEntrypoint(entrypointName) {
return true
}
if backendName, ok := labels["backend"]; ok {
if !ps.dynamicConfig.hasBackend(backendName) {
return true
}
if url, ok := labels["url"]; ok && !ps.dynamicConfig.hasServerURL(backendName, url) {
return true
}
}
return false
}
func newDynamicConfig() *dynamicConfig {
return &dynamicConfig{
entrypoints: make(map[string]bool),
backends: make(map[string]map[string]bool),
}
}
// dynamicConfig holds the current configuration for entrypoints, backends,
// and server URLs in an optimized way to check for existence. This provides
// a performant way to check whether the collected metrics belong to the
// current configuration or to an outdated one.
type dynamicConfig struct {
entrypoints map[string]bool
backends map[string]map[string]bool
}
func (d *dynamicConfig) hasEntrypoint(entrypointName string) bool {
_, ok := d.entrypoints[entrypointName]
return ok
}
func (d *dynamicConfig) hasBackend(backendName string) bool {
_, ok := d.backends[backendName]
return ok
}
func (d *dynamicConfig) hasServerURL(backendName, serverURL string) bool {
if backend, hasBackend := d.backends[backendName]; hasBackend {
_, ok := backend[serverURL]
return ok
}
return false
}
func newCollector(metricName string, labels stdprometheus.Labels, c stdprometheus.Collector, delete func()) *collector {
return &collector{
id: buildMetricID(metricName, lnvs),
maxAge: maxAge,
id: buildMetricID(metricName, labels),
labels: labels,
collector: c,
delete: delete,
}
}
@@ -255,16 +321,19 @@ func newCollector(metricName string, lnvs labelNamesValues, c stdprometheus.Coll
// It adds information on how many generations this metric should be present
// in the /metrics output, relatived to the time it was last tracked.
type collector struct {
id string
collector stdprometheus.Collector
lastTrackedGeneration int
maxAge int
id string
labels stdprometheus.Labels
collector stdprometheus.Collector
delete func()
}
func buildMetricID(metricName string, lnvs labelNamesValues) string {
newLnvs := append([]string{}, lnvs...)
sort.Strings(newLnvs)
return metricName + ":" + strings.Join(newLnvs, "|")
func buildMetricID(metricName string, labels stdprometheus.Labels) string {
var labelNamesValues []string
for name, value := range labels {
labelNamesValues = append(labelNamesValues, name, value)
}
sort.Strings(labelNamesValues)
return metricName + ":" + strings.Join(labelNamesValues, "|")
}
func newCounterFrom(collectors chan<- *collector, opts stdprometheus.CounterOpts, labelNames []string) *counter {
@@ -297,9 +366,12 @@ func (c *counter) With(labelValues ...string) metrics.Counter {
}
func (c *counter) Add(delta float64) {
collector := c.cv.With(c.labelNamesValues.ToLabels())
labels := c.labelNamesValues.ToLabels()
collector := c.cv.With(labels)
collector.Add(delta)
c.collectors <- newCollector(c.name, c.labelNamesValues, collector)
c.collectors <- newCollector(c.name, labels, collector, func() {
c.cv.Delete(labels)
})
}
func (c *counter) Describe(ch chan<- *stdprometheus.Desc) {
@@ -336,15 +408,21 @@ func (g *gauge) With(labelValues ...string) metrics.Gauge {
}
func (g *gauge) Add(delta float64) {
collector := g.gv.With(g.labelNamesValues.ToLabels())
labels := g.labelNamesValues.ToLabels()
collector := g.gv.With(labels)
collector.Add(delta)
g.collectors <- newCollector(g.name, g.labelNamesValues, collector)
g.collectors <- newCollector(g.name, labels, collector, func() {
g.gv.Delete(labels)
})
}
func (g *gauge) Set(value float64) {
collector := g.gv.With(g.labelNamesValues.ToLabels())
labels := g.labelNamesValues.ToLabels()
collector := g.gv.With(labels)
collector.Set(value)
g.collectors <- newCollector(g.name, g.labelNamesValues, collector)
g.collectors <- newCollector(g.name, labels, collector, func() {
g.gv.Delete(labels)
})
}
func (g *gauge) Describe(ch chan<- *stdprometheus.Desc) {
@@ -377,9 +455,12 @@ func (h *histogram) With(labelValues ...string) metrics.Histogram {
}
func (h *histogram) Observe(value float64) {
collector := h.hv.With(h.labelNamesValues.ToLabels())
labels := h.labelNamesValues.ToLabels()
collector := h.hv.With(labels)
collector.Observe(value)
h.collectors <- newCollector(h.name, h.labelNamesValues, collector)
h.collectors <- newCollector(h.name, labels, collector, func() {
h.hv.Delete(labels)
})
}
func (h *histogram) Describe(ch chan<- *stdprometheus.Desc) {

View File

@@ -7,12 +7,16 @@ import (
"testing"
"time"
th "github.com/containous/traefik/testhelpers"
"github.com/containous/traefik/types"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
)
func TestPrometheus(t *testing.T) {
// Reset state of global promState.
defer promState.reset()
prometheusRegistry := RegisterPrometheus(&types.Prometheus{})
defer prometheus.Unregister(promState)
@@ -177,56 +181,94 @@ func TestPrometheus(t *testing.T) {
}
}
func TestPrometheusGenerationLogicForMetricWithLabel(t *testing.T) {
func TestPrometheusMetricRemoval(t *testing.T) {
// Reset state of global promState.
defer promState.reset()
prometheusRegistry := RegisterPrometheus(&types.Prometheus{})
defer prometheus.Unregister(promState)
// Metrics with labels belonging to a specific configuration in Traefik
// should be removed when the generationMaxAge is exceeded. As example
// we use the traefik_backend_requests_total metric.
configurations := make(types.Configurations)
configurations["providerName"] = th.BuildConfiguration(
th.WithFrontends(
th.WithFrontend("backend1", th.WithEntryPoints("entrypoint1")),
),
th.WithBackends(
th.WithBackendNew("backend1", th.WithServersNew(th.WithServerNew("http://localhost:9000"))),
),
)
OnConfigurationUpdate(configurations)
// Register some metrics manually that are not part of the active configuration.
// Those metrics should be part of the /metrics output on the first scrape but
// should be removed after that scrape.
prometheusRegistry.
EntrypointReqsCounter().
With("entrypoint", "entrypoint2", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet, "protocol", "http").
Add(1)
prometheusRegistry.
BackendReqsCounter().
With("backend", "backend1", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet, "protocol", "http").
With("backend", "backend2", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet, "protocol", "http").
Add(1)
prometheusRegistry.
BackendServerUpGauge().
With("backend", "backend1", "url", "http://localhost:9999").
Set(1)
delayForTrackingCompletion()
assertMetricsExist(t, mustScrape(), entrypointReqsTotalName, backendReqsTotalName, backendServerUpName)
assertMetricsAbsent(t, mustScrape(), entrypointReqsTotalName, backendReqsTotalName, backendServerUpName)
// To verify that metrics belonging to active configurations are not removed
// here the counter examples.
prometheusRegistry.
EntrypointReqsCounter().
With("entrypoint", "entrypoint1", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet, "protocol", "http").
Add(1)
delayForTrackingCompletion()
assertMetricExists(t, backendReqsTotalName, mustScrape())
// Increase the config generation one more than the max age of a metric.
for i := 0; i < generationAgeDefault+1; i++ {
OnConfigurationUpdate()
}
// On the next scrape the metric still exists and will be removed
// after the scrape completed.
assertMetricExists(t, backendReqsTotalName, mustScrape())
// Now the metric should be absent.
assertMetricAbsent(t, backendReqsTotalName, mustScrape())
assertMetricsExist(t, mustScrape(), entrypointReqsTotalName)
assertMetricsExist(t, mustScrape(), entrypointReqsTotalName)
}
func TestPrometheusGenerationLogicForMetricWithoutLabel(t *testing.T) {
func TestPrometheusRemovedMetricsReset(t *testing.T) {
// Reset state of global promState.
defer promState.reset()
prometheusRegistry := RegisterPrometheus(&types.Prometheus{})
defer prometheus.Unregister(promState)
// Metrics without labels like traefik_config_reloads_total should live forever
// and never get removed.
prometheusRegistry.ConfigReloadsCounter().Add(1)
labelNamesValues := []string{
"backend", "backend",
"code", strconv.Itoa(http.StatusOK),
"method", http.MethodGet,
"protocol", "http",
}
prometheusRegistry.
BackendReqsCounter().
With(labelNamesValues...).
Add(3)
delayForTrackingCompletion()
assertMetricExists(t, configReloadsTotalName, mustScrape())
metricsFamilies := mustScrape()
assertCounterValue(t, 3, findMetricFamily(backendReqsTotalName, metricsFamilies), labelNamesValues...)
// Increase the config generation one more than the max age of a metric.
for i := 0; i < generationAgeDefault+100; i++ {
OnConfigurationUpdate()
}
// There is no dynamic configuration and so this metric will be deleted
// after the first scrape.
assertMetricsAbsent(t, mustScrape(), backendReqsTotalName)
// Scrape two times in order to verify, that it is not removed after the
// first scrape completed.
assertMetricExists(t, configReloadsTotalName, mustScrape())
assertMetricExists(t, configReloadsTotalName, mustScrape())
prometheusRegistry.
BackendReqsCounter().
With(labelNamesValues...).
Add(1)
delayForTrackingCompletion()
metricsFamilies = mustScrape()
assertCounterValue(t, 1, findMetricFamily(backendReqsTotalName, metricsFamilies), labelNamesValues...)
}
// Tracking and gathering the metrics happens concurrently.
@@ -247,17 +289,23 @@ func mustScrape() []*dto.MetricFamily {
return families
}
func assertMetricExists(t *testing.T, name string, families []*dto.MetricFamily) {
func assertMetricsExist(t *testing.T, families []*dto.MetricFamily, metricNames ...string) {
t.Helper()
if findMetricFamily(name, families) == nil {
t.Errorf("gathered metrics do not contain %q", name)
for _, metricName := range metricNames {
if findMetricFamily(metricName, families) == nil {
t.Errorf("gathered metrics should contain %q", metricName)
}
}
}
func assertMetricAbsent(t *testing.T, name string, families []*dto.MetricFamily) {
func assertMetricsAbsent(t *testing.T, families []*dto.MetricFamily, metricNames ...string) {
t.Helper()
if findMetricFamily(name, families) != nil {
t.Errorf("gathered metrics contain %q, but should not", name)
for _, metricName := range metricNames {
if findMetricFamily(metricName, families) != nil {
t.Errorf("gathered metrics should not contain %q", metricName)
}
}
}
@@ -270,6 +318,58 @@ func findMetricFamily(name string, families []*dto.MetricFamily) *dto.MetricFami
return nil
}
func findMetricByLabelNamesValues(family *dto.MetricFamily, labelNamesValues ...string) *dto.Metric {
if family == nil {
return nil
}
for _, metric := range family.Metric {
if hasMetricAllLabelPairs(metric, labelNamesValues...) {
return metric
}
}
return nil
}
func hasMetricAllLabelPairs(metric *dto.Metric, labelNamesValues ...string) bool {
for i := 0; i < len(labelNamesValues); i += 2 {
name, val := labelNamesValues[i], labelNamesValues[i+1]
if !hasMetricLabelPair(metric, name, val) {
return false
}
}
return true
}
func hasMetricLabelPair(metric *dto.Metric, labelName, labelValue string) bool {
for _, labelPair := range metric.Label {
if labelPair.GetName() == labelName && labelPair.GetValue() == labelValue {
return true
}
}
return false
}
func assertCounterValue(t *testing.T, want float64, family *dto.MetricFamily, labelNamesValues ...string) {
t.Helper()
metric := findMetricByLabelNamesValues(family, labelNamesValues...)
if metric == nil {
t.Error("metric must not be nil")
return
}
if metric.Counter == nil {
t.Errorf("metric %s must be a counter", family.GetName())
return
}
if cv := metric.Counter.GetValue(); cv != want {
t.Errorf("metric %s has value %v, want %v", family.GetName(), cv, want)
}
}
func buildCounterAssert(t *testing.T, metricName string, expectedValue int) func(family *dto.MetricFamily) {
return func(family *dto.MetricFamily) {
if cv := int(family.Metric[0].Counter.GetValue()); cv != expectedValue {

View File

@@ -3,15 +3,14 @@ package accesslog
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/urfave/negroni"
"github.com/vulcand/oxy/utils"
)
// SaveBackend sends the backend name to the logger. These are always used with a corresponding
// SaveFrontend handler.
// SaveBackend sends the backend name to the logger.
// These are always used with a corresponding SaveFrontend handler.
type SaveBackend struct {
next http.Handler
backendName string
@@ -23,61 +22,9 @@ func NewSaveBackend(next http.Handler, backendName string) http.Handler {
}
func (sb *SaveBackend) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
table := GetLogDataTable(r)
table.Core[BackendName] = sb.backendName
table.Core[BackendURL] = r.URL // note that this is *not* the original incoming URL
table.Core[BackendAddr] = r.URL.Host
crw := &captureResponseWriter{rw: rw}
start := time.Now().UTC()
sb.next.ServeHTTP(crw, r)
// use UTC to handle switchover of daylight saving correctly
table.Core[OriginDuration] = time.Now().UTC().Sub(start)
table.Core[OriginStatus] = crw.Status()
table.Core[OriginStatusLine] = fmt.Sprintf("%03d %s", crw.Status(), http.StatusText(crw.Status()))
// make copy of headers so we can ensure there is no subsequent mutation during response processing
table.OriginResponse = make(http.Header)
utils.CopyHeaders(table.OriginResponse, crw.Header())
table.Core[OriginContentSize] = crw.Size()
}
// SaveFrontend sends the frontend name to the logger. These are sometimes used with a corresponding
// SaveBackend handler, but not always. For example, redirected requests don't reach a backend.
type SaveFrontend struct {
next http.Handler
frontendName string
}
// NewSaveFrontend creates a SaveFrontend handler.
func NewSaveFrontend(next http.Handler, frontendName string) http.Handler {
return &SaveFrontend{next, frontendName}
}
func (sb *SaveFrontend) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
table := GetLogDataTable(r)
table.Core[FrontendName] = strings.TrimPrefix(sb.frontendName, "frontend-")
sb.next.ServeHTTP(rw, r)
}
// SaveNegroniFrontend sends the frontend name to the logger.
type SaveNegroniFrontend struct {
next negroni.Handler
frontendName string
}
// NewSaveNegroniFrontend creates a SaveNegroniFrontend handler.
func NewSaveNegroniFrontend(next negroni.Handler, frontendName string) negroni.Handler {
return &SaveNegroniFrontend{next, frontendName}
}
func (sb *SaveNegroniFrontend) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
table := GetLogDataTable(r)
table.Core[FrontendName] = strings.TrimPrefix(sb.frontendName, "frontend-")
sb.next.ServeHTTP(rw, r, next)
serveSaveBackend(rw, r, sb.backendName, func(crw *captureResponseWriter) {
sb.next.ServeHTTP(crw, r)
})
}
// SaveNegroniBackend sends the backend name to the logger.
@@ -92,13 +39,21 @@ func NewSaveNegroniBackend(next negroni.Handler, backendName string) negroni.Han
}
func (sb *SaveNegroniBackend) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
serveSaveBackend(rw, r, sb.backendName, func(crw *captureResponseWriter) {
sb.next.ServeHTTP(crw, r, next)
})
}
func serveSaveBackend(rw http.ResponseWriter, r *http.Request, backendName string, apply func(*captureResponseWriter)) {
table := GetLogDataTable(r)
table.Core[BackendName] = sb.backendName
table.Core[BackendName] = backendName
table.Core[BackendURL] = r.URL // note that this is *not* the original incoming URL
table.Core[BackendAddr] = r.URL.Host
crw := &captureResponseWriter{rw: rw}
start := time.Now().UTC()
sb.next.ServeHTTP(crw, r, next)
apply(crw)
// use UTC to handle switchover of daylight saving correctly
table.Core[OriginDuration] = time.Now().UTC().Sub(start)

View File

@@ -0,0 +1,51 @@
package accesslog
import (
"net/http"
"strings"
"github.com/urfave/negroni"
)
// SaveFrontend sends the frontend name to the logger.
// These are sometimes used with a corresponding SaveBackend handler, but not always.
// For example, redirected requests don't reach a backend.
type SaveFrontend struct {
next http.Handler
frontendName string
}
// NewSaveFrontend creates a SaveFrontend handler.
func NewSaveFrontend(next http.Handler, frontendName string) http.Handler {
return &SaveFrontend{next, frontendName}
}
func (sf *SaveFrontend) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
serveSaveFrontend(r, sf.frontendName, func() {
sf.next.ServeHTTP(rw, r)
})
}
// SaveNegroniFrontend sends the frontend name to the logger.
type SaveNegroniFrontend struct {
next negroni.Handler
frontendName string
}
// NewSaveNegroniFrontend creates a SaveNegroniFrontend handler.
func NewSaveNegroniFrontend(next negroni.Handler, frontendName string) negroni.Handler {
return &SaveNegroniFrontend{next, frontendName}
}
func (sf *SaveNegroniFrontend) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
serveSaveFrontend(r, sf.frontendName, func() {
sf.next.ServeHTTP(rw, r, next)
})
}
func serveSaveFrontend(r *http.Request, frontendName string, apply func()) {
table := GetLogDataTable(r)
table.Core[FrontendName] = strings.TrimPrefix(frontendName, "frontend-")
apply()
}

View File

@@ -92,15 +92,18 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request, next http.
if err != nil {
log.Error(err)
w.WriteHeader(recorder.GetCode())
w.Write([]byte(http.StatusText(recorder.GetCode())))
fmt.Fprint(w, http.StatusText(recorder.GetCode()))
return
}
recorderErrorPage := newResponseRecorder(w)
utils.CopyHeaders(pageReq.Header, req.Header)
utils.CopyHeaders(w.Header(), recorder.Header())
w.WriteHeader(recorder.GetCode())
h.backendHandler.ServeHTTP(w, pageReq.WithContext(req.Context()))
h.backendHandler.ServeHTTP(recorderErrorPage, pageReq.WithContext(req.Context()))
utils.CopyHeaders(w.Header(), recorderErrorPage.Header())
w.WriteHeader(recorder.GetCode())
w.Write(recorderErrorPage.GetBody().Bytes())
return
}
}
@@ -166,64 +169,65 @@ type responseRecorderWithCloseNotify struct {
// CloseNotify returns a channel that receives at most a
// single value (true) when the client connection has gone away.
func (rw *responseRecorderWithCloseNotify) CloseNotify() <-chan bool {
return rw.responseWriter.(http.CloseNotifier).CloseNotify()
func (r *responseRecorderWithCloseNotify) CloseNotify() <-chan bool {
return r.responseWriter.(http.CloseNotifier).CloseNotify()
}
// Header returns the response headers.
func (rw *responseRecorderWithoutCloseNotify) Header() http.Header {
if rw.HeaderMap == nil {
rw.HeaderMap = make(http.Header)
func (r *responseRecorderWithoutCloseNotify) Header() http.Header {
if r.HeaderMap == nil {
r.HeaderMap = make(http.Header)
}
return rw.HeaderMap
return r.HeaderMap
}
func (rw *responseRecorderWithoutCloseNotify) GetCode() int {
return rw.Code
func (r *responseRecorderWithoutCloseNotify) GetCode() int {
return r.Code
}
func (rw *responseRecorderWithoutCloseNotify) GetBody() *bytes.Buffer {
return rw.Body
func (r *responseRecorderWithoutCloseNotify) GetBody() *bytes.Buffer {
return r.Body
}
func (rw *responseRecorderWithoutCloseNotify) IsStreamingResponseStarted() bool {
return rw.streamingResponseStarted
func (r *responseRecorderWithoutCloseNotify) IsStreamingResponseStarted() bool {
return r.streamingResponseStarted
}
// Write always succeeds and writes to rw.Body, if not nil.
func (rw *responseRecorderWithoutCloseNotify) Write(buf []byte) (int, error) {
if rw.err != nil {
return 0, rw.err
func (r *responseRecorderWithoutCloseNotify) Write(buf []byte) (int, error) {
if r.err != nil {
return 0, r.err
}
return rw.Body.Write(buf)
return r.Body.Write(buf)
}
// WriteHeader sets rw.Code.
func (rw *responseRecorderWithoutCloseNotify) WriteHeader(code int) {
rw.Code = code
func (r *responseRecorderWithoutCloseNotify) WriteHeader(code int) {
r.Code = code
}
// Hijack hijacks the connection
func (rw *responseRecorderWithoutCloseNotify) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return rw.responseWriter.(http.Hijacker).Hijack()
func (r *responseRecorderWithoutCloseNotify) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return r.responseWriter.(http.Hijacker).Hijack()
}
// Flush sends any buffered data to the client.
func (rw *responseRecorderWithoutCloseNotify) Flush() {
if !rw.streamingResponseStarted {
utils.CopyHeaders(rw.responseWriter.Header(), rw.Header())
rw.responseWriter.WriteHeader(rw.Code)
rw.streamingResponseStarted = true
func (r *responseRecorderWithoutCloseNotify) Flush() {
if !r.streamingResponseStarted {
utils.CopyHeaders(r.responseWriter.Header(), r.Header())
r.responseWriter.WriteHeader(r.Code)
r.streamingResponseStarted = true
}
_, err := rw.responseWriter.Write(rw.Body.Bytes())
_, err := r.responseWriter.Write(r.Body.Bytes())
if err != nil {
log.Errorf("Error writing response in responseRecorder: %s", err)
rw.err = err
log.Errorf("Error writing response in responseRecorder: %v", err)
r.err = err
}
rw.Body.Reset()
r.Body.Reset()
if flusher, ok := rw.responseWriter.(http.Flusher); ok {
if flusher, ok := r.responseWriter.(http.Flusher); ok {
flusher.Flush()
}
}

View File

@@ -318,7 +318,6 @@ 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))
})
@@ -331,7 +330,6 @@ func TestHandlerOldWayIntegration(t *testing.T) {
n.ServeHTTP(recorder, req)
test.validate(t, recorder)
assert.Equal(t, "bar", recorder.Header().Get("X-Foo"), "missing header")
})
}
}

View File

@@ -0,0 +1,62 @@
package pipelining
import (
"bufio"
"net"
"net/http"
)
// Pipelining returns a middleware
type Pipelining struct {
next http.Handler
}
// NewPipelining returns a new Pipelining instance
func NewPipelining(next http.Handler) *Pipelining {
return &Pipelining{
next: next,
}
}
func (p *Pipelining) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
// https://github.com/golang/go/blob/3d59583836630cf13ec4bfbed977d27b1b7adbdc/src/net/http/server.go#L201-L218
if r.Method == http.MethodPut || r.Method == http.MethodPost {
p.next.ServeHTTP(rw, r)
} else {
p.next.ServeHTTP(&writerWithoutCloseNotify{rw}, r)
}
}
// writerWithoutCloseNotify helps to disable closeNotify
type writerWithoutCloseNotify struct {
W http.ResponseWriter
}
// Header returns the response headers.
func (w *writerWithoutCloseNotify) Header() http.Header {
return w.W.Header()
}
// Write writes the data to the connection as part of an HTTP reply.
func (w *writerWithoutCloseNotify) Write(buf []byte) (int, error) {
return w.W.Write(buf)
}
// WriteHeader sends an HTTP response header with the provided
// status code.
func (w *writerWithoutCloseNotify) WriteHeader(code int) {
w.W.WriteHeader(code)
}
// Flush sends any buffered data to the client.
func (w *writerWithoutCloseNotify) Flush() {
if f, ok := w.W.(http.Flusher); ok {
f.Flush()
}
}
// Hijack hijacks the connection.
func (w *writerWithoutCloseNotify) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return w.W.(http.Hijacker).Hijack()
}

View File

@@ -0,0 +1,69 @@
package pipelining
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
type recorderWithCloseNotify struct {
*httptest.ResponseRecorder
}
func (r *recorderWithCloseNotify) CloseNotify() <-chan bool {
panic("implement me")
}
func TestNewPipelining(t *testing.T) {
testCases := []struct {
desc string
HTTPMethod string
implementCloseNotifier bool
}{
{
desc: "should not implement CloseNotifier with GET method",
HTTPMethod: http.MethodGet,
implementCloseNotifier: false,
},
{
desc: "should implement CloseNotifier with PUT method",
HTTPMethod: http.MethodPut,
implementCloseNotifier: true,
},
{
desc: "should implement CloseNotifier with POST method",
HTTPMethod: http.MethodPost,
implementCloseNotifier: true,
},
{
desc: "should not implement CloseNotifier with GET method",
HTTPMethod: http.MethodHead,
implementCloseNotifier: false,
},
{
desc: "should not implement CloseNotifier with PROPFIND method",
HTTPMethod: "PROPFIND",
implementCloseNotifier: false,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, ok := w.(http.CloseNotifier)
assert.Equal(t, test.implementCloseNotifier, ok)
w.WriteHeader(http.StatusOK)
})
handler := NewPipelining(nextHandler)
req := httptest.NewRequest(test.HTTPMethod, "http://localhost", nil)
handler.ServeHTTP(&recorderWithCloseNotify{httptest.NewRecorder()}, req)
})
}
}

View File

@@ -69,27 +69,27 @@ pages:
- 'Logs': 'configuration/logs.md'
- 'EntryPoints': 'configuration/entrypoints.md'
- 'Let''s Encrypt': 'configuration/acme.md'
- 'Backend: Web': 'configuration/backends/web.md'
- 'Backend: BoltDB': 'configuration/backends/boltdb.md'
- 'Backend: Consul': 'configuration/backends/consul.md'
- 'Backend: Consul Catalog': 'configuration/backends/consulcatalog.md'
- 'Backend: Docker': 'configuration/backends/docker.md'
- 'Backend: DynamoDB': 'configuration/backends/dynamodb.md'
- 'Backend: ECS': 'configuration/backends/ecs.md'
- 'Backend: Etcd': 'configuration/backends/etcd.md'
- 'Backend: Eureka': 'configuration/backends/eureka.md'
- 'Backend: File': 'configuration/backends/file.md'
- 'Backend: Kubernetes Ingress': 'configuration/backends/kubernetes.md'
- 'Backend: Marathon': 'configuration/backends/marathon.md'
- 'Backend: Mesos': 'configuration/backends/mesos.md'
- 'Backend: Rancher': 'configuration/backends/rancher.md'
- 'Backend: Rest': 'configuration/backends/rest.md'
- 'Backend: Azure Service Fabric': 'configuration/backends/servicefabric.md'
- 'Backend: Zookeeper': 'configuration/backends/zookeeper.md'
- 'API / Dashboard': 'configuration/api.md'
- 'BoltDB': 'configuration/backends/boltdb.md'
- 'Consul': 'configuration/backends/consul.md'
- 'Consul Catalog': 'configuration/backends/consulcatalog.md'
- 'Docker': 'configuration/backends/docker.md'
- 'DynamoDB': 'configuration/backends/dynamodb.md'
- 'ECS': 'configuration/backends/ecs.md'
- 'Etcd': 'configuration/backends/etcd.md'
- 'Eureka': 'configuration/backends/eureka.md'
- 'File': 'configuration/backends/file.md'
- 'Kubernetes Ingress': 'configuration/backends/kubernetes.md'
- 'Marathon': 'configuration/backends/marathon.md'
- 'Mesos': 'configuration/backends/mesos.md'
- 'Rancher': 'configuration/backends/rancher.md'
- 'Rest': 'configuration/backends/rest.md'
- 'Azure Service Fabric': 'configuration/backends/servicefabric.md'
- 'Zookeeper': 'configuration/backends/zookeeper.md'
- 'Ping': 'configuration/ping.md'
- 'Metrics': 'configuration/metrics.md'
- 'Tracing': 'configuration/tracing.md'
- 'Web (Deprecated)': 'configuration/backends/web.md'
- User Guides:
- 'Configuration Examples': 'user-guide/examples.md'
- 'Swarm Mode Cluster': 'user-guide/swarm-mode.md'
@@ -101,4 +101,3 @@ pages:
- 'Clustering/HA': 'user-guide/cluster.md'
- 'gRPC Example': 'user-guide/grpc.md'
- 'Traefik cluster example with Swarm': 'user-guide/cluster-docker-consul.md'
- Benchmarks: benchmarks.md

View File

@@ -7,7 +7,7 @@ import (
"crypto/x509"
"github.com/containous/traefik/log"
acme "github.com/xenolf/lego/acmev2"
"github.com/xenolf/lego/acme"
)
// Account is used to store lets encrypt registration info

View File

@@ -8,7 +8,7 @@ import (
"github.com/containous/flaeg"
"github.com/containous/traefik/log"
"github.com/containous/traefik/safe"
acme "github.com/xenolf/lego/acmev2"
"github.com/xenolf/lego/acme"
)
func dnsOverrideDelay(delay flaeg.Duration) error {
@@ -34,15 +34,9 @@ func getTokenValue(token, domain string, store Store) []byte {
var result []byte
operation := func() error {
var ok bool
httpChallenges, err := store.GetHTTPChallenges()
if err != nil {
return fmt.Errorf("HTTPChallenges not available : %s", err)
}
if result, ok = httpChallenges[token][domain]; !ok {
return fmt.Errorf("cannot find challenge for token %v", token)
}
return nil
var err error
result, err = store.GetHTTPChallengeToken(token, domain)
return err
}
notify := func(err error, time time.Duration) {
@@ -60,40 +54,9 @@ func getTokenValue(token, domain string, store Store) []byte {
}
func presentHTTPChallenge(domain, token, keyAuth string, store Store) error {
httpChallenges, err := store.GetHTTPChallenges()
if err != nil {
return fmt.Errorf("unable to get HTTPChallenges : %s", err)
}
if httpChallenges == nil {
httpChallenges = map[string]map[string][]byte{}
}
if _, ok := httpChallenges[token]; !ok {
httpChallenges[token] = map[string][]byte{}
}
httpChallenges[token][domain] = []byte(keyAuth)
return store.SaveHTTPChallenges(httpChallenges)
return store.SetHTTPChallengeToken(token, domain, []byte(keyAuth))
}
func cleanUpHTTPChallenge(domain, token string, store Store) error {
httpChallenges, err := store.GetHTTPChallenges()
if err != nil {
return fmt.Errorf("unable to get HTTPChallenges : %s", err)
}
log.Debugf("Challenge CleanUp for domain %s", domain)
if _, ok := httpChallenges[token]; ok {
if _, domainOk := httpChallenges[token][domain]; domainOk {
delete(httpChallenges[token], domain)
}
if len(httpChallenges[token]) == 0 {
delete(httpChallenges, token)
}
return store.SaveHTTPChallenges(httpChallenges)
}
return nil
return store.RemoveHTTPChallengeToken(token, domain)
}

View File

@@ -2,9 +2,11 @@ package acme
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"regexp"
"sync"
"github.com/containous/traefik/log"
"github.com/containous/traefik/safe"
@@ -17,11 +19,12 @@ type LocalStore struct {
filename string
storedData *StoredData
SaveDataChan chan *StoredData `json:"-"`
lock sync.RWMutex
}
// NewLocalStore initializes a new LocalStore with a file name
func NewLocalStore(filename string) LocalStore {
store := LocalStore{filename: filename, SaveDataChan: make(chan *StoredData)}
func NewLocalStore(filename string) *LocalStore {
store := &LocalStore{filename: filename, SaveDataChan: make(chan *StoredData)}
store.listenSaveAction()
return store
}
@@ -60,6 +63,7 @@ func (s *LocalStore) get() (*StoredData, error) {
return nil, err
}
if isOldRegistration {
log.Debug("Reset ACME account.")
s.storedData.Account = nil
s.SaveDataChan <- s.storedData
}
@@ -148,13 +152,59 @@ func (s *LocalStore) SaveCertificates(certificates []*Certificate) error {
return nil
}
// GetHTTPChallenges returns ACME HTTP Challenges list
func (s *LocalStore) GetHTTPChallenges() (map[string]map[string][]byte, error) {
return s.storedData.HTTPChallenges, nil
// GetHTTPChallengeToken Get the http challenge token from the store
func (s *LocalStore) GetHTTPChallengeToken(token, domain string) ([]byte, error) {
s.lock.RLock()
defer s.lock.RUnlock()
if s.storedData.HTTPChallenges == nil {
s.storedData.HTTPChallenges = map[string]map[string][]byte{}
}
if _, ok := s.storedData.HTTPChallenges[token]; !ok {
return nil, fmt.Errorf("cannot find challenge for token %v", token)
}
result, ok := s.storedData.HTTPChallenges[token][domain]
if !ok {
return nil, fmt.Errorf("cannot find challenge for token %v", token)
}
return result, nil
}
// SaveHTTPChallenges stores ACME HTTP Challenges list
func (s *LocalStore) SaveHTTPChallenges(httpChallenges map[string]map[string][]byte) error {
s.storedData.HTTPChallenges = httpChallenges
// SetHTTPChallengeToken Set the http challenge token in the store
func (s *LocalStore) SetHTTPChallengeToken(token, domain string, keyAuth []byte) error {
s.lock.Lock()
defer s.lock.Unlock()
if s.storedData.HTTPChallenges == nil {
s.storedData.HTTPChallenges = map[string]map[string][]byte{}
}
if _, ok := s.storedData.HTTPChallenges[token]; !ok {
s.storedData.HTTPChallenges[token] = map[string][]byte{}
}
s.storedData.HTTPChallenges[token][domain] = []byte(keyAuth)
return nil
}
// RemoveHTTPChallengeToken Remove the http challenge token in the store
func (s *LocalStore) RemoveHTTPChallengeToken(token, domain string) error {
s.lock.Lock()
defer s.lock.Unlock()
if s.storedData.HTTPChallenges == nil {
return nil
}
if _, ok := s.storedData.HTTPChallenges[token]; ok {
if _, domainOk := s.storedData.HTTPChallenges[token][domain]; domainOk {
delete(s.storedData.HTTPChallenges[token], domain)
}
if len(s.storedData.HTTPChallenges[token]) == 0 {
delete(s.storedData.HTTPChallenges, token)
}
}
return nil
}

View File

@@ -8,7 +8,6 @@ import (
fmtlog "log"
"net"
"net/http"
"os"
"reflect"
"strings"
"sync"
@@ -22,8 +21,11 @@ import (
"github.com/containous/traefik/safe"
traefikTLS "github.com/containous/traefik/tls"
"github.com/containous/traefik/types"
"github.com/containous/traefik/version"
"github.com/pkg/errors"
acme "github.com/xenolf/lego/acmev2"
"github.com/sirupsen/logrus"
"github.com/xenolf/lego/acme"
legolog "github.com/xenolf/lego/log"
"github.com/xenolf/lego/providers/dns"
)
@@ -61,6 +63,8 @@ type Provider struct {
clientMutex sync.Mutex
configFromListenerChan chan types.Configuration
pool *safe.Pool
resolvingDomains map[string]struct{}
resolvingDomainsMutex sync.RWMutex
}
// Certificate is a struct which contains all data needed from an ACME certificate
@@ -97,10 +101,11 @@ func (p *Provider) SetConfigListenerChan(configFromListenerChan chan types.Confi
}
func (p *Provider) init() error {
acme.UserAgent = fmt.Sprintf("containous-traefik/%s", version.Version)
if p.ACMELogging {
acme.Logger = fmtlog.New(os.Stderr, "legolog: ", fmtlog.LstdFlags)
legolog.Logger = fmtlog.New(log.WriterLevel(logrus.DebugLevel), "legolog: ", 0)
} else {
acme.Logger = fmtlog.New(ioutil.Discard, "", 0)
legolog.Logger = fmtlog.New(ioutil.Discard, "", 0)
}
var err error
@@ -114,11 +119,19 @@ func (p *Provider) init() error {
return fmt.Errorf("unable to get ACME account : %v", err)
}
// Reset Account if caServer changed, thus registration URI can be updated
if p.account != nil && p.account.Registration != nil && !strings.HasPrefix(p.account.Registration.URI, p.CAServer) {
p.account = nil
}
p.certificates, err = p.Store.GetCertificates()
if err != nil {
return fmt.Errorf("unable to get ACME certificates : %v", err)
}
// Init the currently resolved domain map
p.resolvingDomains = make(map[string]struct{})
p.watchCertificate()
p.watchNewDomains()
@@ -218,6 +231,9 @@ func (p *Provider) resolveCertificate(domain types.Domain, domainFromConfigurati
return nil, nil
}
p.addResolvingDomains(uncheckedDomains)
defer p.removeResolvingDomains(uncheckedDomains)
log.Debugf("Loading ACME certificates %+v...", uncheckedDomains)
client, err := p.getClient()
if err != nil {
@@ -226,9 +242,9 @@ func (p *Provider) resolveCertificate(domain types.Domain, domainFromConfigurati
bundle := true
certificate, failures := client.ObtainCertificate(uncheckedDomains, bundle, nil, OSCPMustStaple)
if len(failures) > 0 {
return nil, fmt.Errorf("cannot obtain certificates %+v", failures)
certificate, err := client.ObtainCertificate(uncheckedDomains, bundle, nil, OSCPMustStaple)
if err != nil {
return nil, fmt.Errorf("cannot obtain certificates: %+v", err)
}
if len(certificate.Certificate) == 0 || len(certificate.PrivateKey) == 0 {
@@ -243,7 +259,25 @@ func (p *Provider) resolveCertificate(domain types.Domain, domainFromConfigurati
}
p.addCertificateForDomain(domain, certificate.Certificate, certificate.PrivateKey)
return &certificate, nil
return certificate, nil
}
func (p *Provider) removeResolvingDomains(resolvingDomains []string) {
p.resolvingDomainsMutex.Lock()
defer p.resolvingDomainsMutex.Unlock()
for _, domain := range resolvingDomains {
delete(p.resolvingDomains, domain)
}
}
func (p *Provider) addResolvingDomains(resolvingDomains []string) {
p.resolvingDomainsMutex.Lock()
defer p.resolvingDomainsMutex.Unlock()
for _, domain := range resolvingDomains {
p.resolvingDomains[domain] = struct{}{}
}
}
func (p *Provider) getClient() (*acme.Client, error) {
@@ -315,7 +349,6 @@ func (p *Provider) getClient() (*acme.Client, error) {
}
p.client = client
}
return p.client, nil
}
@@ -496,6 +529,9 @@ func (p *Provider) AddRoutes(router *mux.Router) {
// Get provided certificate which check a domains list (Main and SANs)
// from static and dynamic provided certificates
func (p *Provider) getUncheckedDomains(domainsToCheck []string, checkConfigurationDomains bool) []string {
p.resolvingDomainsMutex.RLock()
defer p.resolvingDomainsMutex.RUnlock()
log.Debugf("Looking for provided certificate(s) to validate %q...", domainsToCheck)
var allCerts []string
@@ -516,6 +552,11 @@ func (p *Provider) getUncheckedDomains(domainsToCheck []string, checkConfigurati
allCerts = append(allCerts, strings.Join(certificate.Domain.ToStrArray(), ","))
}
// Get currently resolved domains
for domain := range p.resolvingDomains {
allCerts = append(allCerts, domain)
}
// Get Configuration Domains
if checkConfigurationDomains {
for i := 0; i < len(p.Domains); i++ {
@@ -533,8 +574,9 @@ func searchUncheckedDomains(domainsToCheck []string, existentDomains []string) [
uncheckedDomains = append(uncheckedDomains, domainToCheck)
}
}
if len(uncheckedDomains) == 0 {
log.Debugf("No ACME certificate to generate for domains %q.", domainsToCheck)
log.Debugf("No ACME certificate generation required for domains %q.", domainsToCheck)
} else {
log.Debugf("Domains %q need ACME certificates generation for domains %q.", domainsToCheck, strings.Join(uncheckedDomains, ","))
}

View File

@@ -26,6 +26,7 @@ func TestGetUncheckedCertificates(t *testing.T) {
desc string
dynamicCerts *safe.Safe
staticCerts map[string]*tls.Certificate
resolvingDomains map[string]struct{}
acmeCertificates []*Certificate
domains []string
expectedDomains []string
@@ -138,17 +139,55 @@ func TestGetUncheckedCertificates(t *testing.T) {
},
expectedDomains: []string{"traefik.wtf"},
},
{
desc: "all domains already managed by ACME",
domains: []string{"traefik.wtf", "foo.traefik.wtf"},
resolvingDomains: map[string]struct{}{
"traefik.wtf": {},
"foo.traefik.wtf": {},
},
expectedDomains: []string{},
},
{
desc: "one domain already managed by ACME",
domains: []string{"traefik.wtf", "foo.traefik.wtf"},
resolvingDomains: map[string]struct{}{
"traefik.wtf": {},
},
expectedDomains: []string{"foo.traefik.wtf"},
},
{
desc: "wildcard domain already managed by ACME checks the domains",
domains: []string{"bar.traefik.wtf", "foo.traefik.wtf"},
resolvingDomains: map[string]struct{}{
"*.traefik.wtf": {},
},
expectedDomains: []string{},
},
{
desc: "wildcard domain already managed by ACME checks domains and another domain checks one other domain, one domain still unchecked",
domains: []string{"traefik.wtf", "bar.traefik.wtf", "foo.traefik.wtf", "acme.wtf"},
resolvingDomains: map[string]struct{}{
"*.traefik.wtf": {},
"traefik.wtf": {},
},
expectedDomains: []string{"acme.wtf"},
},
}
for _, test := range testCases {
test := test
if test.resolvingDomains == nil {
test.resolvingDomains = make(map[string]struct{})
}
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
acmeProvider := Provider{
dynamicCerts: test.dynamicCerts,
staticCerts: test.staticCerts,
certificates: test.acmeCertificates,
dynamicCerts: test.dynamicCerts,
staticCerts: test.staticCerts,
certificates: test.acmeCertificates,
resolvingDomains: test.resolvingDomains,
}
domains := acmeProvider.getUncheckedDomains(test.domains, false)

View File

@@ -13,6 +13,7 @@ type Store interface {
SaveAccount(*Account) error
GetCertificates() ([]*Certificate, error)
SaveCertificates([]*Certificate) error
GetHTTPChallenges() (map[string]map[string][]byte, error)
SaveHTTPChallenges(map[string]map[string][]byte) error
GetHTTPChallengeToken(token, domain string) ([]byte, error)
SetHTTPChallengeToken(token, domain string, keyAuth []byte) error
RemoveHTTPChallengeToken(token, domain string) error
}

View File

@@ -5,6 +5,7 @@ import (
"crypto/sha1"
"encoding/base64"
"fmt"
"net"
"sort"
"strconv"
"strings"
@@ -116,7 +117,7 @@ func (p *Provider) getServer(node *api.ServiceEntry) types.Server {
address := getBackendAddress(node)
return types.Server{
URL: fmt.Sprintf("%s://%s:%d", scheme, address, node.Service.Port),
URL: fmt.Sprintf("%s://%s", scheme, net.JoinHostPort(address, strconv.Itoa(node.Service.Port))),
Weight: p.getWeight(node.Service.Tags),
}
}

View File

@@ -113,6 +113,97 @@ func TestProviderBuildConfiguration(t *testing.T) {
},
},
},
{
desc: "Should build config containing one frontend, one IPv4 and one IPv6 backend",
nodes: []catalogUpdate{
{
Service: &serviceUpdate{
ServiceName: "test",
Attributes: []string{
"random.foo=bar",
label.TraefikBackendLoadBalancerMethod + "=drr",
label.TraefikBackendCircuitBreakerExpression + "=NetworkErrorRatio() > 0.5",
label.TraefikBackendMaxConnAmount + "=1000",
label.TraefikBackendMaxConnExtractorFunc + "=client.ip",
label.TraefikFrontendAuthBasic + "=test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
},
},
Nodes: []*api.ServiceEntry{
{
Service: &api.AgentService{
Service: "test",
Address: "127.0.0.1",
Port: 80,
Tags: []string{
"random.foo=bar",
label.Prefix + "backend.weight=42", // Deprecated label
label.TraefikFrontendPassHostHeader + "=true",
label.TraefikProtocol + "=https",
},
},
Node: &api.Node{
Node: "localhost",
Address: "127.0.0.1",
},
},
{
Service: &api.AgentService{
Service: "test",
Address: "::1",
Port: 80,
Tags: []string{
"random.foo=bar",
label.Prefix + "backend.weight=42", // Deprecated label
label.TraefikFrontendPassHostHeader + "=true",
label.TraefikProtocol + "=https",
},
},
Node: &api.Node{
Node: "localhost",
Address: "::1",
},
},
},
},
},
expectedFrontends: map[string]*types.Frontend{
"frontend-test": {
Backend: "backend-test",
PassHostHeader: true,
Routes: map[string]types.Route{
"route-host-test": {
Rule: "Host:test.localhost",
},
},
EntryPoints: []string{},
BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
},
},
expectedBackends: map[string]*types.Backend{
"backend-test": {
Servers: map[string]types.Server{
"test-0-us4-27hAOu2ARV7nNrmv6GoKlcA": {
URL: "https://127.0.0.1:80",
Weight: 42,
},
"test-1-Gh4zrXo5flAAz1A8LAEHm1-TSnE": {
URL: "https://[::1]:80",
Weight: 42,
},
},
LoadBalancer: &types.LoadBalancer{
Method: "drr",
},
CircuitBreaker: &types.CircuitBreaker{
Expression: "NetworkErrorRatio() > 0.5",
},
MaxConn: &types.MaxConn{
Amount: 1000,
ExtractorFunc: "client.ip",
},
},
},
},
}
for _, test := range testCases {

View File

@@ -1,7 +1,7 @@
package consulcatalog
import (
"errors"
"fmt"
"strconv"
"strings"
"sync"
@@ -154,14 +154,8 @@ func (p *Provider) watch(configurationChan chan<- types.ConfigMessage, stop chan
defer close(stopCh)
defer close(watchCh)
for {
select {
case <-stop:
return nil
case index, ok := <-watchCh:
if !ok {
return errors.New("consul service list nil")
}
safe.Go(func() {
for index := range watchCh {
log.Debug("List of services changed")
nodes, err := p.getNodes(index)
if err != nil {
@@ -172,6 +166,13 @@ func (p *Provider) watch(configurationChan chan<- types.ConfigMessage, stop chan
ProviderName: "consul_catalog",
Configuration: configuration,
}
}
})
for {
select {
case <-stop:
return nil
case err := <-errorCh:
return err
}
@@ -255,7 +256,8 @@ func (p *Provider) watchHealthState(stopCh <-chan struct{}, watchCh chan<- map[s
safe.Go(func() {
// variable to hold previous state
var flashback []string
var flashback map[string][]string
var flashbackMaintenance []string
options := &api.QueryOptions{WaitTime: DefaultWatchWaitTime}
@@ -267,19 +269,31 @@ func (p *Provider) watchHealthState(stopCh <-chan struct{}, watchCh chan<- map[s
}
// Listening to changes that leads to `passing` state or degrades from it.
healthyState, meta, err := health.State("passing", options)
healthyState, meta, err := health.State("any", options)
if err != nil {
log.WithError(err).Error("Failed to retrieve health checks")
notifyError(err)
return
}
var current []string
var current = make(map[string][]string)
var currentFailing = make(map[string]*api.HealthCheck)
var maintenance []string
if healthyState != nil {
for _, healthy := range healthyState {
current = append(current, healthy.ServiceID)
key := fmt.Sprintf("%s-%s", healthy.Node, healthy.ServiceID)
_, failing := currentFailing[key]
if healthy.Status == "passing" && !failing {
current[key] = append(current[key], healthy.Node)
} else if strings.HasPrefix(healthy.CheckID, "_service_maintenance") || strings.HasPrefix(healthy.CheckID, "_node_maintenance") {
maintenance = append(maintenance, healthy.CheckID)
} else {
currentFailing[key] = healthy
if _, ok := current[key]; ok {
delete(current, key)
}
}
}
}
// If LastIndex didn't change then it means `Get` returned
@@ -302,18 +316,26 @@ func (p *Provider) watchHealthState(stopCh <-chan struct{}, watchCh chan<- map[s
// A critical note is that the return of a blocking request is no guarantee of a change.
// It is possible that there was an idempotent write that does not affect the result of the query.
// Thus it is required to do extra check for changes...
addedKeys, removedKeys := getChangedStringKeys(current, flashback)
addedKeys, removedKeys, changedKeys := getChangedHealth(current, flashback)
if len(addedKeys) > 0 || len(removedKeys) > 0 || len(changedKeys) > 0 {
log.WithField("DiscoveredServices", addedKeys).
WithField("MissingServices", removedKeys).
WithField("ChangedServices", changedKeys).
Debug("Health State change detected.")
if len(addedKeys) > 0 {
log.WithField("DiscoveredServices", addedKeys).Debug("Health State change detected.")
watchCh <- data
flashback = current
}
flashbackMaintenance = maintenance
} else {
addedKeysMaintenance, removedMaintenance := getChangedStringKeys(maintenance, flashbackMaintenance)
if len(removedKeys) > 0 {
log.WithField("MissingServices", removedKeys).Debug("Health State change detected.")
watchCh <- data
flashback = current
if len(addedKeysMaintenance) > 0 || len(removedMaintenance) > 0 {
log.WithField("MaintenanceMode", maintenance).Debug("Maintenance change detected.")
watchCh <- data
flashback = current
flashbackMaintenance = maintenance
}
}
}
}
@@ -394,6 +416,27 @@ func getChangedStringKeys(currState []string, prevState []string) ([]string, []s
return fun.Keys(addedKeys).([]string), fun.Keys(removedKeys).([]string)
}
func getChangedHealth(current map[string][]string, previous map[string][]string) ([]string, []string, []string) {
currKeySet := fun.Set(fun.Keys(current).([]string)).(map[string]bool)
prevKeySet := fun.Set(fun.Keys(previous).([]string)).(map[string]bool)
addedKeys := fun.Difference(currKeySet, prevKeySet).(map[string]bool)
removedKeys := fun.Difference(prevKeySet, currKeySet).(map[string]bool)
var changedKeys []string
for key, value := range current {
if prevValue, ok := previous[key]; ok {
addedNodesKeys, removedNodesKeys := getChangedStringKeys(value, prevValue)
if len(addedNodesKeys) > 0 || len(removedNodesKeys) > 0 {
changedKeys = append(changedKeys, key)
}
}
}
return fun.Keys(addedKeys).([]string), fun.Keys(removedKeys).([]string), changedKeys
}
func getChangedIntKeys(currState []int, prevState []int) ([]int, []int) {
currKeySet := fun.Set(currState).(map[int]bool)
prevKeySet := fun.Set(prevState).(map[int]bool)

View File

@@ -2,7 +2,10 @@ package docker
import (
"context"
"crypto/md5"
"encoding/hex"
"fmt"
"net"
"strconv"
"strings"
"text/template"
@@ -106,13 +109,11 @@ func (p *Provider) buildConfigurationV2(containersInspected []dockerData) *types
}
func getServiceNameKey(container dockerData, swarmMode bool, segmentName string) string {
serviceNameKey := container.ServiceName
if values, err := label.GetStringMultipleStrict(container.Labels, labelDockerComposeProject, labelDockerComposeService); !swarmMode && err == nil {
serviceNameKey = values[labelDockerComposeService] + values[labelDockerComposeProject]
if swarmMode {
return container.ServiceName + segmentName
}
return serviceNameKey + segmentName
return getServiceName(container) + segmentName
}
func (p *Provider) containerFilter(container dockerData) bool {
@@ -169,7 +170,7 @@ func checkSegmentPort(labels map[string]string, segmentName string) error {
func (p *Provider) getFrontendName(container dockerData, idx int) string {
var name string
if len(container.SegmentName) > 0 {
name = getBackendName(container)
name = container.SegmentName + "-" + getBackendName(container)
} else {
name = p.getFrontendRule(container, container.SegmentLabels) + "-" + strconv.Itoa(idx)
}
@@ -261,12 +262,21 @@ func isBackendLBSwarm(container dockerData) bool {
return label.GetBoolValue(container.Labels, labelBackendLoadBalancerSwarm, false)
}
func getSegmentBackendName(container dockerData) string {
if value := label.GetStringValue(container.SegmentLabels, label.TraefikBackend, ""); len(value) > 0 {
return provider.Normalize(container.ServiceName + "-" + value)
func getBackendName(container dockerData) string {
if len(container.SegmentName) > 0 {
return getSegmentBackendName(container)
}
return provider.Normalize(container.ServiceName + "-" + getDefaultBackendName(container) + "-" + container.SegmentName)
return getDefaultBackendName(container)
}
func getSegmentBackendName(container dockerData) string {
serviceName := getServiceName(container)
if value := label.GetStringValue(container.SegmentLabels, label.TraefikBackend, ""); len(value) > 0 {
return provider.Normalize(serviceName + "-" + value)
}
return provider.Normalize(serviceName + "-" + container.SegmentName)
}
func getDefaultBackendName(container dockerData) string {
@@ -274,19 +284,17 @@ func getDefaultBackendName(container dockerData) string {
return provider.Normalize(value)
}
if values, err := label.GetStringMultipleStrict(container.Labels, labelDockerComposeProject, labelDockerComposeService); err == nil {
return provider.Normalize(values[labelDockerComposeService] + "_" + values[labelDockerComposeProject])
}
return provider.Normalize(container.ServiceName)
return provider.Normalize(getServiceName(container))
}
func getBackendName(container dockerData) string {
if len(container.SegmentName) > 0 {
return getSegmentBackendName(container)
func getServiceName(container dockerData) string {
serviceName := container.ServiceName
if values, err := label.GetStringMultipleStrict(container.Labels, labelDockerComposeProject, labelDockerComposeService); err == nil {
serviceName = values[labelDockerComposeService] + "_" + values[labelDockerComposeProject]
}
return getDefaultBackendName(container)
return serviceName
}
func getPort(container dockerData) string {
@@ -316,7 +324,7 @@ func getPort(container dockerData) string {
func (p *Provider) getServers(containers []dockerData) map[string]types.Server {
var servers map[string]types.Server
for i, container := range containers {
for _, 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)
@@ -330,16 +338,30 @@ func (p *Provider) getServers(containers []dockerData) map[string]types.Server {
protocol := label.GetStringValue(container.SegmentLabels, label.TraefikProtocol, label.DefaultProtocol)
port := getPort(container)
serverName := "server-" + container.SegmentName + "-" + container.Name
if len(container.SegmentName) > 0 {
serverName += "-" + strconv.Itoa(i)
serverURL := fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(ip, port))
serverName := getServerName(container.Name, serverURL)
if _, exist := servers[serverName]; exist {
log.Debugf("Skipping server %q with the same URL.", serverName)
continue
}
servers[provider.Normalize(serverName)] = types.Server{
URL: fmt.Sprintf("%s://%s:%s", protocol, ip, port),
servers[serverName] = types.Server{
URL: serverURL,
Weight: label.GetIntValue(container.SegmentLabels, label.TraefikWeight, label.DefaultWeight),
}
}
return servers
}
func getServerName(containerName, url string) string {
hash := md5.New()
_, err := hash.Write([]byte(url))
if err != nil {
// Impossible case
log.Errorf("Fail to hash server URL %q", url)
}
return provider.Normalize("server-" + containerName + "-" + hex.EncodeToString(hash.Sum(nil)))
}

View File

@@ -55,7 +55,7 @@ func TestDockerBuildConfiguration(t *testing.T) {
expectedBackends: map[string]*types.Backend{
"backend-test": {
Servers: map[string]types.Server{
"server-test": {
"server-test-842895ca2aca17f6ee36ddb2f621194d": {
URL: "http://127.0.0.1:80",
Weight: label.DefaultWeight,
},
@@ -270,7 +270,7 @@ func TestDockerBuildConfiguration(t *testing.T) {
expectedBackends: map[string]*types.Backend{
"backend-foobar": {
Servers: map[string]types.Server{
"server-test1": {
"server-test1-7f6444e0dff3330c8b0ad2bbbd383b0f": {
URL: "https://127.0.0.1:666",
Weight: 12,
},
@@ -372,10 +372,11 @@ func TestDockerBuildConfiguration(t *testing.T) {
expectedBackends: map[string]*types.Backend{
"backend-myService-myProject": {
Servers: map[string]types.Server{
"server-test-0": {
"server-test-0-842895ca2aca17f6ee36ddb2f621194d": {
URL: "http://127.0.0.1:80",
Weight: label.DefaultWeight,
}, "server-test-1": {
},
"server-test-1-48093b9fc43454203aacd2bc4057a08c": {
URL: "http://127.0.0.2:80",
Weight: label.DefaultWeight,
},
@@ -384,7 +385,7 @@ func TestDockerBuildConfiguration(t *testing.T) {
},
"backend-myService2-myProject": {
Servers: map[string]types.Server{
"server-test-2": {
"server-test-2-405767e9733427148cd8dae6c4d331b0": {
URL: "http://127.0.0.3:80",
Weight: label.DefaultWeight,
},
@@ -850,8 +851,9 @@ func TestDockerGetFrontendRule(t *testing.T) {
func TestDockerGetBackendName(t *testing.T) {
testCases := []struct {
container docker.ContainerJSON
expected string
container docker.ContainerJSON
segmentName string
expected string
}{
{
container: containerJSON(name("foo")),
@@ -874,6 +876,15 @@ func TestDockerGetBackendName(t *testing.T) {
})),
expected: "bar-foo",
},
{
container: containerJSON(labels(map[string]string{
"com.docker.compose.project": "foo",
"com.docker.compose.service": "bar",
"traefik.sauternes.backend": "titi",
})),
segmentName: "sauternes",
expected: "bar-foo-titi",
},
}
for containerID, test := range testCases {
@@ -883,7 +894,8 @@ func TestDockerGetBackendName(t *testing.T) {
dData := parseContainer(test.container)
segmentProperties := label.ExtractTraefikLabels(dData.Labels)
dData.SegmentLabels = segmentProperties[""]
dData.SegmentLabels = segmentProperties[test.segmentName]
dData.SegmentName = test.segmentName
actual := getBackendName(dData)
assert.Equal(t, test.expected, actual)
@@ -1044,7 +1056,7 @@ func TestDockerGetServers(t *testing.T) {
})),
},
expected: map[string]types.Server{
"server-test1": {
"server-test1-fb00f762970935200c76ccdaf91458f6": {
URL: "http://10.10.10.10:80",
Weight: 1,
},
@@ -1073,15 +1085,15 @@ func TestDockerGetServers(t *testing.T) {
})),
},
expected: map[string]types.Server{
"server-test1": {
"server-test1-743440b6f4a8ffd8737626215f2c5a33": {
URL: "http://10.10.10.11:80",
Weight: 1,
},
"server-test2": {
"server-test2-547f74bbb5da02b6c8141ce9aa96c13b": {
URL: "http://10.10.10.12:81",
Weight: 1,
},
"server-test3": {
"server-test3-c57fd8b848c814a3f2a4a4c12e13c179": {
URL: "http://10.10.10.13:82",
Weight: 1,
},
@@ -1110,11 +1122,11 @@ func TestDockerGetServers(t *testing.T) {
})),
},
expected: map[string]types.Server{
"server-test2": {
"server-test2-547f74bbb5da02b6c8141ce9aa96c13b": {
URL: "http://10.10.10.12:81",
Weight: 1,
},
"server-test3": {
"server-test3-c57fd8b848c814a3f2a4a4c12e13c179": {
URL: "http://10.10.10.13:82",
Weight: 1,
},

View File

@@ -57,7 +57,7 @@ func TestSwarmBuildConfiguration(t *testing.T) {
expectedBackends: map[string]*types.Backend{
"backend-test": {
Servers: map[string]types.Server{
"server-test": {
"server-test-842895ca2aca17f6ee36ddb2f621194d": {
URL: "http://127.0.0.1:80",
Weight: label.DefaultWeight,
},
@@ -238,7 +238,6 @@ func TestSwarmBuildConfiguration(t *testing.T) {
ReferrerPolicy: "foo",
IsDevelopment: true,
},
Errors: map[string]*types.ErrorPage{
"foo": {
Status: []string{"404"},
@@ -276,7 +275,7 @@ func TestSwarmBuildConfiguration(t *testing.T) {
expectedBackends: map[string]*types.Backend{
"backend-foobar": {
Servers: map[string]types.Server{
"server-test1": {
"server-test1-7f6444e0dff3330c8b0ad2bbbd383b0f": {
URL: "https://127.0.0.1:666",
Weight: 12,
},

View File

@@ -42,22 +42,22 @@ func TestSegmentBuildConfiguration(t *testing.T) {
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-foo-foo-sauternes": {
Backend: "backend-foo-foo-sauternes",
"frontend-sauternes-foo-sauternes": {
Backend: "backend-foo-sauternes",
PassHostHeader: true,
EntryPoints: []string{"http", "https"},
BasicAuth: []string{},
Routes: map[string]types.Route{
"route-frontend-foo-foo-sauternes": {
"route-frontend-sauternes-foo-sauternes": {
Rule: "Host:foo.docker.localhost",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-foo-foo-sauternes": {
"backend-foo-sauternes": {
Servers: map[string]types.Server{
"server-sauternes-foo-0": {
"server-foo-863563a2e23c95502862016417ee95ea": {
URL: "http://127.0.0.1:2503",
Weight: label.DefaultWeight,
},
@@ -132,8 +132,8 @@ func TestSegmentBuildConfiguration(t *testing.T) {
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-foo-foo-sauternes": {
Backend: "backend-foo-foo-sauternes",
"frontend-sauternes-foo-sauternes": {
Backend: "backend-foo-sauternes",
EntryPoints: []string{
"http",
"https",
@@ -224,16 +224,16 @@ func TestSegmentBuildConfiguration(t *testing.T) {
},
Routes: map[string]types.Route{
"route-frontend-foo-foo-sauternes": {
"route-frontend-sauternes-foo-sauternes": {
Rule: "Host:foo.docker.localhost",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-foo-foo-sauternes": {
"backend-foo-sauternes": {
Servers: map[string]types.Server{
"server-sauternes-foo-0": {
"server-foo-7f6444e0dff3330c8b0ad2bbbd383b0f": {
URL: "https://127.0.0.1:666",
Weight: 12,
},
@@ -278,7 +278,7 @@ func TestSegmentBuildConfiguration(t *testing.T) {
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-test1-foobar": {
"frontend-sauternes-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: false,
Priority: 5000,
@@ -288,18 +288,18 @@ func TestSegmentBuildConfiguration(t *testing.T) {
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-test1-foobar": {
"route-frontend-sauternes-test1-foobar": {
Rule: "Path:/mypath",
},
},
},
"frontend-test2-test2-anothersauternes": {
Backend: "backend-test2-test2-anothersauternes",
"frontend-anothersauternes-test2-anothersauternes": {
Backend: "backend-test2-anothersauternes",
PassHostHeader: true,
EntryPoints: []string{},
BasicAuth: []string{},
Routes: map[string]types.Route{
"route-frontend-test2-test2-anothersauternes": {
"route-frontend-anothersauternes-test2-anothersauternes": {
Rule: "Path:/anotherpath",
},
},
@@ -308,16 +308,16 @@ func TestSegmentBuildConfiguration(t *testing.T) {
expectedBackends: map[string]*types.Backend{
"backend-test1-foobar": {
Servers: map[string]types.Server{
"server-sauternes-test1-0": {
"server-test1-79533a101142718f0fdf84c42593c41e": {
URL: "https://127.0.0.1:2503",
Weight: 80,
},
},
CircuitBreaker: nil,
},
"backend-test2-test2-anothersauternes": {
"backend-test2-anothersauternes": {
Servers: map[string]types.Server{
"server-anothersauternes-test2-0": {
"server-test2-e9c1b66f9af919aa46053fbc2391bb4a": {
URL: "http://127.0.0.1:8079",
Weight: 33,
},
@@ -326,6 +326,152 @@ func TestSegmentBuildConfiguration(t *testing.T) {
},
},
},
{
desc: "several segments with the same backend name and same port",
containers: []docker.ContainerJSON{
containerJSON(
name("test1"),
labels(map[string]string{
"traefik.port": "2503",
"traefik.protocol": "https",
"traefik.weight": "80",
"traefik.frontend.entryPoints": "http,https",
"traefik.frontend.redirect.entryPoint": "https",
"traefik.sauternes.backend": "foobar",
"traefik.sauternes.frontend.rule": "Path:/sauternes",
"traefik.sauternes.frontend.priority": "5000",
"traefik.arbois.backend": "foobar",
"traefik.arbois.frontend.rule": "Path:/arbois",
"traefik.arbois.frontend.priority": "3000",
}),
ports(nat.PortMap{
"80/tcp": {},
}),
withNetwork("bridge", ipv4("127.0.0.1")),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: true,
Priority: 5000,
EntryPoints: []string{"http", "https"},
BasicAuth: []string{},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-sauternes-test1-foobar": {
Rule: "Path:/sauternes",
},
},
},
"frontend-arbois-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: true,
Priority: 3000,
EntryPoints: []string{"http", "https"},
BasicAuth: []string{},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-arbois-test1-foobar": {
Rule: "Path:/arbois",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-test1-foobar": {
Servers: map[string]types.Server{
"server-test1-79533a101142718f0fdf84c42593c41e": {
URL: "https://127.0.0.1:2503",
Weight: 80,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "several segments with the same backend name and different port (wrong behavior)",
containers: []docker.ContainerJSON{
containerJSON(
name("test1"),
labels(map[string]string{
"traefik.protocol": "https",
"traefik.frontend.entryPoints": "http,https",
"traefik.frontend.redirect.entryPoint": "https",
"traefik.sauternes.port": "2503",
"traefik.sauternes.weight": "80",
"traefik.sauternes.backend": "foobar",
"traefik.sauternes.frontend.rule": "Path:/sauternes",
"traefik.sauternes.frontend.priority": "5000",
"traefik.arbois.port": "2504",
"traefik.arbois.weight": "90",
"traefik.arbois.backend": "foobar",
"traefik.arbois.frontend.rule": "Path:/arbois",
"traefik.arbois.frontend.priority": "3000",
}),
ports(nat.PortMap{
"80/tcp": {},
}),
withNetwork("bridge", ipv4("127.0.0.1")),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: true,
Priority: 5000,
EntryPoints: []string{"http", "https"},
BasicAuth: []string{},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-sauternes-test1-foobar": {
Rule: "Path:/sauternes",
},
},
},
"frontend-arbois-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: true,
Priority: 3000,
EntryPoints: []string{"http", "https"},
BasicAuth: []string{},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-arbois-test1-foobar": {
Rule: "Path:/arbois",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-test1-foobar": {
Servers: map[string]types.Server{
"server-test1-79533a101142718f0fdf84c42593c41e": {
URL: "https://127.0.0.1:2503",
Weight: 80,
},
"server-test1-315a41140f1bd825b066e39686c18482": {
URL: "https://127.0.0.1:2504",
Weight: 90,
},
},
CircuitBreaker: nil,
},
},
},
}
provider := &Provider{

View File

@@ -2,6 +2,7 @@ package ecs
import (
"fmt"
"net"
"strconv"
"strings"
"text/template"
@@ -134,7 +135,7 @@ func getServers(instances []ecsInstance) map[string]types.Server {
serverName := provider.Normalize(fmt.Sprintf("server-%s-%s", instance.Name, instance.ID))
servers[serverName] = types.Server{
URL: fmt.Sprintf("%s://%s:%s", protocol, host, port),
URL: fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(host, port)),
Weight: label.GetIntValue(instance.TraefikLabels, label.TraefikWeight, label.DefaultWeight),
}
}

View File

@@ -205,7 +205,7 @@ func getFuncFirstStringValueV1(labelName string, defaultValue string) func(insta
// Deprecated
func getFuncFirstBoolValueV1(labelName string, defaultValue bool) func(instances []ecsInstance) bool {
return func(instances []ecsInstance) bool {
if len(instances) < 0 {
if len(instances) == 0 {
return defaultValue
}
return getBoolValueV1(instances[0], labelName, defaultValue)

View File

@@ -14,6 +14,7 @@ import (
"github.com/containous/traefik/safe"
"github.com/containous/traefik/tls"
"github.com/containous/traefik/types"
"github.com/pkg/errors"
"gopkg.in/fsnotify.v1"
)
@@ -23,6 +24,7 @@ var _ provider.Provider = (*Provider)(nil)
type Provider struct {
provider.BaseProvider `mapstructure:",squash" export:"true"`
Directory string `description:"Load configuration from one or more .toml files in a directory" export:"true"`
TraefikFile string
}
// Provide allows the file provider to provide configurations to traefik
@@ -37,10 +39,12 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
if p.Watch {
var watchItem string
if p.Directory != "" {
if len(p.Directory) > 0 {
watchItem = p.Directory
} else {
} else if len(p.Filename) > 0 {
watchItem = filepath.Dir(p.Filename)
} else {
watchItem = filepath.Dir(p.TraefikFile)
}
if err := p.addWatcher(pool, watchItem, configurationChan, p.watcherCallback); err != nil {
@@ -55,10 +59,19 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
// BuildConfiguration loads configuration either from file or a directory specified by 'Filename'/'Directory'
// and returns a 'Configuration' object
func (p *Provider) BuildConfiguration() (*types.Configuration, error) {
if p.Directory != "" {
if len(p.Directory) > 0 {
return p.loadFileConfigFromDirectory(p.Directory, nil)
}
return p.loadFileConfig(p.Filename)
if len(p.Filename) > 0 {
return p.loadFileConfig(p.Filename, true)
}
if len(p.TraefikFile) > 0 {
return p.loadFileConfig(p.TraefikFile, false)
}
return nil, errors.New("Error using file configuration backend, no filename defined")
}
func (p *Provider) addWatcher(pool *safe.Pool, directory string, configurationChan chan<- types.ConfigMessage, callback func(chan<- types.ConfigMessage, fsnotify.Event)) error {
@@ -67,6 +80,11 @@ func (p *Provider) addWatcher(pool *safe.Pool, directory string, configurationCh
return fmt.Errorf("error creating file watcher: %s", err)
}
err = watcher.Add(directory)
if err != nil {
return fmt.Errorf("error adding file watcher: %s", err)
}
// Process events
pool.Go(func(stop chan bool) {
defer watcher.Close()
@@ -76,8 +94,15 @@ func (p *Provider) addWatcher(pool *safe.Pool, directory string, configurationCh
return
case evt := <-watcher.Events:
if p.Directory == "" {
var filename string
if len(p.Filename) > 0 {
filename = p.Filename
} else {
filename = p.TraefikFile
}
_, evtFileName := filepath.Split(evt.Name)
_, confFileName := filepath.Split(p.Filename)
_, confFileName := filepath.Split(filename)
if evtFileName == confFileName {
callback(configurationChan, evt)
}
@@ -89,18 +114,15 @@ func (p *Provider) addWatcher(pool *safe.Pool, directory string, configurationCh
}
}
})
err = watcher.Add(directory)
if err != nil {
return fmt.Errorf("error adding file watcher: %s", err)
}
return nil
}
func (p *Provider) watcherCallback(configurationChan chan<- types.ConfigMessage, event fsnotify.Event) {
watchItem := p.Filename
if p.Directory != "" {
watchItem := p.TraefikFile
if len(p.Directory) > 0 {
watchItem = p.Directory
} else if len(p.Filename) > 0 {
watchItem = p.Filename
}
if _, err := os.Stat(watchItem); err != nil {
@@ -136,12 +158,19 @@ func readFile(filename string) (string, error) {
return "", fmt.Errorf("invalid filename: %s", filename)
}
func (p *Provider) loadFileConfig(filename string) (*types.Configuration, error) {
func (p *Provider) loadFileConfig(filename string, parseTemplate bool) (*types.Configuration, error) {
fileContent, err := readFile(filename)
if err != nil {
return nil, fmt.Errorf("error reading configuration file: %s - %s", filename, err)
}
configuration, err := p.CreateConfiguration(fileContent, template.FuncMap{}, false)
var configuration *types.Configuration
if parseTemplate {
configuration, err = p.CreateConfiguration(fileContent, template.FuncMap{}, false)
} else {
configuration, err = p.DecodeConfiguration(fileContent)
}
if err != nil {
return nil, err
}
@@ -182,7 +211,7 @@ func (p *Provider) loadFileConfigFromDirectory(directory string, configuration *
}
var c *types.Configuration
c, err = p.loadFileConfig(path.Join(directory, item.Name()))
c, err = p.loadFileConfig(path.Join(directory, item.Name()), true)
if err != nil {
return configuration, err

View File

@@ -14,216 +14,6 @@ import (
"github.com/stretchr/testify/assert"
)
func TestProvideSingleFileAndWatch(t *testing.T) {
tempDir := createTempDir(t, "testfile")
defer os.RemoveAll(tempDir)
expectedNumFrontends := 2
expectedNumBackends := 2
expectedNumTLSConf := 2
tempFile := createFile(t,
tempDir, "simple.toml",
createFrontendConfiguration(expectedNumFrontends),
createBackendConfiguration(expectedNumBackends),
createTLS(expectedNumTLSConf))
configurationChan, signal := createConfigurationRoutine(t, &expectedNumFrontends, &expectedNumBackends, &expectedNumTLSConf)
provide(configurationChan, watch, withFile(tempFile))
// Wait for initial message to be tested
err := waitForSignal(signal, 2*time.Second, "initial config")
assert.NoError(t, err)
// Now test again with single frontend and backend
expectedNumFrontends = 1
expectedNumBackends = 1
expectedNumTLSConf = 1
createFile(t,
tempDir, "simple.toml",
createFrontendConfiguration(expectedNumFrontends),
createBackendConfiguration(expectedNumBackends),
createTLS(expectedNumTLSConf))
err = waitForSignal(signal, 2*time.Second, "single frontend, backend, TLS configuration")
assert.NoError(t, err)
}
func TestProvideSingleFileAndNotWatch(t *testing.T) {
tempDir := createTempDir(t, "testfile")
defer os.RemoveAll(tempDir)
expectedNumFrontends := 2
expectedNumBackends := 2
expectedNumTLSConf := 2
tempFile := createFile(t,
tempDir, "simple.toml",
createFrontendConfiguration(expectedNumFrontends),
createBackendConfiguration(expectedNumBackends),
createTLS(expectedNumTLSConf))
configurationChan, signal := createConfigurationRoutine(t, &expectedNumFrontends, &expectedNumBackends, &expectedNumTLSConf)
provide(configurationChan, withFile(tempFile))
// Wait for initial message to be tested
err := waitForSignal(signal, 2*time.Second, "initial config")
assert.NoError(t, err)
// Now test again with single frontend and backend
expectedNumFrontends = 1
expectedNumBackends = 1
expectedNumTLSConf = 1
createFile(t,
tempDir, "simple.toml",
createFrontendConfiguration(expectedNumFrontends),
createBackendConfiguration(expectedNumBackends),
createTLS(expectedNumTLSConf))
// Must fail because we don't watch the changes
err = waitForSignal(signal, 2*time.Second, "single frontend, backend and TLS configuration")
assert.Error(t, err)
}
func TestProvideDirectoryAndWatch(t *testing.T) {
tempDir := createTempDir(t, "testdir")
defer os.RemoveAll(tempDir)
expectedNumFrontends := 2
expectedNumBackends := 2
expectedNumTLSConf := 2
tempFile1 := createRandomFile(t, tempDir, createFrontendConfiguration(expectedNumFrontends))
tempFile2 := createRandomFile(t, tempDir, createBackendConfiguration(expectedNumBackends))
tempFile3 := createRandomFile(t, tempDir, createTLS(expectedNumTLSConf))
configurationChan, signal := createConfigurationRoutine(t, &expectedNumFrontends, &expectedNumBackends, &expectedNumTLSConf)
provide(configurationChan, watch, withDirectory(tempDir))
// Wait for initial config message to be tested
err := waitForSignal(signal, 2*time.Second, "initial config")
assert.NoError(t, err)
// Now remove the backends file
expectedNumFrontends = 2
expectedNumBackends = 0
expectedNumTLSConf = 2
os.Remove(tempFile2.Name())
err = waitForSignal(signal, 2*time.Second, "remove the backends file")
assert.NoError(t, err)
// Now remove the frontends file
expectedNumFrontends = 0
expectedNumBackends = 0
expectedNumTLSConf = 2
os.Remove(tempFile1.Name())
err = waitForSignal(signal, 2*time.Second, "remove the frontends file")
assert.NoError(t, err)
// Now remove the TLS configuration file
expectedNumFrontends = 0
expectedNumBackends = 0
expectedNumTLSConf = 0
os.Remove(tempFile3.Name())
err = waitForSignal(signal, 2*time.Second, "remove the TLS configuration file")
assert.NoError(t, err)
}
func TestProvideDirectoryAndNotWatch(t *testing.T) {
tempDir := createTempDir(t, "testdir")
tempTLSDir := createSubDir(t, tempDir, "tls")
defer os.RemoveAll(tempDir)
expectedNumFrontends := 2
expectedNumBackends := 2
expectedNumTLSConf := 2
createRandomFile(t, tempDir, createFrontendConfiguration(expectedNumFrontends))
tempFile2 := createRandomFile(t, tempDir, createBackendConfiguration(expectedNumBackends))
createRandomFile(t, tempTLSDir, createTLS(expectedNumTLSConf))
configurationChan, signal := createConfigurationRoutine(t, &expectedNumFrontends, &expectedNumBackends, &expectedNumTLSConf)
provide(configurationChan, withDirectory(tempDir))
// Wait for initial config message to be tested
err := waitForSignal(signal, 2*time.Second, "initial config")
assert.NoError(t, err)
// Now remove the backends file
expectedNumFrontends = 2
expectedNumBackends = 0
expectedNumTLSConf = 2
os.Remove(tempFile2.Name())
// Must fail because we don't watch the changes
err = waitForSignal(signal, 2*time.Second, "remove the backends file")
assert.Error(t, err)
}
func createConfigurationRoutine(t *testing.T, expectedNumFrontends *int, expectedNumBackends *int, expectedNumTLSes *int) (chan types.ConfigMessage, chan interface{}) {
configurationChan := make(chan types.ConfigMessage)
signal := make(chan interface{})
safe.Go(func() {
for {
data := <-configurationChan
assert.Equal(t, "file", data.ProviderName)
assert.Len(t, data.Configuration.Frontends, *expectedNumFrontends)
assert.Len(t, data.Configuration.Backends, *expectedNumBackends)
assert.Len(t, data.Configuration.TLS, *expectedNumTLSes)
signal <- nil
}
})
return configurationChan, signal
}
func waitForSignal(signal chan interface{}, timeout time.Duration, caseName string) error {
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case <-signal:
case <-timer.C:
return fmt.Errorf("Timed out waiting for assertions to be tested: %s", caseName)
}
return nil
}
func provide(configurationChan chan types.ConfigMessage, builders ...func(p *Provider)) {
pvd := &Provider{}
for _, builder := range builders {
builder(pvd)
}
pvd.Provide(configurationChan, safe.NewPool(context.Background()), nil)
}
func watch(pvd *Provider) {
pvd.Watch = true
}
func withDirectory(name string) func(*Provider) {
return func(pvd *Provider) {
pvd.Directory = name
}
}
func withFile(tempFile *os.File) func(*Provider) {
return func(p *Provider) {
p.Filename = tempFile.Name()
}
}
// createRandomFile Helper
func createRandomFile(t *testing.T, tempDir string, contents ...string) *os.File {
return createFile(t, tempDir, fmt.Sprintf("temp%d.toml", time.Now().UnixNano()), contents...)
@@ -264,25 +54,12 @@ func createTempDir(t *testing.T, dir string) string {
return d
}
// createDir Helper
func createSubDir(t *testing.T, rootDir, dir string) string {
t.Helper()
err := os.Mkdir(rootDir+"/"+dir, 0775)
if err != nil {
t.Fatal(err)
}
return rootDir + "/" + dir
}
// createFrontendConfiguration Helper
func createFrontendConfiguration(n int) string {
conf := "{{$home := env \"HOME\"}}\n[frontends]\n"
conf := "[frontends]\n"
for i := 1; i <= n; i++ {
conf += fmt.Sprintf(` [frontends."frontend%[1]d"]
backend = "backend%[1]d"
`, i)
conf += fmt.Sprintf(` [frontends."frontend%[1]d".headers]
"PublicKey" = "{{$home}}/pub.key"
`, i)
}
return conf
@@ -313,3 +90,240 @@ func createTLS(n int) string {
}
return conf
}
type ProvideTestCase struct {
desc string
directoryContent []string
fileContent string
traefikFileContent string
expectedNumFrontend int
expectedNumBackend int
expectedNumTLSConf int
}
func getTestCases() []ProvideTestCase {
return []ProvideTestCase{
{
desc: "simple file",
fileContent: createFrontendConfiguration(2) + createBackendConfiguration(3) + createTLS(4),
expectedNumFrontend: 2,
expectedNumBackend: 3,
expectedNumTLSConf: 4,
},
{
desc: "simple file and a traefik file",
fileContent: createFrontendConfiguration(2) + createBackendConfiguration(3) + createTLS(4),
traefikFileContent: `
debug=true
`,
expectedNumFrontend: 2,
expectedNumBackend: 3,
expectedNumTLSConf: 4,
},
{
desc: "template file",
fileContent: `
[frontends]
{{ range $i, $e := until 20 }}
[frontends.frontend{{ $e }}]
backend = "backend"
{{ end }}
`,
expectedNumFrontend: 20,
},
{
desc: "simple directory",
directoryContent: []string{
createFrontendConfiguration(2),
createBackendConfiguration(3),
createTLS(4),
},
expectedNumFrontend: 2,
expectedNumBackend: 3,
expectedNumTLSConf: 4,
},
{
desc: "template in directory",
directoryContent: []string{
`
[frontends]
{{ range $i, $e := until 20 }}
[frontends.frontend{{ $e }}]
backend = "backend"
{{ end }}
`,
`
[backends]
{{ range $i, $e := until 20 }}
[backends.backend{{ $e }}]
[backends.backend{{ $e }}.servers.server1]
url="http://127.0.0.1"
{{ end }}
`,
},
expectedNumFrontend: 20,
expectedNumBackend: 20,
},
{
desc: "simple traefik file",
traefikFileContent: `
debug=true
[file]
` + createFrontendConfiguration(2) + createBackendConfiguration(3) + createTLS(4),
expectedNumFrontend: 2,
expectedNumBackend: 3,
expectedNumTLSConf: 4,
},
{
desc: "simple traefik file with templating",
traefikFileContent: `
temp="{{ getTag \"test\" }}"
[file]
` + createFrontendConfiguration(2) + createBackendConfiguration(3) + createTLS(4),
expectedNumFrontend: 2,
expectedNumBackend: 3,
expectedNumTLSConf: 4,
},
}
}
func TestProvideWithoutWatch(t *testing.T) {
for _, test := range getTestCases() {
test := test
t.Run(test.desc+" without watch", func(t *testing.T) {
t.Parallel()
provider, clean := createProvider(t, test, false)
defer clean()
configChan := make(chan types.ConfigMessage)
go func() {
err := provider.Provide(configChan, safe.NewPool(context.Background()), types.Constraints{})
assert.NoError(t, err)
}()
timeout := time.After(time.Second)
select {
case config := <-configChan:
assert.Len(t, config.Configuration.Backends, test.expectedNumBackend)
assert.Len(t, config.Configuration.Frontends, test.expectedNumFrontend)
assert.Len(t, config.Configuration.TLS, test.expectedNumTLSConf)
case <-timeout:
t.Errorf("timeout while waiting for config")
}
})
}
}
func TestProvideWithWatch(t *testing.T) {
for _, test := range getTestCases() {
test := test
t.Run(test.desc+" with watch", func(t *testing.T) {
t.Parallel()
provider, clean := createProvider(t, test, true)
defer clean()
configChan := make(chan types.ConfigMessage)
go func() {
err := provider.Provide(configChan, safe.NewPool(context.Background()), types.Constraints{})
assert.NoError(t, err)
}()
timeout := time.After(time.Second)
select {
case config := <-configChan:
assert.Len(t, config.Configuration.Backends, 0)
assert.Len(t, config.Configuration.Frontends, 0)
assert.Len(t, config.Configuration.TLS, 0)
case <-timeout:
t.Errorf("timeout while waiting for config")
}
if len(test.fileContent) > 0 {
ioutil.WriteFile(provider.Filename, []byte(test.fileContent), 0755)
}
if len(test.traefikFileContent) > 0 {
ioutil.WriteFile(provider.TraefikFile, []byte(test.traefikFileContent), 0755)
}
if len(test.directoryContent) > 0 {
for _, fileContent := range test.directoryContent {
createRandomFile(t, provider.Directory, fileContent)
}
}
timeout = time.After(time.Second * 1)
success := false
for !success {
select {
case config := <-configChan:
success = assert.Len(t, config.Configuration.Backends, test.expectedNumBackend)
success = success && assert.Len(t, config.Configuration.Frontends, test.expectedNumFrontend)
success = success && assert.Len(t, config.Configuration.TLS, test.expectedNumTLSConf)
case <-timeout:
t.Errorf("timeout while waiting for config")
return
}
}
})
}
}
func TestErrorWhenEmptyConfig(t *testing.T) {
provider := &Provider{}
configChan := make(chan types.ConfigMessage)
errorChan := make(chan struct{})
go func() {
err := provider.Provide(configChan, safe.NewPool(context.Background()), types.Constraints{})
assert.Error(t, err)
close(errorChan)
}()
timeout := time.After(time.Second)
select {
case <-configChan:
t.Fatal("We should not receive config message")
case <-timeout:
t.Fatal("timeout while waiting for config")
case <-errorChan:
}
}
func createProvider(t *testing.T, test ProvideTestCase, watch bool) (*Provider, func()) {
tempDir := createTempDir(t, "testdir")
provider := &Provider{}
provider.Watch = watch
if len(test.directoryContent) > 0 {
if !watch {
for _, fileContent := range test.directoryContent {
createRandomFile(t, tempDir, fileContent)
}
}
provider.Directory = tempDir
}
if len(test.fileContent) > 0 {
if watch {
test.fileContent = ""
}
filename := createRandomFile(t, tempDir, test.fileContent)
provider.Filename = filename.Name()
}
if len(test.traefikFileContent) > 0 {
if watch {
test.traefikFileContent = ""
}
filename := createRandomFile(t, tempDir, test.traefikFileContent)
provider.TraefikFile = filename.Name()
}
return provider, func() {
os.Remove(tempDir)
}
}

View File

@@ -6,6 +6,7 @@ import (
"errors"
"flag"
"fmt"
"net"
"os"
"reflect"
"strconv"
@@ -302,7 +303,7 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
for _, subset := range endpoints.Subsets {
for _, address := range subset.Addresses {
url := protocol + "://" + address.IP + ":" + strconv.Itoa(endpointPortNumber(port, subset.Ports))
url := protocol + "://" + net.JoinHostPort(address.IP, strconv.Itoa(endpointPortNumber(port, subset.Ports)))
name := url
if address.TargetRef != nil && address.TargetRef.Name != "" {
name = address.TargetRef.Name

View File

@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"math"
"net"
"strconv"
"strings"
"text/template"
@@ -340,7 +341,7 @@ func (p *Provider) getServer(app appData, task marathon.Task) (string, *types.Se
serverName := provider.Normalize("server-" + app.ID + "-" + task.ID + getSegmentNameSuffix(app.SegmentName))
return serverName, &types.Server{
URL: fmt.Sprintf("%s://%s:%v", protocol, host, port),
URL: fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(host, port)),
Weight: label.GetIntValue(app.SegmentLabels, label.TraefikWeight, label.DefaultWeight),
}, nil
}

View File

@@ -3,6 +3,7 @@ package mesos
import (
"fmt"
"math"
"net"
"strconv"
"strings"
"text/template"
@@ -185,7 +186,7 @@ func (p *Provider) getServers(tasks []taskData) map[string]types.Server {
serverName := "server-" + getID(task)
servers[serverName] = types.Server{
URL: fmt.Sprintf("%s://%s:%s", protocol, host, port),
URL: fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(host, port)),
Weight: getIntValue(task.TraefikLabels, label.TraefikWeight, label.DefaultWeight, math.MaxInt32),
}
}

View File

@@ -62,8 +62,6 @@ func (p *BaseProvider) GetConfiguration(defaultTemplate string, funcMap template
// CreateConfiguration create a provider configuration from content using templating
func (p *BaseProvider) CreateConfiguration(tmplContent string, funcMap template.FuncMap, templateObjects interface{}) (*types.Configuration, error) {
configuration := new(types.Configuration)
var defaultFuncMap = sprig.TxtFuncMap()
// tolower is deprecated in favor of sprig's lower function
defaultFuncMap["tolower"] = strings.ToLower
@@ -91,7 +89,13 @@ func (p *BaseProvider) CreateConfiguration(tmplContent string, funcMap template.
log.Debugf("Template content: %s", tmplContent)
log.Debugf("Rendering results: %s", renderedTemplate)
}
if _, err := toml.Decode(renderedTemplate, configuration); err != nil {
return p.DecodeConfiguration(renderedTemplate)
}
// DecodeConfiguration Decode a *types.Configuration from a content
func (p *BaseProvider) DecodeConfiguration(content string) (*types.Configuration, error) {
configuration := new(types.Configuration)
if _, err := toml.Decode(content, configuration); err != nil {
return nil, err
}
return configuration, nil

View File

@@ -2,6 +2,7 @@ package rancher
import (
"fmt"
"net"
"strconv"
"strings"
"text/template"
@@ -181,7 +182,7 @@ func getServers(service rancherData) map[string]types.Server {
serverName := "server-" + strconv.Itoa(index)
servers[serverName] = types.Server{
URL: fmt.Sprintf("%s://%s:%s", protocol, ip, port),
URL: fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(ip, port)),
Weight: weight,
}
}

View File

@@ -1,4 +1,4 @@
mkdocs>=0.17.3
pymdown-extensions>=1.4
mkdocs-bootswatch>=0.4.0
mkdocs-material>=2.2.6
mkdocs==0.17.5
pymdown-extensions==4.12
mkdocs-bootswatch==0.5.0
mkdocs-material==2.9.4

27
server/bufferpool.go Normal file
View File

@@ -0,0 +1,27 @@
package server
import "sync"
const bufferPoolSize int = 32 * 1024
func newBufferPool() *bufferPool {
return &bufferPool{
pool: sync.Pool{
New: func() interface{} {
return make([]byte, bufferPoolSize)
},
},
}
}
type bufferPool struct {
pool sync.Pool
}
func (b *bufferPool) Get() []byte {
return b.pool.Get().([]byte)
}
func (b *bufferPool) Put(bytes []byte) {
b.pool.Put(bytes)
}

View File

@@ -1,13 +1,21 @@
package server
import (
"context"
"io"
"net"
"net/http"
"github.com/containous/traefik/log"
"github.com/containous/traefik/middlewares"
)
// StatusClientClosedRequest non-standard HTTP status code for client disconnection
const StatusClientClosedRequest = 499
// StatusClientClosedRequestText non-standard HTTP status for client disconnection
const StatusClientClosedRequestText = "Client Closed Request"
// RecordingErrorHandler is an error handler, implementing the vulcand/oxy
// error handler interface, which is recording network errors by using the netErrorRecorder.
// In addition it sets a proper HTTP status code and body, depending on the type of error occurred.
@@ -33,8 +41,18 @@ func (eh *RecordingErrorHandler) ServeHTTP(w http.ResponseWriter, req *http.Requ
} else if err == io.EOF {
eh.netErrorRecorder.Record(req.Context())
statusCode = http.StatusBadGateway
} else if err == context.Canceled {
statusCode = StatusClientClosedRequest
}
w.WriteHeader(statusCode)
w.Write([]byte(http.StatusText(statusCode)))
w.Write([]byte(statusText(statusCode)))
log.Debugf("'%d %s' caused by: %v", statusCode, statusText(statusCode), err)
}
func statusText(statusCode int) string {
if statusCode == StatusClientClosedRequest {
return StatusClientClosedRequestText
}
return http.StatusText(statusCode)
}

Some files were not shown because too many files have changed in this diff Show More