From e38c0c39691f15aa803fc7402c086780ca8b4664 Mon Sep 17 00:00:00 2001 From: Romain Date: Thu, 27 Apr 2023 16:28:06 +0200 Subject: [PATCH 01/10] Update vulcand/oxy to be5cf38 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b04a34809..abe11af2a 100644 --- a/go.mod +++ b/go.mod @@ -66,7 +66,7 @@ require ( github.com/unrolled/render v1.0.2 github.com/unrolled/secure v1.0.9 github.com/vdemeester/shakers v0.1.0 - github.com/vulcand/oxy/v2 v2.0.0-20230417082832-03de175b3822 + github.com/vulcand/oxy/v2 v2.0.0-20230427132221-be5cf38f3c1c github.com/vulcand/predicate v1.2.0 go.elastic.co/apm v1.13.1 go.elastic.co/apm/module/apmot v1.13.1 diff --git a/go.sum b/go.sum index 1d145c525..7a6444543 100644 --- a/go.sum +++ b/go.sum @@ -1798,8 +1798,8 @@ github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6Ac github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/vmware/govmomi v0.18.0/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= -github.com/vulcand/oxy/v2 v2.0.0-20230417082832-03de175b3822 h1:DXLWOIMPcQV+bxCFhBYSY5AIGP4DGvXH6qkwsg82YYY= -github.com/vulcand/oxy/v2 v2.0.0-20230417082832-03de175b3822/go.mod h1:A2voDnpONyqdplUDK0lt5y4XHLiBXPBw7iQES8+ZWRw= +github.com/vulcand/oxy/v2 v2.0.0-20230427132221-be5cf38f3c1c h1:Qt/YKpE8uAKNF4x2mwBZxmVo2WtgUL1WFDeXr1nlfpA= +github.com/vulcand/oxy/v2 v2.0.0-20230427132221-be5cf38f3c1c/go.mod h1:A2voDnpONyqdplUDK0lt5y4XHLiBXPBw7iQES8+ZWRw= github.com/vulcand/predicate v1.2.0 h1:uFsW1gcnnR7R+QTID+FVcs0sSYlIGntoGOTb3rQJt50= github.com/vulcand/predicate v1.2.0/go.mod h1:VipoNYXny6c8N381zGUWkjuuNHiRbeAZhE7Qm9c+2GA= github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs= From 7805c683e3336501cf168f40d5d03d4b73431783 Mon Sep 17 00:00:00 2001 From: Romain Date: Thu, 27 Apr 2023 16:46:11 +0200 Subject: [PATCH 02/10] Prepare release v2.10.1 --- CHANGELOG.md | 9 +++++++++ script/gcg/traefik-bugfix.toml | 10 +++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc0816118..c1901b001 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [v2.10.1](https://github.com/traefik/traefik/tree/v2.10.1) (2023-04-27) +[All Commits](https://github.com/traefik/traefik/compare/v2.10.0...v2.10.1) + +**Bug fixes:** +- **[middleware,oxy]** Update vulcand/oxy to be5cf38 ([#9874](https://github.com/traefik/traefik/pull/9874) by [rtribotte](https://github.com/rtribotte)) + +**Documentation:** +- Fix v2.10 migration guide ([#9863](https://github.com/traefik/traefik/pull/9863) by [rtribotte](https://github.com/rtribotte)) + ## [v2.10.0](https://github.com/traefik/traefik/tree/v2.10.0) (2023-04-24) [All Commits](https://github.com/traefik/traefik/compare/v2.9.0-rc1...v2.10.0) diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index 189f8c451..bc6c422ee 100644 --- a/script/gcg/traefik-bugfix.toml +++ b/script/gcg/traefik-bugfix.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example new bugfix v2.9.9 -CurrentRef = "v2.9" -PreviousRef = "v2.9.8" -BaseBranch = "v2.9" -FutureCurrentRefName = "v2.9.9" +# example new bugfix v2.10.1 +CurrentRef = "v2.10" +PreviousRef = "v2.10.0" +BaseBranch = "v2.10" +FutureCurrentRefName = "v2.10.1" ThresholdPreviousRef = 10 ThresholdCurrentRef = 10 From e044e2b765876ce4a61e650c24cd00a415e254b3 Mon Sep 17 00:00:00 2001 From: mloiseleur <97035654+mloiseleur@users.noreply.github.com> Date: Fri, 28 Apr 2023 15:36:05 +0200 Subject: [PATCH 03/10] chore: update CI base OS --- .semaphore/semaphore.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 4244a52f7..349249162 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -3,7 +3,7 @@ name: Traefik agent: machine: type: e1-standard-4 - os_image: ubuntu1804 + os_image: ubuntu2004 fail_fast: stop: @@ -57,7 +57,7 @@ blocks: agent: machine: type: e1-standard-8 - os_image: ubuntu1804 + os_image: ubuntu2004 secrets: - name: traefik env_vars: From 65c59c9a09ddaefb14c83851a476be5066dff8ba Mon Sep 17 00:00:00 2001 From: Romain Date: Fri, 28 Apr 2023 17:56:05 +0200 Subject: [PATCH 04/10] Add FAQ documentation about TLS certificates --- docs/content/getting-started/faq.md | 50 +++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/docs/content/getting-started/faq.md b/docs/content/getting-started/faq.md index 1ae3def8e..caa82058f 100644 --- a/docs/content/getting-started/faq.md +++ b/docs/content/getting-started/faq.md @@ -158,6 +158,56 @@ By default, the following headers are automatically added when proxying requests For more details, please check out the [forwarded header](../routing/entrypoints.md#forwarded-headers) documentation. +## How Traefik is Storing and Serving TLS Certificates? + +### Storing TLS Certificates + +[TLS](../https/tls.md "Link to Traefik TLS docs") certificates are either provided directly by the [dynamic configuration](./configuration-overview.md#the-dynamic-configuration "Link to dynamic configuration overview") from [providers](../https/tls.md#user-defined "Link to the TLS configuration"), +or by [ACME resolvers](../https/acme.md#providers "Link to ACME resolvers"), which act themselves as providers internally. + +For each TLS certificate, Traefik produces an identifier used as a key to store it. +This identifier is constructed as the alphabetically ordered concatenation of the SANs `DNSNames` and `IPAddresses` of the TLScertificate. + +#### Examples: + +| X509v3 Subject Alternative Name | TLS Certificate Identifier | +|-----------------------------------------|-----------------------------| +| `DNS:example.com, IP Address:127.0.0.1` | `127.0.0.1,example.com` | +| `DNS:example.com, DNS:*.example.com` | `*.example.com,example.com` | + +The identifier is used to store TLS certificates in order to be later used to handle TLS connections. +This operation happens each time there are configuration changes. + +If multiple TLS certificates are provided with the same SANs definition (same identifier), only the one processed first is kept. +Because the dynamic configuration is aggregated from all providers, +when processing it to gather TLS certificates, +there is no guarantee of the order in which they would be processed. +This means that along with configurations applied, it is possible that the TLS certificate retained for a given identifier differs. + +### Serving TLS Certificates + +For each incoming connection, Traefik is serving the "best" matching TLS certificate for the provided server name. + +The TLS certificate selection process narrows down the list of TLS certificates matching the server name, +and then selects the last TLS certificate in this list after having ordered it by the identifier alphabetically. + +#### Examples: + +| Selected TLS Certificates Identifiers | Sorted TLS Certificates Identifiers | Served Certificate Identifier | +|-----------------------------------------------------|-----------------------------------------------------|-------------------------------| +| `127.0.0.1,example.com`,`*.example.com,example.com` | `*.example.com,example.com`,`127.0.0.1,example.com` | `127.0.0.1,example.com` | +| `*.example.com,example.com`,`example.com` | `*.example.com,example.com`,`example.com` | `example.com` | + +### Caching TLS Certificates + +While Traefik is serving the best matching TLS certificate for each incoming connection, +the selection process cost for each incoming connection is avoided thanks to a cache mechanism. + +Once a TLS certificate has been selected as the "best" TLS certificate for a server name, +it is cached for an hour, avoiding the selection process for further connections. + +Nonetheless, when a new configuration is applied, the cache is reset. + ## What does the "field not found" error mean? ```shell From 8f206ce3193bc7017d27f49fe0c938e475c6da34 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Wed, 3 May 2023 10:20:05 +0200 Subject: [PATCH 05/10] Update go-acme/lego to v4.11.0 --- docs/content/https/acme.md | 235 +++++++++++++++++++------------------ go.mod | 26 ++-- go.sum | 54 +++++---- 3 files changed, 167 insertions(+), 148 deletions(-) diff --git a/docs/content/https/acme.md b/docs/content/https/acme.md index ebd9af1f1..3e1f8345e 100644 --- a/docs/content/https/acme.md +++ b/docs/content/https/acme.md @@ -308,121 +308,126 @@ For example, `CF_API_EMAIL_FILE=/run/secrets/traefik_cf-api-email` could be used For complete details, refer to your provider's _Additional configuration_ link. -| Provider Name | Provider Code | Environment Variables | | -|--------------------------------------------------------------------------|--------------------|---------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------| -| [ACME DNS](https://github.com/joohoi/acme-dns) | `acme-dns` | `ACME_DNS_API_BASE`, `ACME_DNS_STORAGE_PATH` | [Additional configuration](https://go-acme.github.io/lego/dns/acme-dns) | -| [Alibaba Cloud](https://www.alibabacloud.com) | `alidns` | `ALICLOUD_ACCESS_KEY`, `ALICLOUD_SECRET_KEY`, `ALICLOUD_REGION_ID` | [Additional configuration](https://go-acme.github.io/lego/dns/alidns) | -| [all-inkl](https://all-inkl.com) | `allinkl` | `ALL_INKL_LOGIN`, `ALL_INKL_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/allinkl) | -| [ArvanCloud](https://www.arvancloud.com/en) | `arvancloud` | `ARVANCLOUD_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/arvancloud) | -| [Auroradns](https://www.pcextreme.com/dns-health-checks) | `auroradns` | `AURORA_USER_ID`, `AURORA_KEY`, `AURORA_ENDPOINT` | [Additional configuration](https://go-acme.github.io/lego/dns/auroradns) | -| [Autodns](https://www.internetx.com/domains/autodns/) | `autodns` | `AUTODNS_API_USER`, `AUTODNS_API_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/autodns) | -| [Azure](https://azure.microsoft.com/services/dns/) | `azure` | `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_SUBSCRIPTION_ID`, `AZURE_TENANT_ID`, `AZURE_RESOURCE_GROUP`, `[AZURE_METADATA_ENDPOINT]` | [Additional configuration](https://go-acme.github.io/lego/dns/azure) | -| [Bindman](https://github.com/labbsr0x/bindman-dns-webhook) | `bindman` | `BINDMAN_MANAGER_ADDRESS` | [Additional configuration](https://go-acme.github.io/lego/dns/bindman) | -| [Blue Cat](https://www.bluecatnetworks.com/) | `bluecat` | `BLUECAT_SERVER_URL`, `BLUECAT_USER_NAME`, `BLUECAT_PASSWORD`, `BLUECAT_CONFIG_NAME`, `BLUECAT_DNS_VIEW` | [Additional configuration](https://go-acme.github.io/lego/dns/bluecat) | -| [Checkdomain](https://www.checkdomain.de/) | `checkdomain` | `CHECKDOMAIN_TOKEN`, | [Additional configuration](https://go-acme.github.io/lego/dns/checkdomain/) | -| [Civo](https://www.civo.com/) | `civo` | `CIVO_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/civo) | -| [CloudDNS](https://vshosting.eu/) | `clouddns` | `CLOUDDNS_CLIENT_ID`, `CLOUDDNS_EMAIL`, `CLOUDDNS_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/clouddns) | -| [Cloudflare](https://www.cloudflare.com) | `cloudflare` | `CF_API_EMAIL`, `CF_API_KEY` [^5] or `CF_DNS_API_TOKEN`, `[CF_ZONE_API_TOKEN]` | [Additional configuration](https://go-acme.github.io/lego/dns/cloudflare) | -| [ClouDNS](https://www.cloudns.net/) | `cloudns` | `CLOUDNS_AUTH_ID`, `CLOUDNS_AUTH_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/cloudns) | -| [CloudXNS](https://www.cloudxns.net) | `cloudxns` | `CLOUDXNS_API_KEY`, `CLOUDXNS_SECRET_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/cloudxns) | -| [ConoHa](https://www.conoha.jp) | `conoha` | `CONOHA_TENANT_ID`, `CONOHA_API_USERNAME`, `CONOHA_API_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/conoha) | -| [Constellix](https://constellix.com) | `constellix` | `CONSTELLIX_API_KEY`, `CONSTELLIX_SECRET_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/constellix) | -| [deSEC](https://desec.io) | `desec` | `DESEC_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/desec) | -| [DigitalOcean](https://www.digitalocean.com) | `digitalocean` | `DO_AUTH_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/digitalocean) | -| [DNS Made Easy](https://dnsmadeeasy.com) | `dnsmadeeasy` | `DNSMADEEASY_API_KEY`, `DNSMADEEASY_API_SECRET`, `DNSMADEEASY_SANDBOX` | [Additional configuration](https://go-acme.github.io/lego/dns/dnsmadeeasy) | -| [dnsHome.de](https://www.dnshome.de) | `dnsHomede` | `DNSHOMEDE_CREDENTIALS` | [Additional configuration](https://go-acme.github.io/lego/dns/dnshomede) | -| [DNSimple](https://dnsimple.com) | `dnsimple` | `DNSIMPLE_OAUTH_TOKEN`, `DNSIMPLE_BASE_URL` | [Additional configuration](https://go-acme.github.io/lego/dns/dnsimple) | -| [DNSPod](https://www.dnspod.com/) | `dnspod` | `DNSPOD_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/dnspod) | -| [Domain Offensive (do.de)](https://www.do.de/) | `dode` | `DODE_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/dode) | -| [Domeneshop](https://domene.shop) | `domeneshop` | `DOMENESHOP_API_TOKEN`, `DOMENESHOP_API_SECRET` | [Additional configuration](https://go-acme.github.io/lego/dns/domeneshop) | -| [DreamHost](https://www.dreamhost.com/) | `dreamhost` | `DREAMHOST_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/dreamhost) | -| [Duck DNS](https://www.duckdns.org/) | `duckdns` | `DUCKDNS_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/duckdns) | -| [Dyn](https://dyn.com) | `dyn` | `DYN_CUSTOMER_NAME`, `DYN_USER_NAME`, `DYN_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/dyn) | -| [Dynu](https://www.dynu.com) | `dynu` | `DYNU_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/dynu) | -| [EasyDNS](https://easydns.com/) | `easydns` | `EASYDNS_TOKEN`, `EASYDNS_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/easydns) | -| [EdgeDNS](https://www.akamai.com/) | `edgedns` | `AKAMAI_CLIENT_TOKEN`, `AKAMAI_CLIENT_SECRET`, `AKAMAI_ACCESS_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/edgedns) | -| [Epik](https://www.epik.com) | `epik` | `EPIK_SIGNATURE` | [Additional configuration](https://go-acme.github.io/lego/dns/epik) | -| [Exoscale](https://www.exoscale.com) | `exoscale` | `EXOSCALE_API_KEY`, `EXOSCALE_API_SECRET`, `EXOSCALE_ENDPOINT` | [Additional configuration](https://go-acme.github.io/lego/dns/exoscale) | -| [Fast DNS](https://www.akamai.com/) | `fastdns` | `AKAMAI_CLIENT_TOKEN`, `AKAMAI_CLIENT_SECRET`, `AKAMAI_ACCESS_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/edgedns) | -| [Freemyip.com](https://freemyip.com) | `freemyip` | `FREEMYIP_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/freemyip) | -| [G-Core Lab](https://gcorelabs.com/dns/) | `gcore` | `GCORE_PERMANENT_API_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/gcore) | -| [Gandi v5](https://doc.livedns.gandi.net) | `gandiv5` | `GANDIV5_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/gandiv5) | -| [Gandi](https://www.gandi.net) | `gandi` | `GANDI_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/gandi) | -| [Glesys](https://glesys.com/) | `glesys` | `GLESYS_API_USER`, `GLESYS_API_KEY`, `GLESYS_DOMAIN` | [Additional configuration](https://go-acme.github.io/lego/dns/glesys) | -| [GoDaddy](https://www.godaddy.com) | `godaddy` | `GODADDY_API_KEY`, `GODADDY_API_SECRET` | [Additional configuration](https://go-acme.github.io/lego/dns/godaddy) | -| [Google Cloud DNS](https://cloud.google.com/dns/docs/) | `gcloud` | `GCE_PROJECT`, Application Default Credentials [^2] [^3], [`GCE_SERVICE_ACCOUNT_FILE`] | [Additional configuration](https://go-acme.github.io/lego/dns/gcloud) | -| [Hetzner](https://hetzner.com) | `hetzner` | `HETZNER_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/hetzner) | -| [hosting.de](https://www.hosting.de) | `hostingde` | `HOSTINGDE_API_KEY`, `HOSTINGDE_ZONE_NAME` | [Additional configuration](https://go-acme.github.io/lego/dns/hostingde) | -| [Hosttech](https://www.hosttech.eu) | `hosttech` | `HOSTTECH_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/hosttech) | -| [Hurricane Electric](https://dns.he.net) | `hurricane` | `HURRICANE_TOKENS` [^6] | [Additional configuration](https://go-acme.github.io/lego/dns/hurricane) | -| [HyperOne](https://www.hyperone.com) | `hyperone` | `HYPERONE_PASSPORT_LOCATION`, `HYPERONE_LOCATION_ID` | [Additional configuration](https://go-acme.github.io/lego/dns/hyperone) | -| [IBM Cloud (SoftLayer)](https://www.ibm.com/cloud/) | `ibmcloud` | `SOFTLAYER_USERNAME`, `SOFTLAYER_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/ibmcloud) | -| [IIJ DNS Platform Service](https://www.iij.ad.jp) | `iijdpf` | `IIJ_DPF_API_TOKEN` , `IIJ_DPF_DPM_SERVICE_CODE` | [Additional configuration](https://go-acme.github.io/lego/dns/iijdpf) | -| [IIJ](https://www.iij.ad.jp/) | `iij` | `IIJ_API_ACCESS_KEY`, `IIJ_API_SECRET_KEY`, `IIJ_DO_SERVICE_CODE` | [Additional configuration](https://go-acme.github.io/lego/dns/iij) | -| [Infoblox](https://www.infoblox.com/) | `infoblox` | `INFOBLOX_USERNAME`, `INFOBLOX_PASSWORD`, `INFOBLOX_HOST` | [Additional configuration](https://go-acme.github.io/lego/dns/infoblox) | -| [Infomaniak](https://www.infomaniak.com) | `infomaniak` | `INFOMANIAK_ACCESS_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/infomaniak) | -| [Internet.bs](https://internetbs.net) | `internetbs` | `INTERNET_BS_API_KEY`, `INTERNET_BS_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/internetbs) | -| [INWX](https://www.inwx.de/en) | `inwx` | `INWX_USERNAME`, `INWX_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/inwx) | -| [ionos](https://ionos.com/) | `ionos` | `IONOS_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/ionos) | -| [iwantmyname](https://iwantmyname.com) | `iwantmyname` | `IWANTMYNAME_USERNAME` , `IWANTMYNAME_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/iwantmyname) | -| [Joker.com](https://joker.com) | `joker` | `JOKER_API_MODE` with `JOKER_API_KEY` or `JOKER_USERNAME`, `JOKER_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/joker) | -| [Liara](https://liara.ir) | `liara` | `LIARA_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/liara) | -| [Lightsail](https://aws.amazon.com/lightsail/) | `lightsail` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `DNS_ZONE` | [Additional configuration](https://go-acme.github.io/lego/dns/lightsail) | -| [Linode v4](https://www.linode.com) | `linode` | `LINODE_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/linode) | -| [Liquid Web](https://www.liquidweb.com/) | `liquidweb` | `LIQUID_WEB_PASSWORD`, `LIQUID_WEB_USERNAME`, `LIQUID_WEB_ZONE` | [Additional configuration](https://go-acme.github.io/lego/dns/liquidweb) | -| [Loopia](https://loopia.com/) | `loopia` | `LOOPIA_API_PASSWORD`, `LOOPIA_API_USER` | [Additional configuration](https://go-acme.github.io/lego/dns/loopia) | -| [LuaDNS](https://luadns.com) | `luadns` | `LUADNS_API_USERNAME`, `LUADNS_API_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/luadns) | -| [MyDNS.jp](https://www.mydns.jp/) | `mydnsjp` | `MYDNSJP_MASTER_ID`, `MYDNSJP_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/mydnsjp) | -| [Mythic Beasts](https://www.mythic-beasts.com) | `mythicbeasts` | `MYTHICBEASTS_USER_NAME`, `MYTHICBEASTS_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/mythicbeasts) | -| [name.com](https://www.name.com/) | `namedotcom` | `NAMECOM_USERNAME`, `NAMECOM_API_TOKEN`, `NAMECOM_SERVER` | [Additional configuration](https://go-acme.github.io/lego/dns/namedotcom) | -| [Namecheap](https://www.namecheap.com) | `namecheap` | `NAMECHEAP_API_USER`, `NAMECHEAP_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/namecheap) | -| [Namesilo](https://www.namesilo.com/) | `namesilo` | `NAMESILO_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/namesilo) | -| [NearlyFreeSpeech.NET](https://www.nearlyfreespeech.net/) | `nearlyfreespeech` | `NEARLYFREESPEECH_API_KEY`, `NEARLYFREESPEECH_LOGIN` | [Additional configuration](https://go-acme.github.io/lego/dns/nearlyfreespeech) | -| [Netcup](https://www.netcup.eu/) | `netcup` | `NETCUP_CUSTOMER_NUMBER`, `NETCUP_API_KEY`, `NETCUP_API_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/netcup) | -| [Netlify](https://www.netlify.com) | `netlify` | `NETLIFY_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/netlify) | -| [Nicmanager](https://www.nicmanager.com) | `nicmanager` | `NICMANAGER_API_EMAIL`, `NICMANAGER_API_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/nicmanager) | -| [NIFCloud](https://cloud.nifty.com/service/dns.htm) | `nifcloud` | `NIFCLOUD_ACCESS_KEY_ID`, `NIFCLOUD_SECRET_ACCESS_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/nifcloud) | -| [Njalla](https://njal.la) | `njalla` | `NJALLA_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/njalla) | -| [NS1](https://ns1.com/) | `ns1` | `NS1_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/ns1) | -| [Open Telekom Cloud](https://cloud.telekom.de) | `otc` | `OTC_DOMAIN_NAME`, `OTC_USER_NAME`, `OTC_PASSWORD`, `OTC_PROJECT_NAME`, `OTC_IDENTITY_ENDPOINT` | [Additional configuration](https://go-acme.github.io/lego/dns/otc) | -| [Openstack Designate](https://docs.openstack.org/designate) | `designate` | `OS_AUTH_URL`, `OS_USERNAME`, `OS_PASSWORD`, `OS_TENANT_NAME`, `OS_REGION_NAME` | [Additional configuration](https://go-acme.github.io/lego/dns/designate) | -| [Oracle Cloud](https://cloud.oracle.com/home) | `oraclecloud` | `OCI_COMPARTMENT_OCID`, `OCI_PRIVKEY_FILE`, `OCI_PRIVKEY_PASS`, `OCI_PUBKEY_FINGERPRINT`, `OCI_REGION`, `OCI_TENANCY_OCID`, `OCI_USER_OCID` | [Additional configuration](https://go-acme.github.io/lego/dns/oraclecloud) | -| [OVH](https://www.ovh.com) | `ovh` | `OVH_ENDPOINT`, `OVH_APPLICATION_KEY`, `OVH_APPLICATION_SECRET`, `OVH_CONSUMER_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/ovh) | -| [Porkbun](https://porkbun.com/) | `porkbun` | `PORKBUN_SECRET_API_KEY`, `PORKBUN_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/porkbun) | -| [PowerDNS](https://www.powerdns.com) | `pdns` | `PDNS_API_KEY`, `PDNS_API_URL` | [Additional configuration](https://go-acme.github.io/lego/dns/pdns) | -| [Rackspace](https://www.rackspace.com/cloud/dns) | `rackspace` | `RACKSPACE_USER`, `RACKSPACE_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/rackspace) | -| [reg.ru](https://www.reg.ru) | `regru` | `REGRU_USERNAME`, `REGRU_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/regru) | -| [RFC2136](https://tools.ietf.org/html/rfc2136) | `rfc2136` | `RFC2136_TSIG_KEY`, `RFC2136_TSIG_SECRET`, `RFC2136_TSIG_ALGORITHM`, `RFC2136_NAMESERVER` | [Additional configuration](https://go-acme.github.io/lego/dns/rfc2136) | -| [RimuHosting](https://rimuhosting.com) | `rimuhosting` | `RIMUHOSTING_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/rimuhosting) | -| [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. | [Additional configuration](https://go-acme.github.io/lego/dns/route53) | -| [Sakura Cloud](https://cloud.sakura.ad.jp/) | `sakuracloud` | `SAKURACLOUD_ACCESS_TOKEN`, `SAKURACLOUD_ACCESS_TOKEN_SECRET` | [Additional configuration](https://go-acme.github.io/lego/dns/sakuracloud) | -| [Scaleway](https://www.scaleway.com) | `scaleway` | `SCALEWAY_API_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/scaleway) | -| [Selectel](https://selectel.ru/en/) | `selectel` | `SELECTEL_API_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/selectel) | -| [Servercow](https://servercow.de) | `servercow` | `SERVERCOW_USERNAME`, `SERVERCOW_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/servercow) | -| [Simply.com](https://www.simply.com/en/domains/) | `simply` | `SIMPLY_ACCOUNT_NAME`, `SIMPLY_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/simply) | -| [Sonic](https://www.sonic.com/) | `sonic` | `SONIC_USER_ID`, `SONIC_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/sonic) | -| [Stackpath](https://www.stackpath.com/) | `stackpath` | `STACKPATH_CLIENT_ID`, `STACKPATH_CLIENT_SECRET`, `STACKPATH_STACK_ID` | [Additional configuration](https://go-acme.github.io/lego/dns/stackpath) | -| [Tencent Cloud DNS](https://cloud.tencent.com/product/cns) | `tencentcloud` | `TENCENTCLOUD_SECRET_ID`, `TENCENTCLOUD_SECRET_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/tencentcloud) | -| [TransIP](https://www.transip.nl/) | `transip` | `TRANSIP_ACCOUNT_NAME`, `TRANSIP_PRIVATE_KEY_PATH` | [Additional configuration](https://go-acme.github.io/lego/dns/transip) | -| [UKFast SafeDNS](https://docs.ukfast.co.uk/domains/safedns/index.html) | `safedns` | `SAFEDNS_AUTH_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/safedns) | -| [Ultradns](https://neustarsecurityservices.com/dns-services) | `ultradns` | `ULTRADNS_USERNAME`, `ULTRADNS_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/ultradns) | -| [Variomedia](https://www.variomedia.de/) | `variomedia` | `VARIOMEDIA_API_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/variomedia) | -| [VegaDNS](https://github.com/shupp/VegaDNS-API) | `vegadns` | `SECRET_VEGADNS_KEY`, `SECRET_VEGADNS_SECRET`, `VEGADNS_URL` | [Additional configuration](https://go-acme.github.io/lego/dns/vegadns) | -| [Vercel](https://vercel.com) | `vercel` | `VERCEL_API_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/vercel) | -| [Versio](https://www.versio.nl/domeinnamen) | `versio` | `VERSIO_USERNAME`, `VERSIO_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/versio) | -| [VinylDNS](https://www.vinyldns.io) | `vinyldns` | `VINYLDNS_ACCESS_KEY`, `VINYLDNS_SECRET_KEY`, `VINYLDNS_HOST` | [Additional configuration](https://go-acme.github.io/lego/dns/vinyldns) | -| [VK Cloud](https://mcs.mail.ru/) | `vkcloud` | `VK_CLOUD_PASSWORD`, `VK_CLOUD_PROJECT_ID`, `VK_CLOUD_USERNAME` | [Additional configuration](https://go-acme.github.io/lego/dns/vkcloud) | -| [Vscale](https://vscale.io/) | `vscale` | `VSCALE_API_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/vscale) | -| [VULTR](https://www.vultr.com) | `vultr` | `VULTR_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/vultr) | -| [Websupport](https://websupport.sk) | `websupport` | `WEBSUPPORT_API_KEY`, `WEBSUPPORT_SECRET` | [Additional configuration](https://go-acme.github.io/lego/dns/websupport) | -| [WEDOS](https://www.wedos.com) | `wedos` | `WEDOS_USERNAME`, `WEDOS_WAPI_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/wedos) | -| [Yandex Cloud](https://cloud.yandex.com/en/) | `yandexcloud` | `YANDEX_CLOUD_FOLDER_ID`, `YANDEX_CLOUD_IAM_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/yandexcloud) | -| [Yandex](https://yandex.com) | `yandex` | `YANDEX_PDD_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/yandex) | -| [Zone.ee](https://www.zone.ee) | `zoneee` | `ZONEEE_API_USER`, `ZONEEE_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/zoneee) | -| [Zonomi](https://zonomi.com) | `zonomi` | `ZONOMI_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/zonomi) | -| External Program | `exec` | `EXEC_PATH` | [Additional configuration](https://go-acme.github.io/lego/dns/exec) | -| HTTP request | `httpreq` | `HTTPREQ_ENDPOINT`, `HTTPREQ_MODE`, `HTTPREQ_USERNAME`, `HTTPREQ_PASSWORD` [^1] | [Additional configuration](https://go-acme.github.io/lego/dns/httpreq) | -| manual | `manual` | none, but you need to run Traefik interactively [^4], turn on debug log to see instructions and press Enter. | | +| Provider Name | Provider Code | Environment Variables | | +|------------------------------------------------------------------------|--------------------|---------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------| +| [ACME DNS](https://github.com/joohoi/acme-dns) | `acme-dns` | `ACME_DNS_API_BASE`, `ACME_DNS_STORAGE_PATH` | [Additional configuration](https://go-acme.github.io/lego/dns/acme-dns) | +| [Alibaba Cloud](https://www.alibabacloud.com) | `alidns` | `ALICLOUD_ACCESS_KEY`, `ALICLOUD_SECRET_KEY`, `ALICLOUD_REGION_ID` | [Additional configuration](https://go-acme.github.io/lego/dns/alidns) | +| [all-inkl](https://all-inkl.com) | `allinkl` | `ALL_INKL_LOGIN`, `ALL_INKL_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/allinkl) | +| [ArvanCloud](https://www.arvancloud.com/en) | `arvancloud` | `ARVANCLOUD_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/arvancloud) | +| [Auroradns](https://www.pcextreme.com/dns-health-checks) | `auroradns` | `AURORA_USER_ID`, `AURORA_KEY`, `AURORA_ENDPOINT` | [Additional configuration](https://go-acme.github.io/lego/dns/auroradns) | +| [Autodns](https://www.internetx.com/domains/autodns/) | `autodns` | `AUTODNS_API_USER`, `AUTODNS_API_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/autodns) | +| [Azure](https://azure.microsoft.com/services/dns/) | `azure` | `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_SUBSCRIPTION_ID`, `AZURE_TENANT_ID`, `AZURE_RESOURCE_GROUP`, `[AZURE_METADATA_ENDPOINT]` | [Additional configuration](https://go-acme.github.io/lego/dns/azure) | +| [Bindman](https://github.com/labbsr0x/bindman-dns-webhook) | `bindman` | `BINDMAN_MANAGER_ADDRESS` | [Additional configuration](https://go-acme.github.io/lego/dns/bindman) | +| [Blue Cat](https://www.bluecatnetworks.com/) | `bluecat` | `BLUECAT_SERVER_URL`, `BLUECAT_USER_NAME`, `BLUECAT_PASSWORD`, `BLUECAT_CONFIG_NAME`, `BLUECAT_DNS_VIEW` | [Additional configuration](https://go-acme.github.io/lego/dns/bluecat) | +| [Brandit](https://www.brandit.com) | `brandit` | `BRANDIT_API_USERNAME`, `BRANDIT_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/brandit) | +| [Bunny](https://bunny.net) | `bunny` | `BUNNY_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/bunny) | +| [Checkdomain](https://www.checkdomain.de/) | `checkdomain` | `CHECKDOMAIN_TOKEN`, | [Additional configuration](https://go-acme.github.io/lego/dns/checkdomain/) | +| [Civo](https://www.civo.com/) | `civo` | `CIVO_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/civo) | +| [CloudDNS](https://vshosting.eu/) | `clouddns` | `CLOUDDNS_CLIENT_ID`, `CLOUDDNS_EMAIL`, `CLOUDDNS_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/clouddns) | +| [Cloudflare](https://www.cloudflare.com) | `cloudflare` | `CF_API_EMAIL`, `CF_API_KEY` [^5] or `CF_DNS_API_TOKEN`, `[CF_ZONE_API_TOKEN]` | [Additional configuration](https://go-acme.github.io/lego/dns/cloudflare) | +| [ClouDNS](https://www.cloudns.net/) | `cloudns` | `CLOUDNS_AUTH_ID`, `CLOUDNS_AUTH_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/cloudns) | +| [CloudXNS](https://www.cloudxns.net) | `cloudxns` | `CLOUDXNS_API_KEY`, `CLOUDXNS_SECRET_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/cloudxns) | +| [ConoHa](https://www.conoha.jp) | `conoha` | `CONOHA_TENANT_ID`, `CONOHA_API_USERNAME`, `CONOHA_API_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/conoha) | +| [Constellix](https://constellix.com) | `constellix` | `CONSTELLIX_API_KEY`, `CONSTELLIX_SECRET_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/constellix) | +| [deSEC](https://desec.io) | `desec` | `DESEC_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/desec) | +| [DigitalOcean](https://www.digitalocean.com) | `digitalocean` | `DO_AUTH_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/digitalocean) | +| [DNS Made Easy](https://dnsmadeeasy.com) | `dnsmadeeasy` | `DNSMADEEASY_API_KEY`, `DNSMADEEASY_API_SECRET`, `DNSMADEEASY_SANDBOX` | [Additional configuration](https://go-acme.github.io/lego/dns/dnsmadeeasy) | +| [dnsHome.de](https://www.dnshome.de) | `dnsHomede` | `DNSHOMEDE_CREDENTIALS` | [Additional configuration](https://go-acme.github.io/lego/dns/dnshomede) | +| [DNSimple](https://dnsimple.com) | `dnsimple` | `DNSIMPLE_OAUTH_TOKEN`, `DNSIMPLE_BASE_URL` | [Additional configuration](https://go-acme.github.io/lego/dns/dnsimple) | +| [DNSPod](https://www.dnspod.com/) | `dnspod` | `DNSPOD_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/dnspod) | +| [Domain Offensive (do.de)](https://www.do.de/) | `dode` | `DODE_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/dode) | +| [Domeneshop](https://domene.shop) | `domeneshop` | `DOMENESHOP_API_TOKEN`, `DOMENESHOP_API_SECRET` | [Additional configuration](https://go-acme.github.io/lego/dns/domeneshop) | +| [DreamHost](https://www.dreamhost.com/) | `dreamhost` | `DREAMHOST_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/dreamhost) | +| [Duck DNS](https://www.duckdns.org/) | `duckdns` | `DUCKDNS_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/duckdns) | +| [Dyn](https://dyn.com) | `dyn` | `DYN_CUSTOMER_NAME`, `DYN_USER_NAME`, `DYN_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/dyn) | +| [Dynu](https://www.dynu.com) | `dynu` | `DYNU_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/dynu) | +| [EasyDNS](https://easydns.com/) | `easydns` | `EASYDNS_TOKEN`, `EASYDNS_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/easydns) | +| [EdgeDNS](https://www.akamai.com/) | `edgedns` | `AKAMAI_CLIENT_TOKEN`, `AKAMAI_CLIENT_SECRET`, `AKAMAI_ACCESS_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/edgedns) | +| [Epik](https://www.epik.com) | `epik` | `EPIK_SIGNATURE` | [Additional configuration](https://go-acme.github.io/lego/dns/epik) | +| [Exoscale](https://www.exoscale.com) | `exoscale` | `EXOSCALE_API_KEY`, `EXOSCALE_API_SECRET`, `EXOSCALE_ENDPOINT` | [Additional configuration](https://go-acme.github.io/lego/dns/exoscale) | +| [Fast DNS](https://www.akamai.com/) | `fastdns` | `AKAMAI_CLIENT_TOKEN`, `AKAMAI_CLIENT_SECRET`, `AKAMAI_ACCESS_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/edgedns) | +| [Freemyip.com](https://freemyip.com) | `freemyip` | `FREEMYIP_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/freemyip) | +| [G-Core Lab](https://gcorelabs.com/dns/) | `gcore` | `GCORE_PERMANENT_API_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/gcore) | +| [Gandi v5](https://doc.livedns.gandi.net) | `gandiv5` | `GANDIV5_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/gandiv5) | +| [Gandi](https://www.gandi.net) | `gandi` | `GANDI_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/gandi) | +| [Glesys](https://glesys.com/) | `glesys` | `GLESYS_API_USER`, `GLESYS_API_KEY`, `GLESYS_DOMAIN` | [Additional configuration](https://go-acme.github.io/lego/dns/glesys) | +| [GoDaddy](https://www.godaddy.com) | `godaddy` | `GODADDY_API_KEY`, `GODADDY_API_SECRET` | [Additional configuration](https://go-acme.github.io/lego/dns/godaddy) | +| [Google Domains](https://domains.google) | `googledomains` | `GOOGLE_DOMAINS_ACCESS_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/googledomains) | +| [Google Cloud DNS](https://cloud.google.com/dns/docs/) | `gcloud` | `GCE_PROJECT`, Application Default Credentials [^2] [^3], [`GCE_SERVICE_ACCOUNT_FILE`] | [Additional configuration](https://go-acme.github.io/lego/dns/gcloud) | +| [Hetzner](https://hetzner.com) | `hetzner` | `HETZNER_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/hetzner) | +| [hosting.de](https://www.hosting.de) | `hostingde` | `HOSTINGDE_API_KEY`, `HOSTINGDE_ZONE_NAME` | [Additional configuration](https://go-acme.github.io/lego/dns/hostingde) | +| [Hosttech](https://www.hosttech.eu) | `hosttech` | `HOSTTECH_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/hosttech) | +| [Hurricane Electric](https://dns.he.net) | `hurricane` | `HURRICANE_TOKENS` [^6] | [Additional configuration](https://go-acme.github.io/lego/dns/hurricane) | +| [HyperOne](https://www.hyperone.com) | `hyperone` | `HYPERONE_PASSPORT_LOCATION`, `HYPERONE_LOCATION_ID` | [Additional configuration](https://go-acme.github.io/lego/dns/hyperone) | +| [IBM Cloud (SoftLayer)](https://www.ibm.com/cloud/) | `ibmcloud` | `SOFTLAYER_USERNAME`, `SOFTLAYER_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/ibmcloud) | +| [IIJ DNS Platform Service](https://www.iij.ad.jp) | `iijdpf` | `IIJ_DPF_API_TOKEN` , `IIJ_DPF_DPM_SERVICE_CODE` | [Additional configuration](https://go-acme.github.io/lego/dns/iijdpf) | +| [IIJ](https://www.iij.ad.jp/) | `iij` | `IIJ_API_ACCESS_KEY`, `IIJ_API_SECRET_KEY`, `IIJ_DO_SERVICE_CODE` | [Additional configuration](https://go-acme.github.io/lego/dns/iij) | +| [Infoblox](https://www.infoblox.com/) | `infoblox` | `INFOBLOX_USERNAME`, `INFOBLOX_PASSWORD`, `INFOBLOX_HOST` | [Additional configuration](https://go-acme.github.io/lego/dns/infoblox) | +| [Infomaniak](https://www.infomaniak.com) | `infomaniak` | `INFOMANIAK_ACCESS_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/infomaniak) | +| [Internet.bs](https://internetbs.net) | `internetbs` | `INTERNET_BS_API_KEY`, `INTERNET_BS_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/internetbs) | +| [INWX](https://www.inwx.de/en) | `inwx` | `INWX_USERNAME`, `INWX_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/inwx) | +| [ionos](https://ionos.com/) | `ionos` | `IONOS_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/ionos) | +| [iwantmyname](https://iwantmyname.com) | `iwantmyname` | `IWANTMYNAME_USERNAME` , `IWANTMYNAME_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/iwantmyname) | +| [Joker.com](https://joker.com) | `joker` | `JOKER_API_MODE` with `JOKER_API_KEY` or `JOKER_USERNAME`, `JOKER_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/joker) | +| [Liara](https://liara.ir) | `liara` | `LIARA_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/liara) | +| [Lightsail](https://aws.amazon.com/lightsail/) | `lightsail` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `DNS_ZONE` | [Additional configuration](https://go-acme.github.io/lego/dns/lightsail) | +| [Linode v4](https://www.linode.com) | `linode` | `LINODE_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/linode) | +| [Liquid Web](https://www.liquidweb.com/) | `liquidweb` | `LIQUID_WEB_PASSWORD`, `LIQUID_WEB_USERNAME`, `LIQUID_WEB_ZONE` | [Additional configuration](https://go-acme.github.io/lego/dns/liquidweb) | +| [Loopia](https://loopia.com/) | `loopia` | `LOOPIA_API_PASSWORD`, `LOOPIA_API_USER` | [Additional configuration](https://go-acme.github.io/lego/dns/loopia) | +| [LuaDNS](https://luadns.com) | `luadns` | `LUADNS_API_USERNAME`, `LUADNS_API_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/luadns) | +| [MyDNS.jp](https://www.mydns.jp/) | `mydnsjp` | `MYDNSJP_MASTER_ID`, `MYDNSJP_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/mydnsjp) | +| [Mythic Beasts](https://www.mythic-beasts.com) | `mythicbeasts` | `MYTHICBEASTS_USER_NAME`, `MYTHICBEASTS_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/mythicbeasts) | +| [name.com](https://www.name.com/) | `namedotcom` | `NAMECOM_USERNAME`, `NAMECOM_API_TOKEN`, `NAMECOM_SERVER` | [Additional configuration](https://go-acme.github.io/lego/dns/namedotcom) | +| [Namecheap](https://www.namecheap.com) | `namecheap` | `NAMECHEAP_API_USER`, `NAMECHEAP_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/namecheap) | +| [Namesilo](https://www.namesilo.com/) | `namesilo` | `NAMESILO_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/namesilo) | +| [NearlyFreeSpeech.NET](https://www.nearlyfreespeech.net/) | `nearlyfreespeech` | `NEARLYFREESPEECH_API_KEY`, `NEARLYFREESPEECH_LOGIN` | [Additional configuration](https://go-acme.github.io/lego/dns/nearlyfreespeech) | +| [Netcup](https://www.netcup.eu/) | `netcup` | `NETCUP_CUSTOMER_NUMBER`, `NETCUP_API_KEY`, `NETCUP_API_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/netcup) | +| [Netlify](https://www.netlify.com) | `netlify` | `NETLIFY_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/netlify) | +| [Nicmanager](https://www.nicmanager.com) | `nicmanager` | `NICMANAGER_API_EMAIL`, `NICMANAGER_API_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/nicmanager) | +| [NIFCloud](https://cloud.nifty.com/service/dns.htm) | `nifcloud` | `NIFCLOUD_ACCESS_KEY_ID`, `NIFCLOUD_SECRET_ACCESS_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/nifcloud) | +| [Njalla](https://njal.la) | `njalla` | `NJALLA_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/njalla) | +| [Nodion](https://www.nodion.com) | `nodion` | `NODION_API_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/nodion) | +| [NS1](https://ns1.com/) | `ns1` | `NS1_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/ns1) | +| [Open Telekom Cloud](https://cloud.telekom.de) | `otc` | `OTC_DOMAIN_NAME`, `OTC_USER_NAME`, `OTC_PASSWORD`, `OTC_PROJECT_NAME`, `OTC_IDENTITY_ENDPOINT` | [Additional configuration](https://go-acme.github.io/lego/dns/otc) | +| [Openstack Designate](https://docs.openstack.org/designate) | `designate` | `OS_AUTH_URL`, `OS_USERNAME`, `OS_PASSWORD`, `OS_TENANT_NAME`, `OS_REGION_NAME` | [Additional configuration](https://go-acme.github.io/lego/dns/designate) | +| [Oracle Cloud](https://cloud.oracle.com/home) | `oraclecloud` | `OCI_COMPARTMENT_OCID`, `OCI_PRIVKEY_FILE`, `OCI_PRIVKEY_PASS`, `OCI_PUBKEY_FINGERPRINT`, `OCI_REGION`, `OCI_TENANCY_OCID`, `OCI_USER_OCID` | [Additional configuration](https://go-acme.github.io/lego/dns/oraclecloud) | +| [OVH](https://www.ovh.com) | `ovh` | `OVH_ENDPOINT`, `OVH_APPLICATION_KEY`, `OVH_APPLICATION_SECRET`, `OVH_CONSUMER_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/ovh) | +| [Plesk](https://www.plesk.com) | `plesk` | `PLESK_SERVER_BASE_URL`, `PLESK_USERNAME`, `PLESK_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/plesk) | +| [Porkbun](https://porkbun.com/) | `porkbun` | `PORKBUN_SECRET_API_KEY`, `PORKBUN_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/porkbun) | +| [PowerDNS](https://www.powerdns.com) | `pdns` | `PDNS_API_KEY`, `PDNS_API_URL` | [Additional configuration](https://go-acme.github.io/lego/dns/pdns) | +| [Rackspace](https://www.rackspace.com/cloud/dns) | `rackspace` | `RACKSPACE_USER`, `RACKSPACE_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/rackspace) | +| [reg.ru](https://www.reg.ru) | `regru` | `REGRU_USERNAME`, `REGRU_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/regru) | +| [RFC2136](https://tools.ietf.org/html/rfc2136) | `rfc2136` | `RFC2136_TSIG_KEY`, `RFC2136_TSIG_SECRET`, `RFC2136_TSIG_ALGORITHM`, `RFC2136_NAMESERVER` | [Additional configuration](https://go-acme.github.io/lego/dns/rfc2136) | +| [RimuHosting](https://rimuhosting.com) | `rimuhosting` | `RIMUHOSTING_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/rimuhosting) | +| [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. | [Additional configuration](https://go-acme.github.io/lego/dns/route53) | +| [Sakura Cloud](https://cloud.sakura.ad.jp/) | `sakuracloud` | `SAKURACLOUD_ACCESS_TOKEN`, `SAKURACLOUD_ACCESS_TOKEN_SECRET` | [Additional configuration](https://go-acme.github.io/lego/dns/sakuracloud) | +| [Scaleway](https://www.scaleway.com) | `scaleway` | `SCALEWAY_API_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/scaleway) | +| [Selectel](https://selectel.ru/en/) | `selectel` | `SELECTEL_API_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/selectel) | +| [Servercow](https://servercow.de) | `servercow` | `SERVERCOW_USERNAME`, `SERVERCOW_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/servercow) | +| [Simply.com](https://www.simply.com/en/domains/) | `simply` | `SIMPLY_ACCOUNT_NAME`, `SIMPLY_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/simply) | +| [Sonic](https://www.sonic.com/) | `sonic` | `SONIC_USER_ID`, `SONIC_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/sonic) | +| [Stackpath](https://www.stackpath.com/) | `stackpath` | `STACKPATH_CLIENT_ID`, `STACKPATH_CLIENT_SECRET`, `STACKPATH_STACK_ID` | [Additional configuration](https://go-acme.github.io/lego/dns/stackpath) | +| [Tencent Cloud DNS](https://cloud.tencent.com/product/cns) | `tencentcloud` | `TENCENTCLOUD_SECRET_ID`, `TENCENTCLOUD_SECRET_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/tencentcloud) | +| [TransIP](https://www.transip.nl/) | `transip` | `TRANSIP_ACCOUNT_NAME`, `TRANSIP_PRIVATE_KEY_PATH` | [Additional configuration](https://go-acme.github.io/lego/dns/transip) | +| [UKFast SafeDNS](https://docs.ukfast.co.uk/domains/safedns/index.html) | `safedns` | `SAFEDNS_AUTH_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/safedns) | +| [Ultradns](https://neustarsecurityservices.com/dns-services) | `ultradns` | `ULTRADNS_USERNAME`, `ULTRADNS_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/ultradns) | +| [Variomedia](https://www.variomedia.de/) | `variomedia` | `VARIOMEDIA_API_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/variomedia) | +| [VegaDNS](https://github.com/shupp/VegaDNS-API) | `vegadns` | `SECRET_VEGADNS_KEY`, `SECRET_VEGADNS_SECRET`, `VEGADNS_URL` | [Additional configuration](https://go-acme.github.io/lego/dns/vegadns) | +| [Vercel](https://vercel.com) | `vercel` | `VERCEL_API_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/vercel) | +| [Versio](https://www.versio.nl/domeinnamen) | `versio` | `VERSIO_USERNAME`, `VERSIO_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/versio) | +| [VinylDNS](https://www.vinyldns.io) | `vinyldns` | `VINYLDNS_ACCESS_KEY`, `VINYLDNS_SECRET_KEY`, `VINYLDNS_HOST` | [Additional configuration](https://go-acme.github.io/lego/dns/vinyldns) | +| [VK Cloud](https://mcs.mail.ru/) | `vkcloud` | `VK_CLOUD_PASSWORD`, `VK_CLOUD_PROJECT_ID`, `VK_CLOUD_USERNAME` | [Additional configuration](https://go-acme.github.io/lego/dns/vkcloud) | +| [Vscale](https://vscale.io/) | `vscale` | `VSCALE_API_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/vscale) | +| [VULTR](https://www.vultr.com) | `vultr` | `VULTR_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/vultr) | +| [Websupport](https://websupport.sk) | `websupport` | `WEBSUPPORT_API_KEY`, `WEBSUPPORT_SECRET` | [Additional configuration](https://go-acme.github.io/lego/dns/websupport) | +| [WEDOS](https://www.wedos.com) | `wedos` | `WEDOS_USERNAME`, `WEDOS_WAPI_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/wedos) | +| [Yandex Cloud](https://cloud.yandex.com/en/) | `yandexcloud` | `YANDEX_CLOUD_FOLDER_ID`, `YANDEX_CLOUD_IAM_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/yandexcloud) | +| [Yandex](https://yandex.com) | `yandex` | `YANDEX_PDD_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/yandex) | +| [Zone.ee](https://www.zone.ee) | `zoneee` | `ZONEEE_API_USER`, `ZONEEE_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/zoneee) | +| [Zonomi](https://zonomi.com) | `zonomi` | `ZONOMI_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/zonomi) | +| External Program | `exec` | `EXEC_PATH` | [Additional configuration](https://go-acme.github.io/lego/dns/exec) | +| HTTP request | `httpreq` | `HTTPREQ_ENDPOINT`, `HTTPREQ_MODE`, `HTTPREQ_USERNAME`, `HTTPREQ_PASSWORD` [^1] | [Additional configuration](https://go-acme.github.io/lego/dns/httpreq) | +| manual | `manual` | none, but you need to run Traefik interactively [^4], turn on debug log to see instructions and press Enter. | | [^1]: More information about the HTTP message format can be found [here](https://go-acme.github.io/lego/dns/httpreq/). [^2]: [Providing credentials to your application](https://cloud.google.com/docs/authentication/production). diff --git a/go.mod b/go.mod index abe11af2a..95d703af5 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/docker/go-connections v0.4.0 github.com/fatih/structs v1.1.0 github.com/gambol99/go-marathon v0.0.0-20180614232016-99a156b96fb2 - github.com/go-acme/lego/v4 v4.10.2 + github.com/go-acme/lego/v4 v4.11.0 github.com/go-check/check v0.0.0-00010101000000-000000000000 github.com/go-kit/kit v0.10.1-0.20200915143503-439c4d2ed3ea github.com/golang/protobuf v1.5.2 @@ -75,7 +75,7 @@ require ( golang.org/x/text v0.7.0 golang.org/x/time v0.3.0 golang.org/x/tools v0.2.0 - google.golang.org/grpc v1.49.0 + google.golang.org/grpc v1.53.0 gopkg.in/DataDog/dd-trace-go.v1 v1.43.1 gopkg.in/fsnotify.v1 v1.4.7 gopkg.in/yaml.v3 v3.0.1 @@ -89,7 +89,8 @@ require ( ) require ( - cloud.google.com/go v0.81.0 // indirect + cloud.google.com/go/compute v1.18.0 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/AlecAivazis/survey/v2 v2.2.3 // indirect github.com/Azure/azure-sdk-for-go v40.3.0+incompatible // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect @@ -124,7 +125,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/buger/goterm v1.0.0 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible // indirect github.com/circonus-labs/circonusllhist v0.1.3 // indirect github.com/civo/civogo v0.3.11 // indirect @@ -177,7 +178,7 @@ require ( github.com/gogo/googleapis v1.4.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.2.0 // indirect - github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect + github.com/golang/glog v1.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/mock v1.6.0 // indirect github.com/google/btree v1.0.1 // indirect @@ -188,7 +189,8 @@ require ( github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.3.0 // indirect - github.com/googleapis/gax-go/v2 v2.0.5 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect + github.com/googleapis/gax-go/v2 v2.7.0 // indirect github.com/gophercloud/gophercloud v1.0.0 // indirect github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae // indirect github.com/gravitational/trace v1.1.16-0.20220114165159-14a9a7dd6aaf // indirect @@ -264,6 +266,7 @@ require ( github.com/nrdcg/freemyip v0.2.0 // indirect github.com/nrdcg/goinwx v0.8.1 // indirect github.com/nrdcg/namesilo v0.2.1 // indirect + github.com/nrdcg/nodion v0.1.0 // indirect github.com/nrdcg/porkbun v0.1.1 // indirect github.com/onsi/ginkgo/v2 v2.4.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect @@ -290,6 +293,7 @@ require ( github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect github.com/segmentio/fasthash v1.0.3 // indirect github.com/shopspring/decimal v1.2.0 // indirect + github.com/simplesurance/bunny-go v0.0.0-20221115111006-e11d9dc91f04 // indirect github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect github.com/softlayer/softlayer-go v1.0.6 // indirect github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect @@ -318,7 +322,7 @@ require ( go.etcd.io/etcd/api/v3 v3.5.5 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.5 // indirect go.etcd.io/etcd/client/v3 v3.5.5 // indirect - go.opencensus.io v0.23.0 // indirect + go.opencensus.io v0.24.0 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/ratelimit v0.2.0 // indirect @@ -328,14 +332,14 @@ require ( golang.org/x/crypto v0.5.0 // indirect golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect - golang.org/x/oauth2 v0.4.0 // indirect + golang.org/x/oauth2 v0.5.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.5.0 // indirect golang.org/x/term v0.5.0 // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect - google.golang.org/api v0.44.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + google.golang.org/api v0.111.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect + google.golang.org/genproto v0.0.0-20230223222841-637eb2293923 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.66.6 // indirect diff --git a/go.sum b/go.sum index 7a6444543..c29771870 100644 --- a/go.sum +++ b/go.sum @@ -22,17 +22,22 @@ cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKP cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0 h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.107.0 h1:qkj22L7bgkl6vIeZDlOY2po43Mx/TIa2Wsa7VR+PEww= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY= +cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -276,8 +281,9 @@ github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEex github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= @@ -312,12 +318,8 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200313221541-5f7e5dd04533/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20160425231609-f8ad88b59a58/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= @@ -610,7 +612,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= @@ -656,8 +657,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= -github.com/go-acme/lego/v4 v4.10.2 h1:5eW3qmda5v/LP21v1Hj70edKY1jeFZQwO617tdkwp6Q= -github.com/go-acme/lego/v4 v4.10.2/go.mod h1:EMbf0Jmqwv94nJ5WL9qWnSXIBZnvsS9gNypansHGc6U= +github.com/go-acme/lego/v4 v4.11.0 h1:oIPoU7zBJoTfoVrbqk62+/2NsGCSgCVK1JtZSZZ28SU= +github.com/go-acme/lego/v4 v4.11.0/go.mod h1:dENL0J3/WughN2NLy0T35otK5k1EWCmXTwCw0+X5ZaE= github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= @@ -769,8 +770,9 @@ github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -878,9 +880,12 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= +github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= +github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= github.com/googleapis/gnostic v0.0.0-20170426233943-68f4ded48ba9/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= @@ -1383,6 +1388,8 @@ github.com/nrdcg/goinwx v0.8.1 h1:20EQ/JaGFnSKwiDH2JzjIpicffl3cPk6imJBDqVBVtU= github.com/nrdcg/goinwx v0.8.1/go.mod h1:tILVc10gieBp/5PMvbcYeXM6pVQ+c9jxDZnpaR1UW7c= github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg= github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw= +github.com/nrdcg/nodion v0.1.0 h1:zLKaqTn2X0aDuBHHfyA1zFgeZfiCpmu/O9DM73okavw= +github.com/nrdcg/nodion v0.1.0/go.mod h1:inbuh3neCtIWlMPZHtEpe43TmRXxHV6+hk97iCZicms= github.com/nrdcg/porkbun v0.1.1 h1:gxVzQYfFUGXhnBax/aVugoE3OIBAdHgrJgyMPyY5Sjo= github.com/nrdcg/porkbun v0.1.1/go.mod h1:JWl/WKnguWos4mjfp4YizvvToigk9qpQwrodOk+CPoA= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -1640,6 +1647,8 @@ github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9Nz github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/simplesurance/bunny-go v0.0.0-20221115111006-e11d9dc91f04 h1:ZTzdx88+AcnjqUfJwnz89UBrMSBQ1NEysg9u5d+dU9c= +github.com/simplesurance/bunny-go v0.0.0-20221115111006-e11d9dc91f04/go.mod h1:5KS21fpch8TIMyAUv/qQqTa3GZfBDYgjaZbd2KXKYfg= github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -1876,8 +1885,9 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -2082,8 +2092,8 @@ golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M= -golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= +golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -2342,8 +2352,9 @@ golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2 h1:CCXrcPKiGGotvnN6jfUsKk4rRqm7q09/YbKb5xCEvtM= @@ -2374,8 +2385,9 @@ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34q google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0 h1:URs6qR1lAxDsqWITsQXI4ZkGiYJ5dHtRNiCpfs2OeKA= google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/api v0.111.0 h1:bwKi+z2BsdwYFRKrqwutM+axAlYLz83gt5pDSXCJT+0= +google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -2437,8 +2449,8 @@ google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20211021150943-2b146023228c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 h1:hrbNEivu7Zn1pxvHk6MBrq9iE22woVILTHqexqBxe6I= -google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20230223222841-637eb2293923 h1:znp6mq/drrY+6khTAlJUDNFFcDGV2ENLYKpMq8SyCds= +google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -2470,9 +2482,8 @@ google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw= -google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= +google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -2486,7 +2497,6 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/DataDog/dd-trace-go.v1 v1.43.1 h1:Dez4VzRQWAI5YXJRBx58BiC0gONGuW/oY4l8fWKzOXY= From 9c73c4c584c4e80f35789b0f9da8793e7fbfc53c Mon Sep 17 00:00:00 2001 From: Romain Date: Tue, 9 May 2023 17:38:05 +0200 Subject: [PATCH 06/10] Enable Prometheus provider cleanup when only the router's metrics level is activated Co-authored-by: Kevin Pollet --- cmd/traefik/traefik.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index c0e78d6eb..746a50b45 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -298,7 +298,7 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err watcher.AddListener(switchRouter(routerFactory, serverEntryPointsTCP, serverEntryPointsUDP)) // Metrics - if metricsRegistry.IsEpEnabled() || metricsRegistry.IsSvcEnabled() { + if metricsRegistry.IsEpEnabled() || metricsRegistry.IsRouterEnabled() || metricsRegistry.IsSvcEnabled() { var eps []string for key := range serverEntryPointsTCP { eps = append(eps, key) From 1522afe2ecc1456a372830db5409b42dc07a3e98 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Wed, 10 May 2023 09:54:05 +0200 Subject: [PATCH 07/10] doc: add logo for GitHub dark mode --- README.md | 6 +++++- docs/content/assets/img/traefik.logo-dark.png | Bin 0 -> 38762 bytes 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 docs/content/assets/img/traefik.logo-dark.png diff --git a/README.md b/README.md index 318a05569..ea0298fbd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@

-Traefik + + + + Traefik +

[![Build Status SemaphoreCI](https://semaphoreci.com/api/v1/containous/traefik/branches/master/shields_badge.svg)](https://semaphoreci.com/containous/traefik) diff --git a/docs/content/assets/img/traefik.logo-dark.png b/docs/content/assets/img/traefik.logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..8b09350e362bcb82f9a2ff3d3540ce1eddc3070c GIT binary patch literal 38762 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 literal 0 HcmV?d00001 From 466d7461b76fc1a292591b450e4bfd3e0c96cc3f Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Wed, 10 May 2023 15:28:05 +0200 Subject: [PATCH 08/10] Split Docker provider --- docs/content/contributing/data-collection.md | 2 - .../include-acme-multiple-domains-example.md | 2 +- ...acme-multiple-domains-from-rule-example.md | 2 +- .../include-acme-single-domain-example.md | 2 +- docs/content/https/tailscale.md | 4 +- docs/content/https/tls.md | 2 +- docs/content/middlewares/http/addprefix.md | 2 +- docs/content/middlewares/http/basicauth.md | 12 +- docs/content/middlewares/http/buffering.md | 12 +- docs/content/middlewares/http/chain.md | 2 +- .../middlewares/http/circuitbreaker.md | 2 +- docs/content/middlewares/http/compress.md | 6 +- docs/content/middlewares/http/contenttype.md | 2 +- docs/content/middlewares/http/digestauth.md | 12 +- docs/content/middlewares/http/errorpages.md | 2 +- docs/content/middlewares/http/forwardauth.md | 20 +- docs/content/middlewares/http/grpcweb.md | 2 +- docs/content/middlewares/http/headers.md | 8 +- docs/content/middlewares/http/inflightreq.md | 12 +- docs/content/middlewares/http/ipallowlist.md | 6 +- docs/content/middlewares/http/overview.md | 2 +- .../middlewares/http/passtlsclientcert.md | 4 +- docs/content/middlewares/http/ratelimit.md | 16 +- .../content/middlewares/http/redirectregex.md | 2 +- .../middlewares/http/redirectscheme.md | 8 +- docs/content/middlewares/http/replacepath.md | 2 +- .../middlewares/http/replacepathregex.md | 2 +- docs/content/middlewares/http/retry.md | 2 +- docs/content/middlewares/http/stripprefix.md | 2 +- .../middlewares/http/stripprefixregex.md | 2 +- docs/content/middlewares/overview.md | 2 +- docs/content/middlewares/tcp/inflightconn.md | 2 +- docs/content/middlewares/tcp/ipallowlist.md | 2 +- docs/content/middlewares/tcp/overview.md | 2 +- docs/content/migration/v1-to-v2.md | 14 +- docs/content/migration/v2-to-v3.md | 7 + .../operations/include-api-examples.md | 2 +- .../operations/include-dashboard-examples.md | 2 +- docs/content/providers/docker.md | 156 +--- docs/content/providers/overview.md | 2 +- docs/content/providers/swarm.md | 697 ++++++++++++++++++ .../reference/dynamic-configuration/docker.md | 2 +- .../dynamic-configuration/docker.yml | 1 - .../reference/dynamic-configuration/swarm.md | 17 + .../reference/dynamic-configuration/swarm.yml | 3 + .../reference/static-configuration/cli-ref.md | 53 +- .../reference/static-configuration/env-ref.md | 53 +- .../reference/static-configuration/file.toml | 18 +- .../reference/static-configuration/file.yaml | 18 +- docs/content/routing/providers/docker.md | 62 -- docs/content/routing/providers/swarm.md | 640 ++++++++++++++++ docs/mkdocs.yml | 3 + pkg/api/handler_overview_test.go | 1 + pkg/api/testdata/overview-providers.json | 1 + pkg/config/dynamic/fixtures/sample.toml | 2 - pkg/config/static/static_config.go | 29 +- pkg/provider/aggregator/aggregator.go | 4 + pkg/provider/docker/config.go | 80 +- pkg/provider/docker/config_test.go | 158 +--- pkg/provider/docker/data.go | 35 + pkg/provider/docker/docker.go | 602 --------------- pkg/provider/docker/pdocker.go | 193 +++++ pkg/provider/docker/pswarm.go | 332 +++++++++ pkg/provider/docker/pswarm_mock_test.go | 49 ++ .../docker/{swarm_test.go => pswarm_test.go} | 51 +- pkg/provider/docker/shared.go | 211 ++++++ .../docker/{label.go => shared_labels.go} | 2 +- pkg/provider/docker/shared_test.go | 112 +++ pkg/redactor/redactor_config_test.go | 56 +- .../testdata/anonymized-static-config.json | 29 +- webui/src/statics/providers/swarm.svg | 6 + 71 files changed, 2677 insertions(+), 1190 deletions(-) create mode 100644 docs/content/providers/swarm.md create mode 100644 docs/content/reference/dynamic-configuration/swarm.md create mode 100644 docs/content/reference/dynamic-configuration/swarm.yml create mode 100644 docs/content/routing/providers/swarm.md create mode 100644 pkg/provider/docker/data.go delete mode 100644 pkg/provider/docker/docker.go create mode 100644 pkg/provider/docker/pdocker.go create mode 100644 pkg/provider/docker/pswarm.go create mode 100644 pkg/provider/docker/pswarm_mock_test.go rename pkg/provider/docker/{swarm_test.go => pswarm_test.go} (86%) create mode 100644 pkg/provider/docker/shared.go rename pkg/provider/docker/{label.go => shared_labels.go} (94%) create mode 100644 pkg/provider/docker/shared_test.go create mode 100644 webui/src/statics/providers/swarm.svg diff --git a/docs/content/contributing/data-collection.md b/docs/content/contributing/data-collection.md index f68f329cc..645170b58 100644 --- a/docs/content/contributing/data-collection.md +++ b/docs/content/contributing/data-collection.md @@ -66,7 +66,6 @@ providers: docker: endpoint: "tcp://10.10.10.10:2375" exposedByDefault: true - swarmMode: true tls: ca: dockerCA @@ -86,7 +85,6 @@ providers: docker: endpoint: "xxxx" exposedByDefault: true - swarmMode: true tls: ca: xxxx diff --git a/docs/content/https/include-acme-multiple-domains-example.md b/docs/content/https/include-acme-multiple-domains-example.md index e60d08f1c..a904b8950 100644 --- a/docs/content/https/include-acme-multiple-domains-example.md +++ b/docs/content/https/include-acme-multiple-domains-example.md @@ -1,5 +1,5 @@ -```yaml tab="Docker" +```yaml tab="Docker & Swarm" ## Dynamic configuration labels: - traefik.http.routers.blog.rule=Host(`example.com`) && Path(`/blog`) diff --git a/docs/content/https/include-acme-multiple-domains-from-rule-example.md b/docs/content/https/include-acme-multiple-domains-from-rule-example.md index 1052228c3..4e27bb84d 100644 --- a/docs/content/https/include-acme-multiple-domains-from-rule-example.md +++ b/docs/content/https/include-acme-multiple-domains-from-rule-example.md @@ -1,5 +1,5 @@ -```yaml tab="Docker" +```yaml tab="Docker & Swarm" ## Dynamic configuration labels: - traefik.http.routers.blog.rule=(Host(`example.com`) && Path(`/blog`)) || Host(`blog.example.org`) diff --git a/docs/content/https/include-acme-single-domain-example.md b/docs/content/https/include-acme-single-domain-example.md index f6fad9af9..7408be5d2 100644 --- a/docs/content/https/include-acme-single-domain-example.md +++ b/docs/content/https/include-acme-single-domain-example.md @@ -1,5 +1,5 @@ -```yaml tab="Docker" +```yaml tab="Docker & Swarm" ## Dynamic configuration labels: - traefik.http.routers.blog.rule=Host(`example.com`) && Path(`/blog`) diff --git a/docs/content/https/tailscale.md b/docs/content/https/tailscale.md index 6cb2f3e6c..d49c314c5 100644 --- a/docs/content/https/tailscale.md +++ b/docs/content/https/tailscale.md @@ -87,7 +87,7 @@ A certificate resolver requests certificates for a set of domain names inferred !!! example "Domain from Router's Rule Example" - ```yaml tab="Docker" + ```yaml tab="Docker & Swarm" ## Dynamic configuration labels: - traefik.http.routers.blog.rule=Host(`monitoring.yak-bebop.ts.net`) && Path(`/metrics`) @@ -141,7 +141,7 @@ A certificate resolver requests certificates for a set of domain names inferred !!! example "Domain from Router's tls.domain Example" - ```yaml tab="Docker" + ```yaml tab="Docker & Swarm" ## Dynamic configuration labels: - traefik.http.routers.blog.rule=Path(`/metrics`) diff --git a/docs/content/https/tls.md b/docs/content/https/tls.md index 296e9b484..b7321413c 100644 --- a/docs/content/https/tls.md +++ b/docs/content/https/tls.md @@ -211,7 +211,7 @@ spec: - bar.example.org ``` -```yaml tab="Docker" +```yaml tab="Docker & Swarm" ## Dynamic configuration labels: - "traefik.tls.stores.default.defaultgeneratedcert.resolver=myresolver" diff --git a/docs/content/middlewares/http/addprefix.md b/docs/content/middlewares/http/addprefix.md index e6f458f31..011a51f2e 100644 --- a/docs/content/middlewares/http/addprefix.md +++ b/docs/content/middlewares/http/addprefix.md @@ -14,7 +14,7 @@ The AddPrefix middleware updates the path of a request before forwarding it. ## Configuration Examples -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Prefixing with /foo labels: - "traefik.http.middlewares.add-foo.addprefix.prefix=/foo" diff --git a/docs/content/middlewares/http/basicauth.md b/docs/content/middlewares/http/basicauth.md index d5118f855..5c77aad19 100644 --- a/docs/content/middlewares/http/basicauth.md +++ b/docs/content/middlewares/http/basicauth.md @@ -14,7 +14,7 @@ The BasicAuth middleware restricts access to your services to known users. ## Configuration Examples -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Declaring the user list # # Note: when used in docker-compose.yml all dollar signs in the hash need to be doubled for escaping. @@ -88,7 +88,7 @@ The `users` option is an array of authorized users. Each user must be declared u Please note that these keys are not hashed or encrypted in any way, and therefore is less secure than other methods. You can find more information on the [Kubernetes Basic Authentication Secret Documentation](https://kubernetes.io/docs/concepts/configuration/secret/#basic-authentication-secret) -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Declaring the user list # # Note: when used in docker-compose.yml all dollar signs in the hash need to be doubled for escaping. @@ -177,7 +177,7 @@ The file content is a list of `name:hashed-password`. - If both `users` and `usersFile` are provided, the two are merged. The contents of `usersFile` have precedence over the values in `users`. - Because it does not make much sense to refer to a file path on Kubernetes, the `usersFile` field doesn't exist for Kubernetes IngressRoute, and one should use the `secret` field instead. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-auth.basicauth.usersfile=/path/to/my/usersfile" ``` @@ -233,7 +233,7 @@ http: You can customize the realm for the authentication with the `realm` option. The default value is `traefik`. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-auth.basicauth.realm=MyRealm" ``` @@ -270,7 +270,7 @@ http: You can define a header field to store the authenticated user using the `headerField`option. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.my-auth.basicauth.headerField=X-WebAuth-User" ``` @@ -309,7 +309,7 @@ http: Set the `removeHeader` option to `true` to remove the authorization header before forwarding the request to your service. (Default value is `false`.) -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-auth.basicauth.removeheader=true" ``` diff --git a/docs/content/middlewares/http/buffering.md b/docs/content/middlewares/http/buffering.md index 85c21a62b..27dc60382 100644 --- a/docs/content/middlewares/http/buffering.md +++ b/docs/content/middlewares/http/buffering.md @@ -18,7 +18,7 @@ This can help services avoid large amounts of data (`multipart/form-data` for ex ## Configuration Examples -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Sets the maximum request body to 2MB labels: - "traefik.http.middlewares.limit.buffering.maxRequestBodyBytes=2000000" @@ -66,7 +66,7 @@ The `maxRequestBodyBytes` option configures the maximum allowed body size for th If the request exceeds the allowed size, it is not forwarded to the service, and the client gets a `413` (Request Entity Too Large) response. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.limit.buffering.maxRequestBodyBytes=2000000" ``` @@ -105,7 +105,7 @@ _Optional, Default=1048576_ You can configure a threshold (in bytes) from which the request will be buffered on disk instead of in memory with the `memRequestBodyBytes` option. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.limit.buffering.memRequestBodyBytes=2000000" ``` @@ -146,7 +146,7 @@ The `maxResponseBodyBytes` option configures the maximum allowed response size f If the response exceeds the allowed size, it is not forwarded to the client. The client gets a `500` (Internal Server Error) response instead. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.limit.buffering.maxResponseBodyBytes=2000000" ``` @@ -185,7 +185,7 @@ _Optional, Default=1048576_ You can configure a threshold (in bytes) from which the response will be buffered on disk instead of in memory with the `memResponseBodyBytes` option. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.limit.buffering.memResponseBodyBytes=2000000" ``` @@ -226,7 +226,7 @@ You can have the Buffering middleware replay the request using `retryExpression` ??? example "Retries once in the case of a network error" - ```yaml tab="Docker" + ```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.limit.buffering.retryExpression=IsNetworkError() && Attempts() < 2" ``` diff --git a/docs/content/middlewares/http/chain.md b/docs/content/middlewares/http/chain.md index de0074426..caf1a84c9 100644 --- a/docs/content/middlewares/http/chain.md +++ b/docs/content/middlewares/http/chain.md @@ -17,7 +17,7 @@ It makes reusing the same groups easier. Below is an example of a Chain containing `AllowList`, `BasicAuth`, and `RedirectScheme`. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.routers.router1.service=service1" - "traefik.http.routers.router1.middlewares=secured" diff --git a/docs/content/middlewares/http/circuitbreaker.md b/docs/content/middlewares/http/circuitbreaker.md index 6edaedbe9..be7b1422d 100644 --- a/docs/content/middlewares/http/circuitbreaker.md +++ b/docs/content/middlewares/http/circuitbreaker.md @@ -30,7 +30,7 @@ To assess if your system is healthy, the circuit breaker constantly monitors the ## Configuration Examples -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Latency Check labels: - "traefik.http.middlewares.latency-check.circuitbreaker.expression=LatencyAtQuantileMS(50.0) > 100" diff --git a/docs/content/middlewares/http/compress.md b/docs/content/middlewares/http/compress.md index 4618fb245..eec8e73b0 100644 --- a/docs/content/middlewares/http/compress.md +++ b/docs/content/middlewares/http/compress.md @@ -15,7 +15,7 @@ The activation of compression, and the compression method choice rely (among oth ## Configuration Examples -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Enable compression labels: - "traefik.http.middlewares.test-compress.compress=true" @@ -82,7 +82,7 @@ Content types are compared in a case-insensitive, whitespace-ignored manner. Note that `application/grpc` is never compressed. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-compress.compress.excludedcontenttypes=text/event-stream" ``` @@ -125,7 +125,7 @@ _Optional, Default=1024_ Responses smaller than the specified values will not be compressed. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-compress.compress.minresponsebodybytes=1200" ``` diff --git a/docs/content/middlewares/http/contenttype.md b/docs/content/middlewares/http/contenttype.md index c4d78a359..f08bccb39 100644 --- a/docs/content/middlewares/http/contenttype.md +++ b/docs/content/middlewares/http/contenttype.md @@ -18,7 +18,7 @@ when it is not set by the backend. ## Configuration Examples -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Enable auto-detection labels: - "traefik.http.middlewares.autodetect.contenttype=true" diff --git a/docs/content/middlewares/http/digestauth.md b/docs/content/middlewares/http/digestauth.md index dcd9ca284..72b868c05 100644 --- a/docs/content/middlewares/http/digestauth.md +++ b/docs/content/middlewares/http/digestauth.md @@ -14,7 +14,7 @@ The DigestAuth middleware restricts access to your services to known users. ## Configuration Examples -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Declaring the user list labels: - "traefik.http.middlewares.test-auth.digestauth.users=test:traefik:a2688e031edb4be6a3797f3882655c05,test2:traefik:518845800f9e2bfb1f1f740ec24f074e" @@ -72,7 +72,7 @@ The `users` option is an array of authorized users. Each user will be declared u - If both `users` and `usersFile` are provided, the two are merged. The contents of `usersFile` have precedence over the values in `users`. - For security reasons, the field `users` doesn't exist for Kubernetes IngressRoute, and one should use the `secret` field instead. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-auth.digestauth.users=test:traefik:a2688e031edb4be6a3797f3882655c05,test2:traefik:518845800f9e2bfb1f1f740ec24f074e" ``` @@ -132,7 +132,7 @@ The file content is a list of `name:realm:encoded-password`. - If both `users` and `usersFile` are provided, the two are merged. The contents of `usersFile` have precedence over the values in `users`. - Because it does not make much sense to refer to a file path on Kubernetes, the `usersFile` field doesn't exist for Kubernetes IngressRoute, and one should use the `secret` field instead. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-auth.digestauth.usersfile=/path/to/my/usersfile" ``` @@ -188,7 +188,7 @@ http: You can customize the realm for the authentication with the `realm` option. The default value is `traefik`. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-auth.digestauth.realm=MyRealm" ``` @@ -225,7 +225,7 @@ http: You can customize the header field for the authenticated user using the `headerField`option. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.my-auth.digestauth.headerField=X-WebAuth-User" ``` @@ -264,7 +264,7 @@ http: Set the `removeHeader` option to `true` to remove the authorization header before forwarding the request to your service. (Default value is `false`.) -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-auth.digestauth.removeheader=true" ``` diff --git a/docs/content/middlewares/http/errorpages.md b/docs/content/middlewares/http/errorpages.md index 0f8b1798a..9456bf978 100644 --- a/docs/content/middlewares/http/errorpages.md +++ b/docs/content/middlewares/http/errorpages.md @@ -18,7 +18,7 @@ The Errors middleware returns a custom page in lieu of the default, according to ## Configuration Examples -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Dynamic Custom Error Page for 5XX Status Code labels: - "traefik.http.middlewares.test-errors.errors.status=500-599" diff --git a/docs/content/middlewares/http/forwardauth.md b/docs/content/middlewares/http/forwardauth.md index 4738747b2..6b69c620b 100644 --- a/docs/content/middlewares/http/forwardauth.md +++ b/docs/content/middlewares/http/forwardauth.md @@ -16,7 +16,7 @@ Otherwise, the response from the authentication server is returned. ## Configuration Examples -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Forward authentication to example.com labels: - "traefik.http.middlewares.test-auth.forwardauth.address=https://example.com/auth" @@ -72,7 +72,7 @@ The following request properties are provided to the forward-auth target endpoin The `address` option defines the authentication server address. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-auth.forwardauth.address=https://example.com/auth" ``` @@ -109,7 +109,7 @@ http: Set the `trustForwardHeader` option to `true` to trust all `X-Forwarded-*` headers. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-auth.forwardauth.trustForwardHeader=true" ``` @@ -150,7 +150,7 @@ http: The `authResponseHeaders` option is the list of headers to copy from the authentication server response and set on forwarded request, replacing any existing conflicting headers. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-auth.forwardauth.authResponseHeaders=X-Auth-User, X-Secret" ``` @@ -197,7 +197,7 @@ set on forwarded request, after stripping all headers that match the regex. It allows partial matching of the regular expression against the header key. The start of string (`^`) and end of string (`$`) anchors should be used to ensure a full match against the header key. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-auth.forwardauth.authResponseHeadersRegex=^X-" ``` @@ -245,7 +245,7 @@ The `authRequestHeaders` option is the list of the headers to copy from the requ It allows filtering headers that should not be passed to the authentication server. If not set or empty then all request headers are passed. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-auth.forwardauth.authRequestHeaders=Accept,X-CustomHeader" ``` @@ -298,7 +298,7 @@ _Optional_ `ca` is the path to the certificate authority used for the secured connection to the authentication server, it defaults to the system bundle. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-auth.forwardauth.tls.ca=path/to/local.crt" ``` @@ -355,7 +355,7 @@ _Optional_ `cert` is the path to the public certificate used for the secure connection to the authentication server. When using this option, setting the `key` option is required. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-auth.forwardauth.tls.cert=path/to/foo.cert" - "traefik.http.middlewares.test-auth.forwardauth.tls.key=path/to/foo.key" @@ -420,7 +420,7 @@ _Optional_ `key` is the path to the private key used for the secure connection to the authentication server. When using this option, setting the `cert` option is required. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-auth.forwardauth.tls.cert=path/to/foo.cert" - "traefik.http.middlewares.test-auth.forwardauth.tls.key=path/to/foo.key" @@ -484,7 +484,7 @@ _Optional, Default=false_ If `insecureSkipVerify` is `true`, the TLS connection to the authentication server accepts any certificate presented by the server regardless of the hostnames it covers. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-auth.forwardauth.tls.insecureSkipVerify=true" ``` diff --git a/docs/content/middlewares/http/grpcweb.md b/docs/content/middlewares/http/grpcweb.md index 8b14214a2..35b4a3ddb 100644 --- a/docs/content/middlewares/http/grpcweb.md +++ b/docs/content/middlewares/http/grpcweb.md @@ -17,7 +17,7 @@ The GrpcWeb middleware converts gRPC Web requests to HTTP/2 gRPC requests before ## Configuration Examples -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-grpcweb.grpcweb.allowOrigins=*" ``` diff --git a/docs/content/middlewares/http/headers.md b/docs/content/middlewares/http/headers.md index f0dc6e7c2..4c30b349e 100644 --- a/docs/content/middlewares/http/headers.md +++ b/docs/content/middlewares/http/headers.md @@ -20,7 +20,7 @@ A set of forwarded headers are automatically added by default. See the [FAQ](../ The following example adds the `X-Script-Name` header to the proxied request and the `X-Custom-Response-Header` header to the response -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.testHeader.headers.customrequestheaders.X-Script-Name=test" - "traefik.http.middlewares.testHeader.headers.customresponseheaders.X-Custom-Response-Header=value" @@ -69,7 +69,7 @@ http: In the following example, requests are proxied with an extra `X-Script-Name` header while their `X-Custom-Request-Header` header gets stripped, and responses are stripped of their `X-Custom-Response-Header` header. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.testheader.headers.customrequestheaders.X-Script-Name=test" - "traefik.http.middlewares.testheader.headers.customrequestheaders.X-Custom-Request-Header=" @@ -123,7 +123,7 @@ http: Security-related headers (HSTS headers, Browser XSS filter, etc) can be managed similarly to custom headers as shown above. This functionality makes it possible to easily use security features by adding headers. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.testHeader.headers.framedeny=true" - "traefik.http.middlewares.testHeader.headers.browserxssfilter=true" @@ -170,7 +170,7 @@ instead the response will be generated and sent back to the client directly. Please note that the example below is by no means authoritative or exhaustive, and should not be used as is for production. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.testheader.headers.accesscontrolallowmethods=GET,OPTIONS,PUT" - "traefik.http.middlewares.testheader.headers.accesscontrolallowheaders=*" diff --git a/docs/content/middlewares/http/inflightreq.md b/docs/content/middlewares/http/inflightreq.md index b7eefd84d..e7b7ef695 100644 --- a/docs/content/middlewares/http/inflightreq.md +++ b/docs/content/middlewares/http/inflightreq.md @@ -14,7 +14,7 @@ To proactively prevent services from being overwhelmed with high load, the numbe ## Configuration Examples -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-inflightreq.inflightreq.amount=10" ``` @@ -57,7 +57,7 @@ http: The `amount` option defines the maximum amount of allowed simultaneous in-flight request. The middleware responds with `HTTP 429 Too Many Requests` if there are already `amount` requests in progress (based on the same `sourceCriterion` strategy). -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-inflightreq.inflightreq.amount=10" ``` @@ -122,7 +122,7 @@ The `depth` option tells Traefik to use the `X-Forwarded-For` header and select | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `3` | `"11.0.0.1"` | | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `5` | `""` | -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.ipstrategy.depth=2" ``` @@ -176,7 +176,7 @@ http: | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `"15.0.0.1,16.0.0.1"` | `"13.0.0.1"` | | `"10.0.0.1,11.0.0.1"` | `"10.0.0.1,11.0.0.1"` | `""` | -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.ipstrategy.excludedips=127.0.0.1/32, 192.168.1.7" ``` @@ -222,7 +222,7 @@ http: Name of the header used to group incoming requests. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.requestheadername=username" ``` @@ -262,7 +262,7 @@ http: Whether to consider the request host as the source. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-inflightreq.inflightreq.sourcecriterion.requesthost=true" ``` diff --git a/docs/content/middlewares/http/ipallowlist.md b/docs/content/middlewares/http/ipallowlist.md index 8892b4fcd..d62e253bb 100644 --- a/docs/content/middlewares/http/ipallowlist.md +++ b/docs/content/middlewares/http/ipallowlist.md @@ -12,7 +12,7 @@ IPAllowList accepts / refuses requests based on the client IP. ## Configuration Examples -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Accepts request from defined IP labels: - "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcerange=127.0.0.1/32, 192.168.1.7" @@ -83,7 +83,7 @@ The `depth` option tells Traefik to use the `X-Forwarded-For` header and take th | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `3` | `"11.0.0.1"` | | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `5` | `""` | -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Allowlisting Based on `X-Forwarded-For` with `depth=2` labels: - "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcerange=127.0.0.1/32, 192.168.1.7" @@ -149,7 +149,7 @@ http: | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `"15.0.0.1,16.0.0.1"` | `"13.0.0.1"` | | `"10.0.0.1,11.0.0.1"` | `"10.0.0.1,11.0.0.1"` | `""` | -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Exclude from `X-Forwarded-For` labels: - "traefik.http.middlewares.test-ipallowlist.ipallowlist.ipstrategy.excludedips=127.0.0.1/32, 192.168.1.7" diff --git a/docs/content/middlewares/http/overview.md b/docs/content/middlewares/http/overview.md index 500e5d38d..17b2ab1cf 100644 --- a/docs/content/middlewares/http/overview.md +++ b/docs/content/middlewares/http/overview.md @@ -12,7 +12,7 @@ Controlling connections ## Configuration Example -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # As a Docker Label whoami: # A container that exposes an API to show its IP address diff --git a/docs/content/middlewares/http/passtlsclientcert.md b/docs/content/middlewares/http/passtlsclientcert.md index 92c5cc83b..8f74647d6 100644 --- a/docs/content/middlewares/http/passtlsclientcert.md +++ b/docs/content/middlewares/http/passtlsclientcert.md @@ -18,7 +18,7 @@ PassTLSClientCert adds the selected data from the passed client TLS certificate Pass the pem in the `X-Forwarded-Tls-Client-Cert` header. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Pass the pem in the `X-Forwarded-Tls-Client-Cert` header. labels: - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.pem=true" @@ -57,7 +57,7 @@ http: ??? example "Pass the pem in the `X-Forwarded-Tls-Client-Cert` header" - ```yaml tab="Docker" + ```yaml tab="Docker & Swarm" # Pass all the available info in the `X-Forwarded-Tls-Client-Cert-Info` header labels: - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.notafter=true" diff --git a/docs/content/middlewares/http/ratelimit.md b/docs/content/middlewares/http/ratelimit.md index 9ae94a31a..2359b1796 100644 --- a/docs/content/middlewares/http/ratelimit.md +++ b/docs/content/middlewares/http/ratelimit.md @@ -14,7 +14,7 @@ It is based on a [token bucket](https://en.wikipedia.org/wiki/Token_bucket) impl ## Configuration Example -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Here, an average of 100 requests per second is allowed. # In addition, a burst of 50 requests is allowed. labels: @@ -73,7 +73,7 @@ It defaults to `0`, which means no rate limiting. The rate is actually defined by dividing `average` by `period`. So for a rate below 1 req/s, one needs to define a `period` larger than a second. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # 100 reqs/s labels: - "traefik.http.middlewares.test-ratelimit.ratelimit.average=100" @@ -121,7 +121,7 @@ r = average / period It defaults to `1` second. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # 6 reqs/minute labels: - "traefik.http.middlewares.test-ratelimit.ratelimit.average=6" @@ -170,7 +170,7 @@ http: It defaults to `1`. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-ratelimit.ratelimit.burst=100" ``` @@ -232,7 +232,7 @@ The `depth` option tells Traefik to use the `X-Forwarded-For` header and select | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `3` | `"11.0.0.1"` | | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `5` | `""` | -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.ipstrategy.depth=2" ``` @@ -313,7 +313,7 @@ and the first IP that is _not_ in the pool (if any) is returned. | `"10.0.0.1,11.0.0.1,13.0.0.1"` | `"15.0.0.1,16.0.0.1"` | `"13.0.0.1"` | | `"10.0.0.1,11.0.0.1"` | `"10.0.0.1,11.0.0.1"` | `""` | -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.ipstrategy.excludedips=127.0.0.1/32, 192.168.1.7" ``` @@ -359,7 +359,7 @@ http: Name of the header used to group incoming requests. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.requestheadername=username" ``` @@ -399,7 +399,7 @@ http: Whether to consider the request host as the source. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.requesthost=true" ``` diff --git a/docs/content/middlewares/http/redirectregex.md b/docs/content/middlewares/http/redirectregex.md index d2e146673..2788a3c6f 100644 --- a/docs/content/middlewares/http/redirectregex.md +++ b/docs/content/middlewares/http/redirectregex.md @@ -16,7 +16,7 @@ The RedirectRegex redirects a request using regex matching and replacement. ## Configuration Examples -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Redirect with domain replacement # Note: all dollar signs need to be doubled for escaping. labels: diff --git a/docs/content/middlewares/http/redirectscheme.md b/docs/content/middlewares/http/redirectscheme.md index e32fecfbc..793d28b17 100644 --- a/docs/content/middlewares/http/redirectscheme.md +++ b/docs/content/middlewares/http/redirectscheme.md @@ -25,7 +25,7 @@ The RedirectScheme middleware redirects the request if the request scheme is dif ## Configuration Examples -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Redirect to https labels: - "traefik.http.middlewares.test-redirectscheme.redirectscheme.scheme=https" @@ -75,7 +75,7 @@ http: Set the `permanent` option to `true` to apply a permanent redirection. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Redirect to https labels: # ... @@ -123,7 +123,7 @@ http: The `scheme` option defines the scheme of the new URL. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Redirect to https labels: - "traefik.http.middlewares.test-redirectscheme.redirectscheme.scheme=https" @@ -166,7 +166,7 @@ http: The `port` option defines the port of the new URL. -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Redirect to https labels: # ... diff --git a/docs/content/middlewares/http/replacepath.md b/docs/content/middlewares/http/replacepath.md index 2c44e9f75..e7024dffe 100644 --- a/docs/content/middlewares/http/replacepath.md +++ b/docs/content/middlewares/http/replacepath.md @@ -16,7 +16,7 @@ Replace the path of the request URL. ## Configuration Examples -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Replace the path with /foo labels: - "traefik.http.middlewares.test-replacepath.replacepath.path=/foo" diff --git a/docs/content/middlewares/http/replacepathregex.md b/docs/content/middlewares/http/replacepathregex.md index 562d3d2f3..7b0f956d3 100644 --- a/docs/content/middlewares/http/replacepathregex.md +++ b/docs/content/middlewares/http/replacepathregex.md @@ -16,7 +16,7 @@ The ReplaceRegex replaces the path of a URL using regex matching and replacement ## Configuration Examples -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Replace path with regex labels: - "traefik.http.middlewares.test-replacepathregex.replacepathregex.regex=^/foo/(.*)" diff --git a/docs/content/middlewares/http/retry.md b/docs/content/middlewares/http/retry.md index 9706ec69f..c8ae38d22 100644 --- a/docs/content/middlewares/http/retry.md +++ b/docs/content/middlewares/http/retry.md @@ -18,7 +18,7 @@ The Retry middleware has an optional configuration to enable an exponential back ## Configuration Examples -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Retry 4 times with exponential backoff labels: - "traefik.http.middlewares.test-retry.retry.attempts=4" diff --git a/docs/content/middlewares/http/stripprefix.md b/docs/content/middlewares/http/stripprefix.md index 4d0d7d567..5da2543b2 100644 --- a/docs/content/middlewares/http/stripprefix.md +++ b/docs/content/middlewares/http/stripprefix.md @@ -16,7 +16,7 @@ Remove the specified prefixes from the URL path. ## Configuration Examples -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Strip prefix /foobar and /fiibar labels: - "traefik.http.middlewares.test-stripprefix.stripprefix.prefixes=/foobar,/fiibar" diff --git a/docs/content/middlewares/http/stripprefixregex.md b/docs/content/middlewares/http/stripprefixregex.md index ba61de6fc..1e8b10ee8 100644 --- a/docs/content/middlewares/http/stripprefixregex.md +++ b/docs/content/middlewares/http/stripprefixregex.md @@ -12,7 +12,7 @@ Remove the matching prefixes from the URL path. ## Configuration Examples -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-stripprefixregex.stripprefixregex.regex=/foo/[a-z0-9]+/[0-9]+/" ``` diff --git a/docs/content/middlewares/overview.md b/docs/content/middlewares/overview.md index c01423123..ca9ae3e6b 100644 --- a/docs/content/middlewares/overview.md +++ b/docs/content/middlewares/overview.md @@ -23,7 +23,7 @@ Middlewares that use the same protocol can be combined into chains to fit every ## Configuration Example -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # As a Docker Label whoami: # A container that exposes an API to show its IP address diff --git a/docs/content/middlewares/tcp/inflightconn.md b/docs/content/middlewares/tcp/inflightconn.md index f9993a7f8..38ad09f65 100644 --- a/docs/content/middlewares/tcp/inflightconn.md +++ b/docs/content/middlewares/tcp/inflightconn.md @@ -7,7 +7,7 @@ To proactively prevent services from being overwhelmed with high load, the numbe ## Configuration Examples -```yaml tab="Docker" +```yaml tab="Docker & Swarm" labels: - "traefik.tcp.middlewares.test-inflightconn.inflightconn.amount=10" ``` diff --git a/docs/content/middlewares/tcp/ipallowlist.md b/docs/content/middlewares/tcp/ipallowlist.md index 8aa5be01c..e8466b94e 100644 --- a/docs/content/middlewares/tcp/ipallowlist.md +++ b/docs/content/middlewares/tcp/ipallowlist.md @@ -12,7 +12,7 @@ IPAllowList accepts / refuses connections based on the client IP. ## Configuration Examples -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Accepts connections from defined IP labels: - "traefik.tcp.middlewares.test-ipallowlist.ipallowlist.sourcerange=127.0.0.1/32, 192.168.1.7" diff --git a/docs/content/middlewares/tcp/overview.md b/docs/content/middlewares/tcp/overview.md index ec28af85d..5572160cb 100644 --- a/docs/content/middlewares/tcp/overview.md +++ b/docs/content/middlewares/tcp/overview.md @@ -12,7 +12,7 @@ Controlling connections ## Configuration Example -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # As a Docker Label whoami: # A container that exposes an API to show its IP address diff --git a/docs/content/migration/v1-to-v2.md b/docs/content/migration/v1-to-v2.md index 8d93e48f3..31cf05e8b 100644 --- a/docs/content/migration/v1-to-v2.md +++ b/docs/content/migration/v1-to-v2.md @@ -38,7 +38,7 @@ Then any router can refer to an instance of the wanted middleware. !!! info "v1" - ```yaml tab="Docker" + ```yaml tab="Docker & Swarm" labels: - "traefik.frontend.rule=Host:test.localhost;PathPrefix:/test" - "traefik.frontend.auth.basic.users=test:$$apr1$$H6uskkkW$$IgXLP6ewTrSuBkTrqE8wj/,test2:$$apr1$$d9hr9HBB$$4HxwgUir3HP4EsggP/QNo0" @@ -100,7 +100,7 @@ Then any router can refer to an instance of the wanted middleware. !!! info "v2" - ```yaml tab="Docker" + ```yaml tab="Docker & Swarm" labels: - "traefik.http.routers.router0.rule=Host(`test.localhost`) && PathPrefix(`/test`)" - "traefik.http.routers.router0.middlewares=auth" @@ -317,7 +317,7 @@ Then, a [router's TLS field](../routing/routers/index.md#tls) can refer to one o namespace: default ``` - ```yaml tab="Docker" + ```yaml tab="Docker & Swarm" labels: # myTLSOptions must be defined by another provider, in this instance in the File Provider. # see the cross provider section @@ -428,7 +428,7 @@ To apply a redirection: !!! info "v2" - ```yaml tab="Docker" + ```yaml tab="Docker & Swarm" labels: traefik.http.routers.app.rule: Host(`example.net`) traefik.http.routers.app.entrypoints: web @@ -556,7 +556,7 @@ with the path `/admin` stripped, e.g. to `http://:/`. In this case, yo !!! info "v1" - ```yaml tab="Docker" + ```yaml tab="Docker & Swarm" labels: - "traefik.frontend.rule=Host:example.org;PathPrefixStrip:/admin" ``` @@ -588,7 +588,7 @@ with the path `/admin` stripped, e.g. to `http://:/`. In this case, yo !!! info "v2" - ```yaml tab="Docker" + ```yaml tab="Docker & Swarm" labels: - "traefik.http.routers.admin.rule=Host(`example.org`) && PathPrefix(`/admin`)" - "traefik.http.routers.admin.middlewares=admin-stripprefix" @@ -1044,7 +1044,7 @@ To activate the dashboard, you can either: !!! info "v2" - ```yaml tab="Docker" + ```yaml tab="Docker & Swarm" # dynamic configuration labels: - "traefik.http.routers.api.rule=Host(`traefik.docker.localhost`)" diff --git a/docs/content/migration/v2-to-v3.md b/docs/content/migration/v2-to-v3.md index 4792467b0..6de93ca9c 100644 --- a/docs/content/migration/v2-to-v3.md +++ b/docs/content/migration/v2-to-v3.md @@ -87,3 +87,10 @@ In v3, the InfluxDB v1 metrics provider has been removed because InfluxDB v1.x m In v3 the Kubernetes CRDs API Group `traefik.containo.us` has been removed. Please use the API Group `traefik.io` instead. + +## Docker & Docker Swarm + +In v3, the provider Docker has been split into 2 providers: + +- Docker provider (without Swarm support) +- Swarm provider (Swarm support only) diff --git a/docs/content/operations/include-api-examples.md b/docs/content/operations/include-api-examples.md index d98db97a9..1f5418bbb 100644 --- a/docs/content/operations/include-api-examples.md +++ b/docs/content/operations/include-api-examples.md @@ -1,4 +1,4 @@ -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Dynamic Configuration labels: - "traefik.http.routers.api.rule=Host(`traefik.example.com`)" diff --git a/docs/content/operations/include-dashboard-examples.md b/docs/content/operations/include-dashboard-examples.md index 5965d7070..6ee184709 100644 --- a/docs/content/operations/include-dashboard-examples.md +++ b/docs/content/operations/include-dashboard-examples.md @@ -1,4 +1,4 @@ -```yaml tab="Docker" +```yaml tab="Docker & Swarm" # Dynamic Configuration labels: - "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))" diff --git a/docs/content/providers/docker.md b/docs/content/providers/docker.md index 177a39702..3d6ce6b72 100644 --- a/docs/content/providers/docker.md +++ b/docs/content/providers/docker.md @@ -12,8 +12,7 @@ A Story of Labels & Containers Attach labels to your containers and let Traefik do the rest! -Traefik works with both [Docker (standalone) Engine](https://docs.docker.com/engine/) -and [Docker Swarm Mode](https://docs.docker.com/engine/swarm/). +This provider works with [Docker (standalone) Engine](https://docs.docker.com/engine/). !!! tip "The Quick Start Uses Docker" @@ -49,49 +48,6 @@ and [Docker Swarm Mode](https://docs.docker.com/engine/swarm/). - traefik.http.routers.my-container.rule=Host(`example.com`) ``` -??? example "Configuring Docker Swarm & Deploying / Exposing Services" - - Enabling the docker provider (Swarm Mode) - - ```yaml tab="File (YAML)" - providers: - docker: - # swarm classic (1.12-) - # endpoint: "tcp://127.0.0.1:2375" - # docker swarm mode (1.12+) - endpoint: "tcp://127.0.0.1:2377" - swarmMode: true - ``` - - ```toml tab="File (TOML)" - [providers.docker] - # swarm classic (1.12-) - # endpoint = "tcp://127.0.0.1:2375" - # docker swarm mode (1.12+) - endpoint = "tcp://127.0.0.1:2377" - swarmMode = true - ``` - - ```bash tab="CLI" - # swarm classic (1.12-) - # --providers.docker.endpoint=tcp://127.0.0.1:2375 - # docker swarm mode (1.12+) - --providers.docker.endpoint=tcp://127.0.0.1:2377 - --providers.docker.swarmMode=true - ``` - - Attach labels to services (not to containers) while in Swarm mode (in your docker compose file) - - ```yaml - version: "3" - services: - my-container: - deploy: - labels: - - traefik.http.routers.my-container.rule=Host(`example.com`) - - traefik.http.services.my-container-service.loadbalancer.server.port=8080 - ``` - ## Routing Configuration When using Docker as a [provider](./overview.md), @@ -124,14 +80,13 @@ Port detection works as follows: - If a container [exposes](https://docs.docker.com/engine/reference/builder/#expose) multiple ports, or does not expose any port, then you must manually specify which port Traefik should use for communication by using the label `traefik.http.services..loadbalancer.server.port` - (Read more on this label in the dedicated section in [routing](../routing/providers/docker.md#port)). + (Read more on this label in the dedicated section in [routing](../routing/providers/docker.md#services)). ### Host networking When exposing containers that are configured with [host networking](https://docs.docker.com/network/host/), the IP address of the host is resolved as follows: - - try a lookup of `host.docker.internal` - if the lookup was unsuccessful, try a lookup of `host.containers.internal`, ([Podman](https://docs.podman.io/en/latest/) equivalent of `host.docker.internal`) - if that lookup was also unsuccessful, fall back to `127.0.0.1` @@ -175,7 +130,6 @@ You can specify which Docker API Endpoint to use with the directive [`endpoint`] - Authorization with the [Docker Authorization Plugin Mechanism](https://web.archive.org/web/20190920092526/https://docs.docker.com/engine/extend/plugins_authorization/) - Accounting at networking level, by exposing the socket only inside a Docker private network, only available for Traefik. - Accounting at container level, by exposing the socket on a another container than Traefik's. - With Swarm mode, it allows scheduling of Traefik on worker nodes, with only the "socket exposer" container on the manager nodes. - Accounting at kernel level, by enforcing kernel calls with mechanisms like [SELinux](https://en.wikipedia.org/wiki/Security-Enhanced_Linux), to only allows an identified set of actions for Traefik's process (or the "socket exposer" process). - SSH public key authentication (SSH is supported with Docker > 18.09) @@ -192,69 +146,13 @@ You can specify which Docker API Endpoint to use with the directive [`endpoint`] - [Letting Traefik run on Worker Nodes](https://blog.mikesir87.io/2018/07/letting-traefik-run-on-worker-nodes/) - [Docker Socket Proxy from Tecnativa](https://github.com/Tecnativa/docker-socket-proxy) -## Docker Swarm Mode - -To enable Docker Swarm (instead of standalone Docker) as a configuration provider, -set the [`swarmMode`](#swarmmode) directive to `true`. - -### Routing Configuration with Labels - -While in Swarm Mode, Traefik uses labels found on services, not on individual containers. - -Therefore, if you use a compose file with Swarm Mode, labels should be defined in the -[`deploy`](https://docs.docker.com/compose/compose-file/compose-file-v3/#labels-1) part of your service. - -This behavior is only enabled for docker-compose version 3+ ([Compose file reference](https://docs.docker.com/compose/compose-file/compose-file-v3/)). - -### Port Detection - -Docker Swarm does not provide any [port detection](#port-detection) information to Traefik. - -Therefore, you **must** specify the port to use for communication by using the label `traefik.http.services..loadbalancer.server.port` -(Check the reference for this label in the [routing section for Docker](../routing/providers/docker.md#port)). - -### Docker API Access - -Docker Swarm Mode follows the same rules as Docker [API Access](#docker-api-access). - -Since the Swarm API is only exposed on the [manager nodes](https://docs.docker.com/engine/swarm/how-swarm-mode-works/nodes/#manager-nodes), -these are the nodes that Traefik should be scheduled on by deploying Traefik with a constraint on the node "role": - -```shell tab="With Docker CLI" -docker service create \ - --constraint=node.role==manager \ - #... \ -``` - -```yml tab="With Docker Compose" -version: '3' - -services: - traefik: - # ... - deploy: - placement: - constraints: - - node.role == manager -``` - -!!! tip "Scheduling Traefik on Worker Nodes" - - Following the guidelines given in the previous section ["Docker API Access"](#docker-api-access), - if you expose the Docker API through TCP, then Traefik can be scheduled on any node if the TCP - socket is reachable. - - Please consider the security implications by reading the [Security Note](#security-note). - - A good example can be found on [Bret Fisher's repository](https://github.com/BretFisher/dogvscat/blob/master/stack-proxy-global.yml#L124). - ## Provider Configuration ### `endpoint` _Required, Default="unix:///var/run/docker.sock"_ -See the sections [Docker API Access](#docker-api-access) and [Docker Swarm API Access](#docker-api-access_1) for more information. +See the [Docker API Access](#docker-api-access) section for more information. ??? example "Using the docker.sock" @@ -464,54 +362,6 @@ providers: # ... ``` -### `swarmMode` - -_Optional, Default=false_ - -Enables the Swarm Mode (instead of standalone Docker). - -```yaml tab="File (YAML)" -providers: - docker: - swarmMode: true - # ... -``` - -```toml tab="File (TOML)" -[providers.docker] - swarmMode = true - # ... -``` - -```bash tab="CLI" ---providers.docker.swarmMode=true -# ... -``` - -### `swarmModeRefreshSeconds` - -_Optional, Default=15_ - -Defines the polling interval (in seconds) for Swarm Mode. - -```yaml tab="File (YAML)" -providers: - docker: - swarmModeRefreshSeconds: 30 - # ... -``` - -```toml tab="File (TOML)" -[providers.docker] - swarmModeRefreshSeconds = 30 - # ... -``` - -```bash tab="CLI" ---providers.docker.swarmModeRefreshSeconds=30 -# ... -``` - ### `httpClientTimeout` _Optional, Default=0_ diff --git a/docs/content/providers/overview.md b/docs/content/providers/overview.md index fbb133589..02d2ec22f 100644 --- a/docs/content/providers/overview.md +++ b/docs/content/providers/overview.md @@ -72,7 +72,7 @@ For the list of the providers names, see the [supported providers](#supported-pr Using the add-foo-prefix middleware from other providers: - ```yaml tab="Docker" + ```yaml tab="Docker & Swarm" your-container: # image: your-docker-image diff --git a/docs/content/providers/swarm.md b/docs/content/providers/swarm.md new file mode 100644 index 000000000..96c987c87 --- /dev/null +++ b/docs/content/providers/swarm.md @@ -0,0 +1,697 @@ +--- +title: "Traefik Docker Swarm Documentation" +description: "Learn how to achieve configuration discovery in Traefik through Docker Swarm. Read the technical documentation." +--- + +# Traefik & Docker Swarm + +A Story of Labels & Containers +{: .subtitle } + +![Docker](../assets/img/providers/docker.png) + +Attach labels to your containers and let Traefik do the rest! + +This provider works with [Docker Swarm Mode](https://docs.docker.com/engine/swarm/). + +!!! tip "The Quick Start Uses Docker" + + If you have not already read it, maybe you would like to go through the [quick start guide](../getting-started/quick-start.md) that uses the Docker provider. + +## Configuration Examples + +??? example "Configuring Docker Swarm & Deploying / Exposing Services" + + Enabling the Swarm provider + + ```yaml tab="File (YAML)" + providers: + swarm: + # swarm classic (1.12-) + # endpoint: "tcp://127.0.0.1:2375" + # docker swarm mode (1.12+) + endpoint: "tcp://127.0.0.1:2377" + ``` + + ```toml tab="File (TOML)" + [providers.swarm] + # swarm classic (1.12-) + # endpoint = "tcp://127.0.0.1:2375" + # docker swarm mode (1.12+) + endpoint = "tcp://127.0.0.1:2377" + ``` + + ```bash tab="CLI" + # swarm classic (1.12-) + # --providers.swarm.endpoint=tcp://127.0.0.1:2375 + # docker swarm mode (1.12+) + --providers.swarm.endpoint=tcp://127.0.0.1:2377 + ``` + + Attach labels to services (not to containers) while in Swarm mode (in your docker compose file) + + ```yaml + version: "3" + services: + my-container: + deploy: + labels: + - traefik.http.routers.my-container.rule=Host(`example.com`) + - traefik.http.services.my-container-service.loadbalancer.server.port=8080 + ``` + +## Routing Configuration + +When using Docker as a [provider](./overview.md), +Traefik uses [container labels](https://docs.docker.com/engine/reference/commandline/run/#label) to retrieve its routing configuration. + +See the list of labels in the dedicated [routing](../routing/providers/docker.md) section. + +### Routing Configuration with Labels + +By default, Traefik watches for [container level labels](https://docs.docker.com/config/labels-custom-metadata/) on a standalone Docker Engine. + +When using Docker Compose, labels are specified by the directive +[`labels`](https://docs.docker.com/compose/compose-file/compose-file-v3/#labels) from the +["services" objects](https://docs.docker.com/compose/compose-file/compose-file-v3/#service-configuration-reference). + +!!! tip "Not Only Docker" + + Please note that any tool like Nomad, Terraform, Ansible, etc. + that is able to define a Docker container with labels can work + with Traefik and the Swarm provider. + +While in Swarm Mode, Traefik uses labels found on services, not on individual containers. + +Therefore, if you use a compose file with Swarm Mode, labels should be defined in the +[`deploy`](https://docs.docker.com/compose/compose-file/compose-file-v3/#labels-1) part of your service. + +This behavior is only enabled for docker-compose version 3+ ([Compose file reference](https://docs.docker.com/compose/compose-file/compose-file-v3/)). + +### Port Detection + +Traefik retrieves the private IP and port of containers from the Docker API. + +Docker Swarm does not provide any port detection information to Traefik. + +Therefore, you **must** specify the port to use for communication by using the label `traefik.http.services..loadbalancer.server.port` +(Check the reference for this label in the [routing section for Swarm](../routing/providers/swarm.md#services)). + +### Host networking + +When exposing containers that are configured with [host networking](https://docs.docker.com/network/host/), +the IP address of the host is resolved as follows: + + +- try a lookup of `host.docker.internal` +- if the lookup was unsuccessful, try a lookup of `host.containers.internal`, ([Podman](https://docs.podman.io/en/latest/) equivalent of `host.docker.internal`) +- if that lookup was also unsuccessful, fall back to `127.0.0.1` + +On Linux, for versions of Docker older than 20.10.0, for `host.docker.internal` to be defined, it should be provided +as an `extra_host` to the Traefik container, using the `--add-host` flag. For example, to set it to the IP address of +the bridge interface (`docker0` by default): `--add-host=host.docker.internal:172.17.0.1` + +### IPv4 && IPv6 + +When using a docker stack that uses IPv6, +Traefik will use the IPv4 container IP before its IPv6 counterpart. +Therefore, on an IPv6 Docker stack, +Traefik will use the IPv6 container IP. + +### Docker API Access + +Traefik requires access to the docker socket to get its dynamic configuration. + +You can specify which Docker API Endpoint to use with the directive [`endpoint`](#endpoint). + +!!! warning "Security Note" + + Accessing the Docker API without any restriction is a security concern: + If Traefik is attacked, then the attacker might get access to the underlying host. + {: #security-note } + + As explained in the [Docker Daemon Attack Surface documentation](https://docs.docker.com/engine/security/#docker-daemon-attack-surface): + + !!! quote + + [...] only **trusted** users should be allowed to control your Docker daemon [...] + + ??? success "Solutions" + + Expose the Docker socket over TCP or SSH, instead of the default Unix socket file. + It allows different implementation levels of the [AAA (Authentication, Authorization, Accounting) concepts](https://en.wikipedia.org/wiki/AAA_(computer_security)), depending on your security assessment: + + - Authentication with Client Certificates as described in ["Protect the Docker daemon socket."](https://docs.docker.com/engine/security/protect-access/) + - Authorize and filter requests to restrict possible actions with [the TecnativaDocker Socket Proxy](https://github.com/Tecnativa/docker-socket-proxy). + - Authorization with the [Docker Authorization Plugin Mechanism](https://web.archive.org/web/20190920092526/https://docs.docker.com/engine/extend/plugins_authorization/) + - Accounting at networking level, by exposing the socket only inside a Docker private network, only available for Traefik. + - Accounting at container level, by exposing the socket on a another container than Traefik's. + It allows scheduling of Traefik on worker nodes, with only the "socket exposer" container on the manager nodes. + - Accounting at kernel level, by enforcing kernel calls with mechanisms like [SELinux](https://en.wikipedia.org/wiki/Security-Enhanced_Linux), to only allows an identified set of actions for Traefik's process (or the "socket exposer" process). + - SSH public key authentication (SSH is supported with Docker > 18.09) + + ??? info "More Resources and Examples" + + - ["Paranoid about mounting /var/run/docker.sock?"](https://medium.com/@containeroo/traefik-2-0-paranoid-about-mounting-var-run-docker-sock-22da9cb3e78c) + - [Traefik and Docker: A Discussion with Docker Captain, Bret Fisher](https://blog.traefik.io/traefik-and-docker-a-discussion-with-docker-captain-bret-fisher-7f0b9a54ff88) + - [KubeCon EU 2018 Keynote, Running with Scissors, from Liz Rice](https://www.youtube.com/watch?v=ltrV-Qmh3oY) + - [Don't expose the Docker socket (not even to a container)](https://www.lvh.io/posts/dont-expose-the-docker-socket-not-even-to-a-container/) + - [A thread on Stack Overflow about sharing the `/var/run/docker.sock` file](https://news.ycombinator.com/item?id=17983623) + - [To DinD or not to DinD](https://blog.loof.fr/2018/01/to-dind-or-not-do-dind.html) + - [Traefik issue GH-4174 about security with Docker socket](https://github.com/traefik/traefik/issues/4174) + - [Inspecting Docker Activity with Socat](https://developers.redhat.com/blog/2015/02/25/inspecting-docker-activity-with-socat/) + - [Letting Traefik run on Worker Nodes](https://blog.mikesir87.io/2018/07/letting-traefik-run-on-worker-nodes/) + - [Docker Socket Proxy from Tecnativa](https://github.com/Tecnativa/docker-socket-proxy) + +Since the Swarm API is only exposed on the [manager nodes](https://docs.docker.com/engine/swarm/how-swarm-mode-works/nodes/#manager-nodes), +these are the nodes that Traefik should be scheduled on by deploying Traefik with a constraint on the node "role": + +```shell tab="With Docker CLI" +docker service create \ + --constraint=node.role==manager \ + #... \ +``` + +```yml tab="With Docker Compose" +version: '3' + +services: + traefik: + # ... + deploy: + placement: + constraints: + - node.role == manager +``` + +!!! tip "Scheduling Traefik on Worker Nodes" + + Following the guidelines given in the previous section ["Docker API Access"](#docker-api-access), + if you expose the Docker API through TCP, then Traefik can be scheduled on any node if the TCP + socket is reachable. + + Please consider the security implications by reading the [Security Note](#security-note). + + A good example can be found on [Bret Fisher's repository](https://github.com/BretFisher/dogvscat/blob/master/stack-proxy-global.yml#L124). + +### `endpoint` + +_Required, Default="unix:///var/run/docker.sock"_ + +See the [Docker Swarm API Access](#docker-api-access) section for more information. + +??? example "Using the docker.sock" + + The docker-compose file shares the docker sock with the Traefik container + + ```yaml + version: '3' + + services: + traefik: + image: traefik:v3.0 # The official v2 Traefik docker image + ports: + - "80:80" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + ``` + + We specify the docker.sock in traefik's configuration file. + + ```yaml tab="File (YAML)" + providers: + swarm: + endpoint: "unix:///var/run/docker.sock" + # ... + ``` + + ```toml tab="File (TOML)" + [providers.swarm] + endpoint = "unix:///var/run/docker.sock" + # ... + ``` + + ```bash tab="CLI" + --providers.swarm.endpoint=unix:///var/run/docker.sock + # ... + ``` + +??? example "Using SSH" + + Using Docker 18.09+ you can connect Traefik to daemon using SSH + We specify the SSH host and user in Traefik's configuration file. + Note that is server requires public keys for authentication you must have those accessible for user who runs Traefik. + + ```yaml tab="File (YAML)" + providers: + docker: + endpoint: "ssh://traefik@192.168.2.5:2022" + # ... + ``` + + ```toml tab="File (TOML)" + [providers.swarm] + endpoint = "ssh://traefik@192.168.2.5:2022" + # ... + ``` + + ```bash tab="CLI" + --providers.swarm.endpoint=ssh://traefik@192.168.2.5:2022 + # ... + ``` + +```yaml tab="File (YAML)" +providers: + swarm: + endpoint: "unix:///var/run/docker.sock" +``` + +```toml tab="File (TOML)" +[providers.swarm] + endpoint = "unix:///var/run/docker.sock" +``` + +```bash tab="CLI" +--providers.swarm.endpoint=unix:///var/run/docker.sock +``` + +### `useBindPortIP` + +_Optional, Default=false_ + +Traefik routes requests to the IP/port of the matching container. +When setting `useBindPortIP=true`, you tell Traefik to use the IP/Port attached to the container's _binding_ instead of its inner network IP/Port. + +When used in conjunction with the `traefik.http.services..loadbalancer.server.port` label (that tells Traefik to route requests to a specific port), +Traefik tries to find a binding on port `traefik.http.services..loadbalancer.server.port`. +If it cannot find such a binding, Traefik falls back on the internal network IP of the container, +but still uses the `traefik.http.services..loadbalancer.server.port` that is set in the label. + +??? example "Examples of `usebindportip` in different situations." + + | port label | Container's binding | Routes to | + |--------------------|----------------------------------------------------|----------------| + | - | - | IntIP:IntPort | + | - | ExtPort:IntPort | IntIP:IntPort | + | - | ExtIp:ExtPort:IntPort | ExtIp:ExtPort | + | LblPort | - | IntIp:LblPort | + | LblPort | ExtIp:ExtPort:LblPort | ExtIp:ExtPort | + | LblPort | ExtIp:ExtPort:OtherPort | IntIp:LblPort | + | LblPort | ExtIp1:ExtPort1:IntPort1 & ExtIp2:LblPort:IntPort2 | ExtIp2:LblPort | + + !!! info "" + In the above table: + + - `ExtIp` stands for "external IP found in the binding" + - `IntIp` stands for "internal network container's IP", + - `ExtPort` stands for "external Port found in the binding" + - `IntPort` stands for "internal network container's port." + +```yaml tab="File (YAML)" +providers: + swarm: + useBindPortIP: true + # ... +``` + +```toml tab="File (TOML)" +[providers.swarm] + useBindPortIP = true + # ... +``` + +```bash tab="CLI" +--providers.swarm.useBindPortIP=true +# ... +``` + +### `exposedByDefault` + +_Optional, Default=true_ + +Expose containers by default through Traefik. +If set to `false`, containers that do not have a `traefik.enable=true` label are ignored from the resulting routing configuration. + +For additional information, refer to [Restrict the Scope of Service Discovery](./overview.md#restrict-the-scope-of-service-discovery). + +```yaml tab="File (YAML)" +providers: + swarm: + exposedByDefault: false + # ... +``` + +```toml tab="File (TOML)" +[providers.swarm] + exposedByDefault = false + # ... +``` + +```bash tab="CLI" +--providers.swarm.exposedByDefault=false +# ... +``` + +### `network` + +_Optional, Default=""_ + +Defines a default docker network to use for connections to all containers. + +This option can be overridden on a per-container basis with the `traefik.docker.network` label. + +```yaml tab="File (YAML)" +providers: + swarm: + network: test + # ... +``` + +```toml tab="File (TOML)" +[providers.swarm] + network = "test" + # ... +``` + +```bash tab="CLI" +--providers.swarm.network=test +# ... +``` + +### `defaultRule` + +_Optional, Default=```Host(`{{ normalize .Name }}`)```_ + +The `defaultRule` option defines what routing rule to apply to a container if no rule is defined by a label. + +It must be a valid [Go template](https://pkg.go.dev/text/template/), and can use +[sprig template functions](https://masterminds.github.io/sprig/). +The container service name can be accessed with the `Name` identifier, +and the template has access to all the labels defined on this container. + +```yaml tab="File (YAML)" +providers: + swarm: + defaultRule: "Host(`{{ .Name }}.{{ index .Labels \"customLabel\"}}`)" + # ... +``` + +```toml tab="File (TOML)" +[providers.swarm] + defaultRule = "Host(`{{ .Name }}.{{ index .Labels \"customLabel\"}}`)" + # ... +``` + +```bash tab="CLI" +--providers.swarm.defaultRule=Host(`{{ .Name }}.{{ index .Labels \"customLabel\"}}`) +# ... +``` + +### `swarmMode` + +_Optional, Default=false_ + +Enables the Swarm Mode (instead of standalone Docker). + +```yaml tab="File (YAML)" +providers: + swarm: + swarmMode: true + # ... +``` + +```toml tab="File (TOML)" +[providers.swarm] + swarmMode = true + # ... +``` + +```bash tab="CLI" +--providers.swarm.swarmMode=true +# ... +``` + +### `swarmModeRefreshSeconds` + +_Optional, Default=15_ + +Defines the polling interval (in seconds) for Swarm Mode. + +```yaml tab="File (YAML)" +providers: + swarm: + swarmModeRefreshSeconds: 30 + # ... +``` + +```toml tab="File (TOML)" +[providers.swarm] + swarmModeRefreshSeconds = 30 + # ... +``` + +```bash tab="CLI" +--providers.swarm.swarmModeRefreshSeconds=30 +# ... +``` + +### `httpClientTimeout` + +_Optional, Default=0_ + +Defines the client timeout (in seconds) for HTTP connections. If its value is `0`, no timeout is set. + +```yaml tab="File (YAML)" +providers: + swarm: + httpClientTimeout: 300 + # ... +``` + +```toml tab="File (TOML)" +[providers.swarm] + httpClientTimeout = 300 + # ... +``` + +```bash tab="CLI" +--providers.swarm.httpClientTimeout=300 +# ... +``` + +### `watch` + +_Optional, Default=true_ + +Watch Docker events. + +```yaml tab="File (YAML)" +providers: + swarm: + watch: false + # ... +``` + +```toml tab="File (TOML)" +[providers.swarm] + watch = false + # ... +``` + +```bash tab="CLI" +--providers.swarm.watch=false +# ... +``` + +### `constraints` + +_Optional, Default=""_ + +The `constraints` option can be set to an expression that Traefik matches against the container labels to determine whether +to create any route for that container. If none of the container labels match the expression, no route for that container is +created. If the expression is empty, all detected containers are included. + +The expression syntax is based on the `Label("key", "value")`, and `LabelRegex("key", "value")` functions, +as well as the usual boolean logic, as shown in examples below. + +??? example "Constraints Expression Examples" + + ```toml + # Includes only containers having a label with key `a.label.name` and value `foo` + constraints = "Label(`a.label.name`, `foo`)" + ``` + + ```toml + # Excludes containers having any label with key `a.label.name` and value `foo` + constraints = "!Label(`a.label.name`, `value`)" + ``` + + ```toml + # With logical AND. + constraints = "Label(`a.label.name`, `valueA`) && Label(`another.label.name`, `valueB`)" + ``` + + ```toml + # With logical OR. + constraints = "Label(`a.label.name`, `valueA`) || Label(`another.label.name`, `valueB`)" + ``` + + ```toml + # With logical AND and OR, with precedence set by parentheses. + constraints = "Label(`a.label.name`, `valueA`) && (Label(`another.label.name`, `valueB`) || Label(`yet.another.label.name`, `valueC`))" + ``` + + ```toml + # Includes only containers having a label with key `a.label.name` and a value matching the `a.+` regular expression. + constraints = "LabelRegex(`a.label.name`, `a.+`)" + ``` + +For additional information, refer to [Restrict the Scope of Service Discovery](./overview.md#restrict-the-scope-of-service-discovery). + +```yaml tab="File (YAML)" +providers: + swarm: + constraints: "Label(`a.label.name`,`foo`)" + # ... +``` + +```toml tab="File (TOML)" +[providers.swarm] + constraints = "Label(`a.label.name`,`foo`)" + # ... +``` + +```bash tab="CLI" +--providers.swarm.constraints=Label(`a.label.name`,`foo`) +# ... +``` + +### `tls` + +_Optional_ + +Defines the TLS configuration used for the secure connection to Docker. + +#### `ca` + +_Optional_ + +`ca` is the path to the certificate authority used for the secure connection to Docker, +it defaults to the system bundle. + +```yaml tab="File (YAML)" +providers: + swarm: + tls: + ca: path/to/ca.crt +``` + +```toml tab="File (TOML)" +[providers.swarm.tls] + ca = "path/to/ca.crt" +``` + +```bash tab="CLI" +--providers.swarm.tls.ca=path/to/ca.crt +``` + +#### `cert` + +`cert` is the path to the public certificate used for the secure connection to Docker. +When using this option, setting the `key` option is required. + +```yaml tab="File (YAML)" +providers: + swarm: + tls: + cert: path/to/foo.cert + key: path/to/foo.key +``` + +```toml tab="File (TOML)" +[providers.swarm.tls] + cert = "path/to/foo.cert" + key = "path/to/foo.key" +``` + +```bash tab="CLI" +--providers.swarm.tls.cert=path/to/foo.cert +--providers.swarm.tls.key=path/to/foo.key +``` + +#### `key` + +_Optional_ + +`key` is the path to the private key used for the secure connection Docker. +When using this option, setting the `cert` option is required. + +```yaml tab="File (YAML)" +providers: + swarm: + tls: + cert: path/to/foo.cert + key: path/to/foo.key +``` + +```toml tab="File (TOML)" +[providers.swarm.tls] + cert = "path/to/foo.cert" + key = "path/to/foo.key" +``` + +```bash tab="CLI" +--providers.swarm.tls.cert=path/to/foo.cert +--providers.swarm.tls.key=path/to/foo.key +``` + +#### `insecureSkipVerify` + +_Optional, Default=false_ + +If `insecureSkipVerify` is `true`, the TLS connection to Docker accepts any certificate presented by the server regardless of the hostnames it covers. + +```yaml tab="File (YAML)" +providers: + swarm: + tls: + insecureSkipVerify: true +``` + +```toml tab="File (TOML)" +[providers.swarm.tls] + insecureSkipVerify = true +``` + +```bash tab="CLI" +--providers.swarm.tls.insecureSkipVerify=true +``` + +### `allowEmptyServices` + +_Optional, Default=false_ + +If the parameter is set to `true`, +any [servers load balancer](../routing/services/index.md#servers-load-balancer) defined for Docker containers is created +regardless of the [healthiness](https://docs.docker.com/engine/reference/builder/#healthcheck) of the corresponding containers. +It also then stays alive and responsive even at times when it becomes empty, +i.e. when all its children containers become unhealthy. +This results in `503` HTTP responses instead of `404` ones, +in the above cases. + +```yaml tab="File (YAML)" +providers: + swarm: + allowEmptyServices: true +``` + +```toml tab="File (TOML)" +[providers.swarm] + allowEmptyServices = true +``` + +```bash tab="CLI" +--providers.swarm.allowEmptyServices=true +``` + +{!traefik-for-business-applications.md!} diff --git a/docs/content/reference/dynamic-configuration/docker.md b/docs/content/reference/dynamic-configuration/docker.md index 1d56559bb..3b9163a53 100644 --- a/docs/content/reference/dynamic-configuration/docker.md +++ b/docs/content/reference/dynamic-configuration/docker.md @@ -8,7 +8,7 @@ description: "Reference dynamic configuration with Docker labels in Traefik Prox Dynamic configuration with Docker Labels {: .subtitle } -The labels are case insensitive. +The labels are case-insensitive. ```yaml labels: diff --git a/docs/content/reference/dynamic-configuration/docker.yml b/docs/content/reference/dynamic-configuration/docker.yml index 6f9e1c62f..097de499d 100644 --- a/docs/content/reference/dynamic-configuration/docker.yml +++ b/docs/content/reference/dynamic-configuration/docker.yml @@ -1,3 +1,2 @@ - "traefik.enable=true" - "traefik.docker.network=foobar" -- "traefik.docker.lbswarm=true" diff --git a/docs/content/reference/dynamic-configuration/swarm.md b/docs/content/reference/dynamic-configuration/swarm.md new file mode 100644 index 000000000..67fec341c --- /dev/null +++ b/docs/content/reference/dynamic-configuration/swarm.md @@ -0,0 +1,17 @@ +--- +title: "Traefik Docker Swarm Configuration Documentation" +description: "Reference dynamic configuration with Docker Swarm labels in Traefik Proxy. Read the technical documentation." +--- + +# Docker Swarm Configuration Reference + +Dynamic configuration with Docker Labels +{: .subtitle } + +The labels are case-insensitive. + +```yaml +labels: + --8<-- "content/reference/dynamic-configuration/swarm.yml" + --8<-- "content/reference/dynamic-configuration/docker-labels.yml" +``` diff --git a/docs/content/reference/dynamic-configuration/swarm.yml b/docs/content/reference/dynamic-configuration/swarm.yml new file mode 100644 index 000000000..6f9e1c62f --- /dev/null +++ b/docs/content/reference/dynamic-configuration/swarm.yml @@ -0,0 +1,3 @@ +- "traefik.enable=true" +- "traefik.docker.network=foobar" +- "traefik.docker.lbswarm=true" diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index ac90226ce..eff863985 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -532,7 +532,7 @@ Constraints is an expression that Traefik matches against the container's labels Default rule. (Default: ```Host(`{{ normalize .Name }}`)```) `--providers.docker.endpoint`: -Docker server endpoint. Can be a tcp or a unix socket endpoint. (Default: ```unix:///var/run/docker.sock```) +Docker server endpoint. Can be a TCP or a Unix socket endpoint. (Default: ```unix:///var/run/docker.sock```) `--providers.docker.exposedbydefault`: Expose containers by default. (Default: ```true```) @@ -543,12 +543,6 @@ Client timeout for HTTP connections. (Default: ```0```) `--providers.docker.network`: Default Docker network used. -`--providers.docker.swarmmode`: -Use Docker on Swarm Mode. (Default: ```false```) - -`--providers.docker.swarmmoderefreshseconds`: -Polling interval for swarm mode. (Default: ```15```) - `--providers.docker.tls.ca`: TLS CA @@ -855,6 +849,51 @@ Enable Rest backend with default settings. (Default: ```false```) `--providers.rest.insecure`: Activate REST Provider directly on the entryPoint named traefik. (Default: ```false```) +`--providers.swarm`: +Enable Docker Swarm backend with default settings. (Default: ```false```) + +`--providers.swarm.allowemptyservices`: +Disregards the Docker containers health checks with respect to the creation or removal of the corresponding services. (Default: ```false```) + +`--providers.swarm.constraints`: +Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container. + +`--providers.swarm.defaultrule`: +Default rule. (Default: ```Host(`{{ normalize .Name }}`)```) + +`--providers.swarm.endpoint`: +Docker server endpoint. Can be a TCP or a Unix socket endpoint. (Default: ```unix:///var/run/docker.sock```) + +`--providers.swarm.exposedbydefault`: +Expose containers by default. (Default: ```true```) + +`--providers.swarm.httpclienttimeout`: +Client timeout for HTTP connections. (Default: ```0```) + +`--providers.swarm.network`: +Default Docker network used. + +`--providers.swarm.refreshseconds`: +Polling interval for swarm mode. (Default: ```15```) + +`--providers.swarm.tls.ca`: +TLS CA + +`--providers.swarm.tls.cert`: +TLS cert + +`--providers.swarm.tls.insecureskipverify`: +TLS insecure skip verify (Default: ```false```) + +`--providers.swarm.tls.key`: +TLS key + +`--providers.swarm.usebindportip`: +Use the ip address from the bound port, rather than from the inner network. (Default: ```false```) + +`--providers.swarm.watch`: +Watch Docker events. (Default: ```true```) + `--providers.zookeeper`: Enable ZooKeeper backend with default settings. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 31748c91c..f83ae69ab 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -532,7 +532,7 @@ Constraints is an expression that Traefik matches against the container's labels Default rule. (Default: ```Host(`{{ normalize .Name }}`)```) `TRAEFIK_PROVIDERS_DOCKER_ENDPOINT`: -Docker server endpoint. Can be a tcp or a unix socket endpoint. (Default: ```unix:///var/run/docker.sock```) +Docker server endpoint. Can be a TCP or a Unix socket endpoint. (Default: ```unix:///var/run/docker.sock```) `TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT`: Expose containers by default. (Default: ```true```) @@ -543,12 +543,6 @@ Client timeout for HTTP connections. (Default: ```0```) `TRAEFIK_PROVIDERS_DOCKER_NETWORK`: Default Docker network used. -`TRAEFIK_PROVIDERS_DOCKER_SWARMMODE`: -Use Docker on Swarm Mode. (Default: ```false```) - -`TRAEFIK_PROVIDERS_DOCKER_SWARMMODEREFRESHSECONDS`: -Polling interval for swarm mode. (Default: ```15```) - `TRAEFIK_PROVIDERS_DOCKER_TLS_CA`: TLS CA @@ -855,6 +849,51 @@ Enable Rest backend with default settings. (Default: ```false```) `TRAEFIK_PROVIDERS_REST_INSECURE`: Activate REST Provider directly on the entryPoint named traefik. (Default: ```false```) +`TRAEFIK_PROVIDERS_SWARM`: +Enable Docker Swarm backend with default settings. (Default: ```false```) + +`TRAEFIK_PROVIDERS_SWARM_ALLOWEMPTYSERVICES`: +Disregards the Docker containers health checks with respect to the creation or removal of the corresponding services. (Default: ```false```) + +`TRAEFIK_PROVIDERS_SWARM_CONSTRAINTS`: +Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container. + +`TRAEFIK_PROVIDERS_SWARM_DEFAULTRULE`: +Default rule. (Default: ```Host(`{{ normalize .Name }}`)```) + +`TRAEFIK_PROVIDERS_SWARM_ENDPOINT`: +Docker server endpoint. Can be a TCP or a Unix socket endpoint. (Default: ```unix:///var/run/docker.sock```) + +`TRAEFIK_PROVIDERS_SWARM_EXPOSEDBYDEFAULT`: +Expose containers by default. (Default: ```true```) + +`TRAEFIK_PROVIDERS_SWARM_HTTPCLIENTTIMEOUT`: +Client timeout for HTTP connections. (Default: ```0```) + +`TRAEFIK_PROVIDERS_SWARM_NETWORK`: +Default Docker network used. + +`TRAEFIK_PROVIDERS_SWARM_REFRESHSECONDS`: +Polling interval for swarm mode. (Default: ```15```) + +`TRAEFIK_PROVIDERS_SWARM_TLS_CA`: +TLS CA + +`TRAEFIK_PROVIDERS_SWARM_TLS_CERT`: +TLS cert + +`TRAEFIK_PROVIDERS_SWARM_TLS_INSECURESKIPVERIFY`: +TLS insecure skip verify (Default: ```false```) + +`TRAEFIK_PROVIDERS_SWARM_TLS_KEY`: +TLS key + +`TRAEFIK_PROVIDERS_SWARM_USEBINDPORTIP`: +Use the ip address from the bound port, rather than from the inner network. (Default: ```false```) + +`TRAEFIK_PROVIDERS_SWARM_WATCH`: +Watch Docker events. (Default: ```true```) + `TRAEFIK_PROVIDERS_ZOOKEEPER`: Enable ZooKeeper backend with default settings. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index b08074bb6..01f1db6d8 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -83,9 +83,7 @@ defaultRule = "foobar" exposedByDefault = true useBindPortIP = true - swarmMode = true network = "foobar" - swarmModeRefreshSeconds = "42s" httpClientTimeout = "42s" allowEmptyServices = true [providers.docker.tls] @@ -93,6 +91,22 @@ cert = "foobar" key = "foobar" insecureSkipVerify = true + [providers.swarm] + constraints = "foobar" + watch = true + endpoint = "foobar" + defaultRule = "foobar" + exposedByDefault = true + useBindPortIP = true + network = "foobar" + refreshSeconds = "42s" + httpClientTimeout = "42s" + allowEmptyServices = true + [providers.swarm.tls] + ca = "foobar" + cert = "foobar" + key = "foobar" + insecureSkipVerify = true [providers.file] directory = "foobar" watch = true diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index cb6f3c257..3f7beb79d 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -95,9 +95,23 @@ providers: insecureSkipVerify: true exposedByDefault: true useBindPortIP: true - swarmMode: true network: foobar - swarmModeRefreshSeconds: 42s + httpClientTimeout: 42s + allowEmptyServices: true + swarm: + constraints: foobar + watch: true + endpoint: foobar + defaultRule: foobar + tls: + ca: foobar + cert: foobar + key: foobar + insecureSkipVerify: true + exposedByDefault: true + useBindPortIP: true + network: foobar + refreshSeconds: 42s httpClientTimeout: 42s allowEmptyServices: true file: diff --git a/docs/content/routing/providers/docker.md b/docs/content/routing/providers/docker.md index 08e8e43ad..4a2b32436 100644 --- a/docs/content/routing/providers/docker.md +++ b/docs/content/routing/providers/docker.md @@ -83,54 +83,6 @@ Attach labels to your containers and let Traefik do the rest! - traefik.http.services.admin-service.loadbalancer.server.port=9000 ``` -??? example "Configuring Docker Swarm & Deploying / Exposing Services" - - Enabling the docker provider (Swarm Mode) - - ```yaml tab="File (YAML)" - providers: - docker: - # swarm classic (1.12-) - # endpoint: "tcp://127.0.0.1:2375" - # docker swarm mode (1.12+) - endpoint: "tcp://127.0.0.1:2377" - swarmMode: true - ``` - - ```toml tab="File (TOML)" - [providers.docker] - # swarm classic (1.12-) - # endpoint = "tcp://127.0.0.1:2375" - # docker swarm mode (1.12+) - endpoint = "tcp://127.0.0.1:2377" - swarmMode = true - ``` - - ```bash tab="CLI" - # swarm classic (1.12-) - # --providers.docker.endpoint=tcp://127.0.0.1:2375 - # docker swarm mode (1.12+) - --providers.docker.endpoint=tcp://127.0.0.1:2377 - --providers.docker.swarmMode=true - ``` - - Attach labels to services (not to containers) while in Swarm mode (in your docker compose file) - - ```yaml - version: "3" - services: - my-container: - deploy: - labels: - - traefik.http.routers.my-container.rule=Host(`example.com`) - - traefik.http.services.my-container-service.loadbalancer.server.port=8080 - ``` - - !!! important "Labels in Docker Swarm Mode" - While in Swarm Mode, Traefik uses labels found on services, not on individual containers. - Therefore, if you use a compose file with Swarm Mode, labels should be defined in the `deploy` part of your service. - This behavior is only enabled for docker-compose version 3+ ([Compose file reference](https://docs.docker.com/compose/compose-file/compose-file-v3/#labels-1)). - ## Routing Configuration !!! info "Labels" @@ -275,9 +227,6 @@ you'd add the label `traefik.http.services..loadbalancer.pa Registers a port. Useful when the container exposes multiples ports. - Mandatory for Docker Swarm (see the section ["Port Detection with Docker Swarm"](../../providers/docker.md#port-detection_1)). - {: #port } - ```yaml - "traefik.http.services.myservice.loadbalancer.server.port=8080" ``` @@ -675,14 +624,3 @@ otherwise it will randomly pick one (depending on how docker is returning them). !!! warning When deploying a stack from a compose file `stack`, the networks defined are prefixed with `stack`. - -#### `traefik.docker.lbswarm` - -```yaml -- "traefik.docker.lbswarm=true" -``` - -Enables Swarm's inbuilt load balancer (only relevant in Swarm Mode). - -If you enable this option, Traefik will use the virtual IP provided by docker swarm instead of the containers IPs. -Which means that Traefik will not perform any kind of load balancing and will delegate this task to swarm. diff --git a/docs/content/routing/providers/swarm.md b/docs/content/routing/providers/swarm.md new file mode 100644 index 000000000..cce7ddb01 --- /dev/null +++ b/docs/content/routing/providers/swarm.md @@ -0,0 +1,640 @@ +--- +title: "Traefik Docker Swarm Routing Documentation" +description: "This guide will teach you how to attach labels to your containers, to route traffic and load balance with Traefik and Docker." +--- + +# Traefik & Docker Swarm + +A Story of Labels & Containers +{: .subtitle } + +![Swarm](../../assets/img/providers/docker.png) + +Attach labels to your containers and let Traefik do the rest! + +## Configuration Examples + +??? example "Configuring Docker Swarm & Deploying / Exposing Services" + + Enabling the docker provider (Swarm Mode) + + ```yaml tab="File (YAML)" + providers: + swarm: + # swarm classic (1.12-) + # endpoint: "tcp://127.0.0.1:2375" + # docker swarm mode (1.12+) + endpoint: "tcp://127.0.0.1:2377" + ``` + + ```toml tab="File (TOML)" + [providers.swarm] + # swarm classic (1.12-) + # endpoint = "tcp://127.0.0.1:2375" + # docker swarm mode (1.12+) + endpoint = "tcp://127.0.0.1:2377" + ``` + + ```bash tab="CLI" + # swarm classic (1.12-) + # --providers.swarm.endpoint=tcp://127.0.0.1:2375 + # docker swarm mode (1.12+) + --providers.swarm.endpoint=tcp://127.0.0.1:2377 + ``` + + Attach labels to services (not to containers) while in Swarm mode (in your docker compose file) + + ```yaml + version: "3" + services: + my-container: + deploy: + labels: + - traefik.http.routers.my-container.rule=Host(`example.com`) + - traefik.http.services.my-container-service.loadbalancer.server.port=8080 + ``` + + !!! important "Labels in Docker Swarm Mode" + While in Swarm Mode, Traefik uses labels found on services, not on individual containers. + Therefore, if you use a compose file with Swarm Mode, labels should be defined in the `deploy` part of your service. + This behavior is only enabled for docker-compose version 3+ ([Compose file reference](https://docs.docker.com/compose/compose-file/compose-file-v3/#labels-1)). + +??? example "Specifying more than one router and service per container" + + Forwarding requests to more than one port on a container requires referencing the service loadbalancer port definition using the service parameter on the router. + + In this example, requests are forwarded for `http://example-a.com` to `http://:8000` in addition to `http://example-b.com` forwarding to `http://:9000`: + + ```yaml + version: "3" + services: + my-container: + # ... + deploy: + labels: + - traefik.http.routers.www-router.rule=Host(`example-a.com`) + - traefik.http.routers.www-router.service=www-service + - traefik.http.services.www-service.loadbalancer.server.port=8000 + - traefik.http.routers.admin-router.rule=Host(`example-b.com`) + - traefik.http.routers.admin-router.service=admin-service + - traefik.http.services.admin-service.loadbalancer.server.port=9000 + ``` + +## Routing Configuration + +!!! info "Labels" + + - Labels are case insensitive. + - The complete list of labels can be found in [the reference page](../../reference/dynamic-configuration/docker.md). + +### General + +Traefik creates, for each container, a corresponding [service](../services/index.md) and [router](../routers/index.md). + +The Service automatically gets a server per instance of the container, +and the router automatically gets a rule defined by `defaultRule` (if no rule for it was defined in labels). + +#### Service definition + +--8<-- "content/routing/providers/service-by-label.md" + +??? example "Automatic service assignment with labels" + + With labels in a compose file + + ```yaml + labels: + - "traefik.http.routers.myproxy.rule=Host(`example.net`)" + # service myservice gets automatically assigned to router myproxy + - "traefik.http.services.myservice.loadbalancer.server.port=80" + ``` + +??? example "Automatic service creation and assignment with labels" + + With labels in a compose file + + ```yaml + labels: + # no service specified or defined and yet one gets automatically created + # and assigned to router myproxy. + - "traefik.http.routers.myproxy.rule=Host(`example.net`)" + ``` + +### Routers + +To update the configuration of the Router automatically attached to the container, +add labels starting with `traefik.http.routers..` and followed by the option you want to change. + +For example, to change the rule, you could add the label ```traefik.http.routers.my-container.rule=Host(`example.com`)```. + +!!! warning "The character `@` is not authorized in the router name ``." + +??? info "`traefik.http.routers..rule`" + + See [rule](../routers/index.md#rule) for more information. + + ```yaml + - "traefik.http.routers.myrouter.rule=Host(`example.com`)" + ``` + +??? info "`traefik.http.routers..entrypoints`" + + See [entry points](../routers/index.md#entrypoints) for more information. + + ```yaml + - "traefik.http.routers.myrouter.entrypoints=ep1,ep2" + ``` + +??? info "`traefik.http.routers..middlewares`" + + See [middlewares](../routers/index.md#middlewares) and [middlewares overview](../../middlewares/overview.md) for more information. + + ```yaml + - "traefik.http.routers.myrouter.middlewares=auth,prefix,cb" + ``` + +??? info "`traefik.http.routers..service`" + + See [service](../routers/index.md#service) for more information. + + ```yaml + - "traefik.http.routers.myrouter.service=myservice" + ``` + +??? info "`traefik.http.routers..tls`" + + See [tls](../routers/index.md#tls) for more information. + + ```yaml + - "traefik.http.routers.myrouter.tls=true" + ``` + +??? info "`traefik.http.routers..tls.certresolver`" + + See [certResolver](../routers/index.md#certresolver) for more information. + + ```yaml + - "traefik.http.routers.myrouter.tls.certresolver=myresolver" + ``` + +??? info "`traefik.http.routers..tls.domains[n].main`" + + See [domains](../routers/index.md#domains) for more information. + + ```yaml + - "traefik.http.routers.myrouter.tls.domains[0].main=example.org" + ``` + +??? info "`traefik.http.routers..tls.domains[n].sans`" + + See [domains](../routers/index.md#domains) for more information. + + ```yaml + - "traefik.http.routers.myrouter.tls.domains[0].sans=test.example.org,dev.example.org" + ``` + +??? info "`traefik.http.routers..tls.options`" + + See [options](../routers/index.md#options) for more information. + + ```yaml + - "traefik.http.routers.myrouter.tls.options=foobar" + ``` + +??? info "`traefik.http.routers..priority`" + + See [priority](../routers/index.md#priority) for more information. + + ```yaml + - "traefik.http.routers.myrouter.priority=42" + ``` + +### Services + +To update the configuration of the Service automatically attached to the container, +add labels starting with `traefik.http.services..`, followed by the option you want to change. + +For example, to change the `passHostHeader` behavior, +you'd add the label `traefik.http.services..loadbalancer.passhostheader=false`. + +!!! warning "The character `@` is not authorized in the service name ``." + +??? info "`traefik.http.services..loadbalancer.server.port`" + + Registers a port. + Useful when the container exposes multiples ports. + + Mandatory for Docker Swarm (see the section ["Port Detection with Docker Swarm"](../../providers/docker.md#port-detection)). + {: #port } + + ```yaml + - "traefik.http.services.myservice.loadbalancer.server.port=8080" + ``` + +??? info "`traefik.http.services..loadbalancer.server.scheme`" + + Overrides the default scheme. + + ```yaml + - "traefik.http.services.myservice.loadbalancer.server.scheme=http" + ``` + +??? info "`traefik.http.services..loadbalancer.serverstransport`" + + Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one. + See [serverstransport](../services/index.md#serverstransport) for more information. + + ```yaml + - "traefik.http.services..loadbalancer.serverstransport=foobar@file" + ``` + +??? info "`traefik.http.services..loadbalancer.passhostheader`" + + See [pass Host header](../services/index.md#pass-host-header) for more information. + + ```yaml + - "traefik.http.services.myservice.loadbalancer.passhostheader=true" + ``` + +??? info "`traefik.http.services..loadbalancer.healthcheck.headers.`" + + See [health check](../services/index.md#health-check) for more information. + + ```yaml + - "traefik.http.services.myservice.loadbalancer.healthcheck.headers.X-Foo=foobar" + ``` + +??? info "`traefik.http.services..loadbalancer.healthcheck.hostname`" + + See [health check](../services/index.md#health-check) for more information. + + ```yaml + - "traefik.http.services.myservice.loadbalancer.healthcheck.hostname=example.org" + ``` + +??? info "`traefik.http.services..loadbalancer.healthcheck.interval`" + + See [health check](../services/index.md#health-check) for more information. + + ```yaml + - "traefik.http.services.myservice.loadbalancer.healthcheck.interval=10s" + ``` + +??? info "`traefik.http.services..loadbalancer.healthcheck.path`" + + See [health check](../services/index.md#health-check) for more information. + + ```yaml + - "traefik.http.services.myservice.loadbalancer.healthcheck.path=/foo" + ``` + +??? info "`traefik.http.services..loadbalancer.healthcheck.method`" + + See [health check](../services/index.md#health-check) for more information. + + ```yaml + - "traefik.http.services.myservice.loadbalancer.healthcheck.method=foobar" + ``` + +??? info "`traefik.http.services..loadbalancer.healthcheck.status`" + + See [health check](../services/index.md#health-check) for more information. + + ```yaml + - "traefik.http.services.myservice.loadbalancer.healthcheck.status=42" + ``` + +??? info "`traefik.http.services..loadbalancer.healthcheck.port`" + + See [health check](../services/index.md#health-check) for more information. + + ```yaml + - "traefik.http.services.myservice.loadbalancer.healthcheck.port=42" + ``` + +??? info "`traefik.http.services..loadbalancer.healthcheck.scheme`" + + See [health check](../services/index.md#health-check) for more information. + + ```yaml + - "traefik.http.services.myservice.loadbalancer.healthcheck.scheme=http" + ``` + +??? info "`traefik.http.services..loadbalancer.healthcheck.timeout`" + + See [health check](../services/index.md#health-check) for more information. + + ```yaml + - "traefik.http.services.myservice.loadbalancer.healthcheck.timeout=10s" + ``` + +??? info "`traefik.http.services..loadbalancer.healthcheck.followredirects`" + + See [health check](../services/index.md#health-check) for more information. + + ```yaml + - "traefik.http.services.myservice.loadbalancer.healthcheck.followredirects=true" + ``` + +??? info "`traefik.http.services..loadbalancer.sticky.cookie`" + + See [sticky sessions](../services/index.md#sticky-sessions) for more information. + + ```yaml + - "traefik.http.services.myservice.loadbalancer.sticky.cookie=true" + ``` + +??? info "`traefik.http.services..loadbalancer.sticky.cookie.httponly`" + + See [sticky sessions](../services/index.md#sticky-sessions) for more information. + + ```yaml + - "traefik.http.services.myservice.loadbalancer.sticky.cookie.httponly=true" + ``` + +??? info "`traefik.http.services..loadbalancer.sticky.cookie.name`" + + See [sticky sessions](../services/index.md#sticky-sessions) for more information. + + ```yaml + - "traefik.http.services.myservice.loadbalancer.sticky.cookie.name=foobar" + ``` + +??? info "`traefik.http.services..loadbalancer.sticky.cookie.secure`" + + See [sticky sessions](../services/index.md#sticky-sessions) for more information. + + ```yaml + - "traefik.http.services.myservice.loadbalancer.sticky.cookie.secure=true" + ``` + +??? info "`traefik.http.services..loadbalancer.sticky.cookie.samesite`" + + See [sticky sessions](../services/index.md#sticky-sessions) for more information. + + ```yaml + - "traefik.http.services.myservice.loadbalancer.sticky.cookie.samesite=none" + ``` + +??? info "`traefik.http.services..loadbalancer.responseforwarding.flushinterval`" + + See [response forwarding](../services/index.md#response-forwarding) for more information. + + ```yaml + - "traefik.http.services.myservice.loadbalancer.responseforwarding.flushinterval=10" + ``` + +### Middleware + +You can declare pieces of middleware using labels starting with `traefik.http.middlewares..`, +followed by the middleware type/options. + +For example, to declare a middleware [`redirectscheme`](../../middlewares/http/redirectscheme.md) named `my-redirect`, +you'd write `traefik.http.middlewares.my-redirect.redirectscheme.scheme=https`. + +More information about available middlewares in the dedicated [middlewares section](../../middlewares/overview.md). + +!!! warning "The character `@` is not authorized in the middleware name." + +??? example "Declaring and Referencing a Middleware" + + ```yaml + services: + my-container: + # ... + deploy: + labels: + # Declaring a middleware + - traefik.http.middlewares.my-redirect.redirectscheme.scheme=https + # Referencing a middleware + - traefik.http.routers.my-container.middlewares=my-redirect + ``` + +!!! warning "Conflicts in Declaration" + + If you declare multiple middleware with the same name but with different parameters, the middleware fails to be declared. + +### TCP + +You can declare TCP Routers and/or Services using labels. + +??? example "Declaring TCP Routers and Services" + + ```yaml + services: + my-container: + # ... + deploy: + labels: + - "traefik.tcp.routers.my-router.rule=HostSNI(`example.com`)" + - "traefik.tcp.routers.my-router.tls=true" + - "traefik.tcp.services.my-service.loadbalancer.server.port=4123" + ``` + +!!! warning "TCP and HTTP" + + If you declare a TCP Router/Service, it will prevent Traefik from automatically creating an HTTP Router/Service (like it does by default if no TCP Router/Service is defined). + You can declare both a TCP Router/Service and an HTTP Router/Service for the same container (but you have to do so manually). + +#### TCP Routers + +??? info "`traefik.tcp.routers..entrypoints`" + + See [entry points](../routers/index.md#entrypoints_1) for more information. + + ```yaml + - "traefik.tcp.routers.mytcprouter.entrypoints=ep1,ep2" + ``` + +??? info "`traefik.tcp.routers..rule`" + + See [rule](../routers/index.md#rule_1) for more information. + + ```yaml + - "traefik.tcp.routers.mytcprouter.rule=HostSNI(`example.com`)" + ``` + +??? info "`traefik.tcp.routers..service`" + + See [service](../routers/index.md#services) for more information. + + ```yaml + - "traefik.tcp.routers.mytcprouter.service=myservice" + ``` + +??? info "`traefik.tcp.routers..tls`" + + See [TLS](../routers/index.md#tls_1) for more information. + + ```yaml + - "traefik.tcp.routers.mytcprouter.tls=true" + ``` + +??? info "`traefik.tcp.routers..tls.certresolver`" + + See [certResolver](../routers/index.md#certresolver_1) for more information. + + ```yaml + - "traefik.tcp.routers.mytcprouter.tls.certresolver=myresolver" + ``` + +??? info "`traefik.tcp.routers..tls.domains[n].main`" + + See [domains](../routers/index.md#domains_1) for more information. + + ```yaml + - "traefik.tcp.routers.mytcprouter.tls.domains[0].main=example.org" + ``` + +??? info "`traefik.tcp.routers..tls.domains[n].sans`" + + See [domains](../routers/index.md#domains_1) for more information. + + ```yaml + - "traefik.tcp.routers.mytcprouter.tls.domains[0].sans=test.example.org,dev.example.org" + ``` + +??? info "`traefik.tcp.routers..tls.options`" + + See [options](../routers/index.md#options_1) for more information. + + ```yaml + - "traefik.tcp.routers.mytcprouter.tls.options=mysoptions" + ``` + +??? info "`traefik.tcp.routers..tls.passthrough`" + + See [TLS](../routers/index.md#tls_1) for more information. + + ```yaml + - "traefik.tcp.routers.mytcprouter.tls.passthrough=true" + ``` + +??? info "`traefik.tcp.routers..priority`" + + See [priority](../routers/index.md#priority_1) for more information. + + ```yaml + - "traefik.tcp.routers.myrouter.priority=42" + ``` + +#### TCP Services + +??? info "`traefik.tcp.services..loadbalancer.server.port`" + + Registers a port of the application. + + ```yaml + - "traefik.tcp.services.mytcpservice.loadbalancer.server.port=423" + ``` + +??? info "`traefik.tcp.services..loadbalancer.server.tls`" + + Determines whether to use TLS when dialing with the backend. + + ```yaml + - "traefik.tcp.services.mytcpservice.loadbalancer.server.tls=true" + ``` + +??? info "`traefik.tcp.services..loadbalancer.proxyprotocol.version`" + + See [PROXY protocol](../services/index.md#proxy-protocol) for more information. + + ```yaml + - "traefik.tcp.services.mytcpservice.loadbalancer.proxyprotocol.version=1" + ``` + +??? info "`traefik.tcp.services..loadbalancer.serverstransport`" + + Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one. + See [serverstransport](../services/index.md#serverstransport_2) for more information. + + ```yaml + - "traefik.tcp.services..loadbalancer.serverstransport=foobar@file" + ``` + +### UDP + +You can declare UDP Routers and/or Services using labels. + +??? example "Declaring UDP Routers and Services" + + ```yaml + services: + my-container: + # ... + deploy: + labels: + - "traefik.udp.routers.my-router.entrypoints=udp" + - "traefik.udp.services.my-service.loadbalancer.server.port=4123" + ``` + +!!! warning "UDP and HTTP" + + If you declare a UDP Router/Service, it will prevent Traefik from automatically creating an HTTP Router/Service (like it does by default if no UDP Router/Service is defined). + You can declare both a UDP Router/Service and an HTTP Router/Service for the same container (but you have to do so manually). + +#### UDP Routers + +??? info "`traefik.udp.routers..entrypoints`" + + See [entry points](../routers/index.md#entrypoints_2) for more information. + + ```yaml + - "traefik.udp.routers.myudprouter.entrypoints=ep1,ep2" + ``` + +??? info "`traefik.udp.routers..service`" + + See [service](../routers/index.md#services_1) for more information. + + ```yaml + - "traefik.udp.routers.myudprouter.service=myservice" + ``` + +#### UDP Services + +??? info "`traefik.udp.services..loadbalancer.server.port`" + + Registers a port of the application. + + ```yaml + - "traefik.udp.services.myudpservice.loadbalancer.server.port=423" + ``` + +### Specific Provider Options + +#### `traefik.enable` + +```yaml +- "traefik.enable=true" +``` + +You can tell Traefik to consider (or not) the container by setting `traefik.enable` to true or false. + +This option overrides the value of `exposedByDefault`. + +#### `traefik.docker.network` + +```yaml +- "traefik.docker.network=mynetwork" +``` + +Overrides the default docker network to use for connections to the container. + +If a container is linked to several networks, be sure to set the proper network name (you can check this with `docker inspect `), +otherwise it will randomly pick one (depending on how docker is returning them). + +!!! warning + When deploying a stack from a compose file `stack`, the networks defined are prefixed with `stack`. + +#### `traefik.docker.lbswarm` + +```yaml +- "traefik.docker.lbswarm=true" +``` + +Enables Swarm's inbuilt load balancer (only relevant in Swarm Mode). + +If you enable this option, Traefik will use the virtual IP provided by docker swarm instead of the containers IPs. +Which means that Traefik will not perform any kind of load balancing and will delegate this task to swarm. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 3237cc82e..a0ca066e2 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -75,6 +75,7 @@ nav: - 'Configuration Discovery': - 'Overview': 'providers/overview.md' - 'Docker': 'providers/docker.md' + - 'Swarm': 'providers/swarm.md' - 'Kubernetes IngressRoute': 'providers/kubernetes-crd.md' - 'Kubernetes Ingress': 'providers/kubernetes-ingress.md' - 'Kubernetes Gateway API': 'providers/kubernetes-gateway.md' @@ -94,6 +95,7 @@ nav: - 'Services': 'routing/services/index.md' - 'Providers': - 'Docker': 'routing/providers/docker.md' + - 'Swarm': 'routing/providers/swarm.md' - 'Kubernetes IngressRoute': 'routing/providers/kubernetes-crd.md' - 'Kubernetes Ingress': 'routing/providers/kubernetes-ingress.md' - 'Kubernetes Gateway API': 'routing/providers/kubernetes-gateway.md' @@ -196,6 +198,7 @@ nav: - 'Dynamic Configuration': - 'File': 'reference/dynamic-configuration/file.md' - 'Docker': 'reference/dynamic-configuration/docker.md' + - 'Swarm': 'reference/dynamic-configuration/swarm.md' - 'Kubernetes CRD': 'reference/dynamic-configuration/kubernetes-crd.md' - 'Kubernetes Gateway API': 'reference/dynamic-configuration/kubernetes-gateway.md' - 'Consul Catalog': 'reference/dynamic-configuration/consul-catalog.md' diff --git a/pkg/api/handler_overview_test.go b/pkg/api/handler_overview_test.go index d76dd55e5..6095e11eb 100644 --- a/pkg/api/handler_overview_test.go +++ b/pkg/api/handler_overview_test.go @@ -235,6 +235,7 @@ func TestHandler_Overview(t *testing.T) { API: &static.API{}, Providers: &static.Providers{ Docker: &docker.Provider{}, + Swarm: &docker.SwarmProvider{}, File: &file.Provider{}, KubernetesIngress: &ingress.Provider{}, KubernetesCRD: &crd.Provider{}, diff --git a/pkg/api/testdata/overview-providers.json b/pkg/api/testdata/overview-providers.json index 20f134354..16e757981 100644 --- a/pkg/api/testdata/overview-providers.json +++ b/pkg/api/testdata/overview-providers.json @@ -24,6 +24,7 @@ }, "providers": [ "Docker", + "Swarm", "File", "KubernetesIngress", "KubernetesCRD", diff --git a/pkg/config/dynamic/fixtures/sample.toml b/pkg/config/dynamic/fixtures/sample.toml index 2cbbf5a60..86242772c 100644 --- a/pkg/config/dynamic/fixtures/sample.toml +++ b/pkg/config/dynamic/fixtures/sample.toml @@ -40,9 +40,7 @@ defaultRule = "foobar" exposedByDefault = true useBindPortIP = true - swarmMode = true network = "foobar" - swarmModeRefreshSeconds = 42 httpClientTimeout = 42 [providers.docker.tls] ca = "foobar" diff --git a/pkg/config/static/static_config.go b/pkg/config/static/static_config.go index 2b2afb386..d71dec626 100644 --- a/pkg/config/static/static_config.go +++ b/pkg/config/static/static_config.go @@ -210,7 +210,9 @@ func (t *Tracing) SetDefaults() { type Providers struct { ProvidersThrottleDuration ptypes.Duration `description:"Backends throttle duration: minimum duration 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." json:"providersThrottleDuration,omitempty" toml:"providersThrottleDuration,omitempty" yaml:"providersThrottleDuration,omitempty" export:"true"` - Docker *docker.Provider `description:"Enable Docker backend with default settings." json:"docker,omitempty" toml:"docker,omitempty" yaml:"docker,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` + Docker *docker.Provider `description:"Enable Docker backend with default settings." json:"docker,omitempty" toml:"docker,omitempty" yaml:"docker,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` + Swarm *docker.SwarmProvider `description:"Enable Docker Swarm backend with default settings." json:"swarm,omitempty" toml:"swarm,omitempty" yaml:"swarm,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` + File *file.Provider `description:"Enable File backend with default settings." json:"file,omitempty" toml:"file,omitempty" yaml:"file,omitempty" export:"true"` KubernetesIngress *ingress.Provider `description:"Enable Kubernetes backend with default settings." json:"kubernetesIngress,omitempty" toml:"kubernetesIngress,omitempty" yaml:"kubernetesIngress,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` KubernetesCRD *crd.Provider `description:"Enable Kubernetes backend with default settings." json:"kubernetesCRD,omitempty" toml:"kubernetesCRD,omitempty" yaml:"kubernetesCRD,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` @@ -219,12 +221,11 @@ type Providers struct { ConsulCatalog *consulcatalog.ProviderBuilder `description:"Enable ConsulCatalog backend with default settings." json:"consulCatalog,omitempty" toml:"consulCatalog,omitempty" yaml:"consulCatalog,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` Nomad *nomad.ProviderBuilder `description:"Enable Nomad backend with default settings." json:"nomad,omitempty" toml:"nomad,omitempty" yaml:"nomad,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` Ecs *ecs.Provider `description:"Enable AWS ECS backend with default settings." json:"ecs,omitempty" toml:"ecs,omitempty" yaml:"ecs,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` - - Consul *consul.ProviderBuilder `description:"Enable Consul backend with default settings." json:"consul,omitempty" toml:"consul,omitempty" yaml:"consul,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` - Etcd *etcd.Provider `description:"Enable Etcd backend with default settings." json:"etcd,omitempty" toml:"etcd,omitempty" yaml:"etcd,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` - ZooKeeper *zk.Provider `description:"Enable ZooKeeper backend with default settings." json:"zooKeeper,omitempty" toml:"zooKeeper,omitempty" yaml:"zooKeeper,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` - Redis *redis.Provider `description:"Enable Redis backend with default settings." json:"redis,omitempty" toml:"redis,omitempty" yaml:"redis,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` - HTTP *http.Provider `description:"Enable HTTP backend with default settings." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` + Consul *consul.ProviderBuilder `description:"Enable Consul backend with default settings." json:"consul,omitempty" toml:"consul,omitempty" yaml:"consul,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` + Etcd *etcd.Provider `description:"Enable Etcd backend with default settings." json:"etcd,omitempty" toml:"etcd,omitempty" yaml:"etcd,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` + ZooKeeper *zk.Provider `description:"Enable ZooKeeper backend with default settings." json:"zooKeeper,omitempty" toml:"zooKeeper,omitempty" yaml:"zooKeeper,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` + Redis *redis.Provider `description:"Enable Redis backend with default settings." json:"redis,omitempty" toml:"redis,omitempty" yaml:"redis,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` + HTTP *http.Provider `description:"Enable HTTP backend with default settings." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` Plugin map[string]PluginConf `description:"Plugins configuration." json:"plugin,omitempty" toml:"plugin,omitempty" yaml:"plugin,omitempty"` } @@ -265,15 +266,21 @@ func (c *Configuration) SetEffectiveConfiguration() { } if c.Providers.Docker != nil { - if c.Providers.Docker.SwarmModeRefreshSeconds <= 0 { - c.Providers.Docker.SwarmModeRefreshSeconds = ptypes.Duration(15 * time.Second) - } - if c.Providers.Docker.HTTPClientTimeout < 0 { c.Providers.Docker.HTTPClientTimeout = 0 } } + if c.Providers.Swarm != nil { + if c.Providers.Swarm.RefreshSeconds <= 0 { + c.Providers.Swarm.RefreshSeconds = ptypes.Duration(15 * time.Second) + } + + if c.Providers.Swarm.HTTPClientTimeout < 0 { + c.Providers.Swarm.HTTPClientTimeout = 0 + } + } + // Disable Gateway API provider if not enabled in experimental. if c.Experimental == nil || !c.Experimental.KubernetesGateway { c.Providers.KubernetesGateway = nil diff --git a/pkg/provider/aggregator/aggregator.go b/pkg/provider/aggregator/aggregator.go index 009aa9c93..9aebb5c34 100644 --- a/pkg/provider/aggregator/aggregator.go +++ b/pkg/provider/aggregator/aggregator.go @@ -80,6 +80,10 @@ func NewProviderAggregator(conf static.Providers) ProviderAggregator { p.quietAddProvider(conf.Docker) } + if conf.Swarm != nil { + p.quietAddProvider(conf.Swarm) + } + if conf.Rest != nil { p.quietAddProvider(conf.Rest) } diff --git a/pkg/provider/docker/config.go b/pkg/provider/docker/config.go index 6095b32f5..d69fbbe81 100644 --- a/pkg/provider/docker/config.go +++ b/pkg/provider/docker/config.go @@ -8,6 +8,7 @@ import ( "strings" dockertypes "github.com/docker/docker/api/types" + "github.com/docker/docker/client" "github.com/docker/go-connections/nat" "github.com/rs/zerolog/log" "github.com/traefik/traefik/v3/pkg/config/dynamic" @@ -17,7 +18,16 @@ import ( "github.com/traefik/traefik/v3/pkg/provider/constraints" ) -func (p *Provider) buildConfiguration(ctx context.Context, containersInspected []dockerData) *dynamic.Configuration { +type DynConfBuilder struct { + Shared + apiClient client.APIClient +} + +func NewDynConfBuilder(configuration Shared, apiClient client.APIClient) *DynConfBuilder { + return &DynConfBuilder{Shared: configuration, apiClient: apiClient} +} + +func (p *DynConfBuilder) build(ctx context.Context, containersInspected []dockerData) *dynamic.Configuration { configurations := make(map[string]*dynamic.Configuration) for _, container := range containersInspected { @@ -92,7 +102,7 @@ func (p *Provider) buildConfiguration(ctx context.Context, containersInspected [ return provider.Merge(ctx, configurations) } -func (p *Provider) buildTCPServiceConfiguration(ctx context.Context, container dockerData, configuration *dynamic.TCPConfiguration) error { +func (p *DynConfBuilder) buildTCPServiceConfiguration(ctx context.Context, container dockerData, configuration *dynamic.TCPConfiguration) error { serviceName := getServiceName(container) if len(configuration.Services) == 0 { @@ -117,7 +127,7 @@ func (p *Provider) buildTCPServiceConfiguration(ctx context.Context, container d return nil } -func (p *Provider) buildUDPServiceConfiguration(ctx context.Context, container dockerData, configuration *dynamic.UDPConfiguration) error { +func (p *DynConfBuilder) buildUDPServiceConfiguration(ctx context.Context, container dockerData, configuration *dynamic.UDPConfiguration) error { serviceName := getServiceName(container) if len(configuration.Services) == 0 { @@ -141,7 +151,7 @@ func (p *Provider) buildUDPServiceConfiguration(ctx context.Context, container d return nil } -func (p *Provider) buildServiceConfiguration(ctx context.Context, container dockerData, configuration *dynamic.HTTPConfiguration) error { +func (p *DynConfBuilder) buildServiceConfiguration(ctx context.Context, container dockerData, configuration *dynamic.HTTPConfiguration) error { serviceName := getServiceName(container) if len(configuration.Services) == 0 { @@ -167,7 +177,7 @@ func (p *Provider) buildServiceConfiguration(ctx context.Context, container dock return nil } -func (p *Provider) keepContainer(ctx context.Context, container dockerData) bool { +func (p *DynConfBuilder) keepContainer(ctx context.Context, container dockerData) bool { logger := log.Ctx(ctx) if !container.ExtraConf.Enable { @@ -193,7 +203,7 @@ func (p *Provider) keepContainer(ctx context.Context, container dockerData) bool return true } -func (p *Provider) addServerTCP(ctx context.Context, container dockerData, loadBalancer *dynamic.TCPServersLoadBalancer) error { +func (p *DynConfBuilder) addServerTCP(ctx context.Context, container dockerData, loadBalancer *dynamic.TCPServersLoadBalancer) error { if loadBalancer == nil { return errors.New("load-balancer is not defined") } @@ -219,7 +229,7 @@ func (p *Provider) addServerTCP(ctx context.Context, container dockerData, loadB return nil } -func (p *Provider) addServerUDP(ctx context.Context, container dockerData, loadBalancer *dynamic.UDPServersLoadBalancer) error { +func (p *DynConfBuilder) addServerUDP(ctx context.Context, container dockerData, loadBalancer *dynamic.UDPServersLoadBalancer) error { if loadBalancer == nil { return errors.New("load-balancer is not defined") } @@ -245,7 +255,7 @@ func (p *Provider) addServerUDP(ctx context.Context, container dockerData, loadB return nil } -func (p *Provider) addServer(ctx context.Context, container dockerData, loadBalancer *dynamic.ServersLoadBalancer) error { +func (p *DynConfBuilder) addServer(ctx context.Context, container dockerData, loadBalancer *dynamic.ServersLoadBalancer) error { if loadBalancer == nil { return errors.New("load-balancer is not defined") } @@ -275,7 +285,7 @@ func (p *Provider) addServer(ctx context.Context, container dockerData, loadBala return nil } -func (p *Provider) getIPPort(ctx context.Context, container dockerData, serverPort string) (string, string, error) { +func (p *DynConfBuilder) getIPPort(ctx context.Context, container dockerData, serverPort string) (string, string, error) { logger := log.Ctx(ctx) var ip, port string @@ -307,7 +317,7 @@ func (p *Provider) getIPPort(ctx context.Context, container dockerData, serverPo return ip, port, nil } -func (p Provider) getIPAddress(ctx context.Context, container dockerData) string { +func (p *DynConfBuilder) getIPAddress(ctx context.Context, container dockerData) string { logger := log.Ctx(ctx) netNotFound := false @@ -338,23 +348,17 @@ func (p Provider) getIPAddress(ctx context.Context, container dockerData) string } if container.NetworkSettings.NetworkMode.IsContainer() { - dockerClient, err := p.createClient() - if err != nil { - logger.Warn().Err(err).Msg("Unable to get IP address") - return "" - } - connectedContainer := container.NetworkSettings.NetworkMode.ConnectedContainer() - containerInspected, err := dockerClient.ContainerInspect(context.Background(), connectedContainer) + containerInspected, err := p.apiClient.ContainerInspect(context.Background(), connectedContainer) if err != nil { logger.Warn().Err(err).Msgf("Unable to get IP address for container %s: failed to inspect container ID %s", container.Name, connectedContainer) return "" } - // Check connected container for traefik.docker.network, falling back to - // the network specified on the current container. + // Check connected container for traefik.docker.network, + // falling back to the network specified on the current container. containerParsed := parseContainer(containerInspected) - extraConf, err := p.getConfiguration(containerParsed) + extraConf, err := p.extractLabels(containerParsed) if err != nil { logger.Warn().Err(err).Msgf("Unable to get IP address for container %s : failed to get extra configuration for container %s", container.Name, containerInspected.Name) return "" @@ -379,8 +383,9 @@ func (p Provider) getIPAddress(ctx context.Context, container dockerData) string return "" } -func (p *Provider) getPortBinding(container dockerData, serverPort string) (*nat.PortBinding, error) { +func (p *DynConfBuilder) getPortBinding(container dockerData, serverPort string) (*nat.PortBinding, error) { port := getPort(container, serverPort) + for netPort, portBindings := range container.NetworkSettings.Ports { if strings.EqualFold(string(netPort), port+"/TCP") || strings.EqualFold(string(netPort), port+"/UDP") { for _, p := range portBindings { @@ -391,36 +396,3 @@ func (p *Provider) getPortBinding(container dockerData, serverPort string) (*nat return nil, fmt.Errorf("unable to find the external IP:Port for the container %q", container.Name) } - -func getPort(container dockerData, serverPort string) string { - if len(serverPort) > 0 { - return serverPort - } - - var ports []nat.Port - for port := range container.NetworkSettings.Ports { - ports = append(ports, port) - } - - less := func(i, j nat.Port) bool { - return i.Int() < j.Int() - } - nat.Sort(ports, less) - - if len(ports) > 0 { - min := ports[0] - return min.Port() - } - - return "" -} - -func getServiceName(container dockerData) string { - serviceName := container.ServiceName - - if values, err := getStringMultipleStrict(container.Labels, labelDockerComposeProject, labelDockerComposeService); err == nil { - serviceName = values[labelDockerComposeService] + "_" + values[labelDockerComposeProject] - } - - return provider.Normalize(serviceName) -} diff --git a/pkg/provider/docker/config_test.go b/pkg/provider/docker/config_test.go index 5f0048b2a..829c5905d 100644 --- a/pkg/provider/docker/config_test.go +++ b/pkg/provider/docker/config_test.go @@ -15,7 +15,7 @@ import ( "github.com/traefik/traefik/v3/pkg/config/dynamic" ) -func TestDefaultRule(t *testing.T) { +func TestDynConfBuilder_DefaultRule(t *testing.T) { testCases := []struct { desc string containers []dockerData @@ -376,8 +376,10 @@ func TestDefaultRule(t *testing.T) { t.Parallel() p := Provider{ - ExposedByDefault: true, - DefaultRule: test.defaultRule, + Shared: Shared{ + ExposedByDefault: true, + DefaultRule: test.defaultRule, + }, } err := p.Init() @@ -385,18 +387,20 @@ func TestDefaultRule(t *testing.T) { for i := 0; i < len(test.containers); i++ { var err error - test.containers[i].ExtraConf, err = p.getConfiguration(test.containers[i]) + test.containers[i].ExtraConf, err = p.extractLabels(test.containers[i]) require.NoError(t, err) } - configuration := p.buildConfiguration(context.Background(), test.containers) + builder := NewDynConfBuilder(p.Shared, nil) + + configuration := builder.build(context.Background(), test.containers) assert.Equal(t, test.expected, configuration) }) } } -func Test_buildConfiguration(t *testing.T) { +func TestDynConfBuilder_build(t *testing.T) { testCases := []struct { desc string containers []dockerData @@ -3381,10 +3385,12 @@ func Test_buildConfiguration(t *testing.T) { t.Parallel() p := Provider{ - AllowEmptyServices: test.allowEmptyServices, - DefaultRule: "Host(`{{ normalize .Name }}.traefik.wtf`)", - ExposedByDefault: true, - UseBindPortIP: test.useBindPortIP, + Shared: Shared{ + AllowEmptyServices: test.allowEmptyServices, + ExposedByDefault: true, + UseBindPortIP: test.useBindPortIP, + DefaultRule: "Host(`{{ normalize .Name }}.traefik.wtf`)", + }, } p.Constraints = test.constraints @@ -3393,18 +3399,20 @@ func Test_buildConfiguration(t *testing.T) { for i := 0; i < len(test.containers); i++ { var err error - test.containers[i].ExtraConf, err = p.getConfiguration(test.containers[i]) + test.containers[i].ExtraConf, err = p.extractLabels(test.containers[i]) require.NoError(t, err) } - configuration := p.buildConfiguration(context.Background(), test.containers) + builder := NewDynConfBuilder(p.Shared, nil) + + configuration := builder.build(context.Background(), test.containers) assert.Equal(t, test.expected, configuration) }) } } -func TestDockerGetIPPort(t *testing.T) { +func TestDynConfBuilder_getIPPort_docker(t *testing.T) { type expected struct { ip string port string @@ -3565,12 +3573,12 @@ func TestDockerGetIPPort(t *testing.T) { dData := parseContainer(test.container) - provider := &Provider{ + builder := NewDynConfBuilder(Shared{ Network: "testnet", UseBindPortIP: true, - } + }, nil) - actualIP, actualPort, actualError := provider.getIPPort(context.Background(), dData, test.serverPort) + actualIP, actualPort, actualError := builder.getIPPort(context.Background(), dData, test.serverPort) if test.expected.error { require.Error(t, actualError) } else { @@ -3582,73 +3590,7 @@ func TestDockerGetIPPort(t *testing.T) { } } -func TestDockerGetPort(t *testing.T) { - testCases := []struct { - desc string - container docker.ContainerJSON - serverPort string - expected string - }{ - { - desc: "no binding, no server port label", - container: containerJSON(name("foo")), - expected: "", - }, - { - desc: "binding, no server port label", - container: containerJSON(ports(nat.PortMap{ - "80/tcp": {}, - })), - expected: "80", - }, - { - desc: "binding, multiple ports, no server port label", - container: containerJSON(ports(nat.PortMap{ - "80/tcp": {}, - "443/tcp": {}, - })), - expected: "80", - }, - { - desc: "no binding, server port label", - container: containerJSON(), - serverPort: "8080", - expected: "8080", - }, - { - desc: "binding, server port label", - container: containerJSON( - ports(nat.PortMap{ - "80/tcp": {}, - })), - serverPort: "8080", - expected: "8080", - }, - { - desc: "binding, multiple ports, server port label", - container: containerJSON(ports(nat.PortMap{ - "8080/tcp": {}, - "80/tcp": {}, - })), - serverPort: "8080", - expected: "8080", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - dData := parseContainer(test.container) - - actual := getPort(dData, test.serverPort) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestDockerGetIPAddress(t *testing.T) { +func TestDynConfBuilder_getIPAddress_docker(t *testing.T) { testCases := []struct { desc string container docker.ContainerJSON @@ -3742,24 +3684,26 @@ func TestDockerGetIPAddress(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - provider := &Provider{ + conf := Shared{ Network: "webnet", } dData := parseContainer(test.container) - dData.ExtraConf.Docker.Network = provider.Network + dData.ExtraConf.Docker.Network = conf.Network if len(test.network) > 0 { dData.ExtraConf.Docker.Network = test.network } - actual := provider.getIPAddress(context.Background(), dData) + builder := NewDynConfBuilder(conf, nil) + + actual := builder.getIPAddress(context.Background(), dData) assert.Equal(t, test.expected, actual) }) } } -func TestSwarmGetIPAddress(t *testing.T) { +func TestDynConfBuilder_getIPAddress_swarm(t *testing.T) { testCases := []struct { service swarm.Service expected string @@ -3810,47 +3754,13 @@ func TestSwarmGetIPAddress(t *testing.T) { t.Run(strconv.Itoa(serviceID), func(t *testing.T) { t.Parallel() - provider := &Provider{ - SwarmMode: true, - } - - dData, err := provider.parseService(context.Background(), test.service, test.networks) - require.NoError(t, err) - - actual := provider.getIPAddress(context.Background(), dData) - assert.Equal(t, test.expected, actual) - }) - } -} - -func TestSwarmGetPort(t *testing.T) { - testCases := []struct { - service swarm.Service - serverPort string - networks map[string]*docker.NetworkResource - expected string - }{ - { - service: swarmService( - withEndpointSpec(modeDNSSR), - ), - networks: map[string]*docker.NetworkResource{}, - serverPort: "8080", - expected: "8080", - }, - } - - for serviceID, test := range testCases { - test := test - t.Run(strconv.Itoa(serviceID), func(t *testing.T) { - t.Parallel() - - p := Provider{} + p := &SwarmProvider{} dData, err := p.parseService(context.Background(), test.service, test.networks) require.NoError(t, err) - actual := getPort(dData, test.serverPort) + builder := NewDynConfBuilder(p.Shared, nil) + actual := builder.getIPAddress(context.Background(), dData) assert.Equal(t, test.expected, actual) }) } diff --git a/pkg/provider/docker/data.go b/pkg/provider/docker/data.go new file mode 100644 index 000000000..a4af51d82 --- /dev/null +++ b/pkg/provider/docker/data.go @@ -0,0 +1,35 @@ +package docker + +import ( + dockertypes "github.com/docker/docker/api/types" + dockercontainertypes "github.com/docker/docker/api/types/container" + "github.com/docker/go-connections/nat" +) + +// dockerData holds the need data to the provider. +type dockerData struct { + ID string + ServiceName string + Name string + Labels map[string]string // List of labels set to container or service + NetworkSettings networkSettings + Health string + Node *dockertypes.ContainerNode + ExtraConf configuration +} + +// NetworkSettings holds the networks data to the provider. +type networkSettings struct { + NetworkMode dockercontainertypes.NetworkMode + Ports nat.PortMap + Networks map[string]*networkData +} + +// Network holds the network data to the provider. +type networkData struct { + Name string + Addr string + Port int + Protocol string + ID string +} diff --git a/pkg/provider/docker/docker.go b/pkg/provider/docker/docker.go deleted file mode 100644 index ec48f714c..000000000 --- a/pkg/provider/docker/docker.go +++ /dev/null @@ -1,602 +0,0 @@ -package docker - -import ( - "context" - "errors" - "fmt" - "io" - "net" - "net/http" - "strconv" - "strings" - "text/template" - "time" - - "github.com/cenkalti/backoff/v4" - "github.com/docker/cli/cli/connhelper" - dockertypes "github.com/docker/docker/api/types" - dockercontainertypes "github.com/docker/docker/api/types/container" - eventtypes "github.com/docker/docker/api/types/events" - "github.com/docker/docker/api/types/filters" - swarmtypes "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/versions" - "github.com/docker/docker/client" - "github.com/docker/go-connections/nat" - "github.com/docker/go-connections/sockets" - "github.com/rs/zerolog/log" - ptypes "github.com/traefik/paerser/types" - "github.com/traefik/traefik/v3/pkg/config/dynamic" - "github.com/traefik/traefik/v3/pkg/job" - "github.com/traefik/traefik/v3/pkg/logs" - "github.com/traefik/traefik/v3/pkg/provider" - "github.com/traefik/traefik/v3/pkg/safe" - "github.com/traefik/traefik/v3/pkg/types" - "github.com/traefik/traefik/v3/pkg/version" -) - -const ( - // DockerAPIVersion is a constant holding the version of the Provider API traefik will use. - DockerAPIVersion = "1.24" - - // SwarmAPIVersion is a constant holding the version of the Provider API traefik will use. - SwarmAPIVersion = "1.24" -) - -// DefaultTemplateRule The default template for the default rule. -const DefaultTemplateRule = "Host(`{{ normalize .Name }}`)" - -var _ provider.Provider = (*Provider)(nil) - -// Provider holds configurations of the provider. -type Provider struct { - Constraints string `description:"Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container." json:"constraints,omitempty" toml:"constraints,omitempty" yaml:"constraints,omitempty" export:"true"` - Watch bool `description:"Watch Docker events." json:"watch,omitempty" toml:"watch,omitempty" yaml:"watch,omitempty" export:"true"` - Endpoint string `description:"Docker server endpoint. Can be a tcp or a unix socket endpoint." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"` - DefaultRule string `description:"Default rule." json:"defaultRule,omitempty" toml:"defaultRule,omitempty" yaml:"defaultRule,omitempty"` - TLS *types.ClientTLS `description:"Enable Docker TLS support." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"` - ExposedByDefault bool `description:"Expose containers by default." json:"exposedByDefault,omitempty" toml:"exposedByDefault,omitempty" yaml:"exposedByDefault,omitempty" export:"true"` - UseBindPortIP bool `description:"Use the ip address from the bound port, rather than from the inner network." json:"useBindPortIP,omitempty" toml:"useBindPortIP,omitempty" yaml:"useBindPortIP,omitempty" export:"true"` - SwarmMode bool `description:"Use Docker on Swarm Mode." json:"swarmMode,omitempty" toml:"swarmMode,omitempty" yaml:"swarmMode,omitempty" export:"true"` - Network string `description:"Default Docker network used." json:"network,omitempty" toml:"network,omitempty" yaml:"network,omitempty" export:"true"` - SwarmModeRefreshSeconds ptypes.Duration `description:"Polling interval for swarm mode." json:"swarmModeRefreshSeconds,omitempty" toml:"swarmModeRefreshSeconds,omitempty" yaml:"swarmModeRefreshSeconds,omitempty" export:"true"` - HTTPClientTimeout ptypes.Duration `description:"Client timeout for HTTP connections." json:"httpClientTimeout,omitempty" toml:"httpClientTimeout,omitempty" yaml:"httpClientTimeout,omitempty" export:"true"` - AllowEmptyServices bool `description:"Disregards the Docker containers health checks with respect to the creation or removal of the corresponding services." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"` - defaultRuleTpl *template.Template -} - -// SetDefaults sets the default values. -func (p *Provider) SetDefaults() { - p.Watch = true - p.ExposedByDefault = true - p.Endpoint = "unix:///var/run/docker.sock" - p.SwarmMode = false - p.SwarmModeRefreshSeconds = ptypes.Duration(15 * time.Second) - p.DefaultRule = DefaultTemplateRule -} - -// Init the provider. -func (p *Provider) Init() error { - defaultRuleTpl, err := provider.MakeDefaultRuleTemplate(p.DefaultRule, nil) - if err != nil { - return fmt.Errorf("error while parsing default rule: %w", err) - } - - p.defaultRuleTpl = defaultRuleTpl - return nil -} - -// dockerData holds the need data to the provider. -type dockerData struct { - ID string - ServiceName string - Name string - Labels map[string]string // List of labels set to container or service - NetworkSettings networkSettings - Health string - Node *dockertypes.ContainerNode - ExtraConf configuration -} - -// NetworkSettings holds the networks data to the provider. -type networkSettings struct { - NetworkMode dockercontainertypes.NetworkMode - Ports nat.PortMap - Networks map[string]*networkData -} - -// Network holds the network data to the provider. -type networkData struct { - Name string - Addr string - Port int - Protocol string - ID string -} - -func (p *Provider) createClient() (client.APIClient, error) { - opts, err := p.getClientOpts() - if err != nil { - return nil, err - } - - httpHeaders := map[string]string{ - "User-Agent": "Traefik " + version.Version, - } - opts = append(opts, client.WithHTTPHeaders(httpHeaders)) - - apiVersion := DockerAPIVersion - if p.SwarmMode { - apiVersion = SwarmAPIVersion - } - opts = append(opts, client.WithVersion(apiVersion)) - - return client.NewClientWithOpts(opts...) -} - -func (p *Provider) getClientOpts() ([]client.Opt, error) { - helper, err := connhelper.GetConnectionHelper(p.Endpoint) - if err != nil { - return nil, err - } - - // SSH - if helper != nil { - // https://github.com/docker/cli/blob/ebca1413117a3fcb81c89d6be226dcec74e5289f/cli/context/docker/load.go#L112-L123 - - httpClient := &http.Client{ - Transport: &http.Transport{ - DialContext: helper.Dialer, - }, - } - - return []client.Opt{ - client.WithHTTPClient(httpClient), - client.WithTimeout(time.Duration(p.HTTPClientTimeout)), - client.WithHost(helper.Host), // To avoid 400 Bad Request: malformed Host header daemon error - client.WithDialContext(helper.Dialer), - }, nil - } - - opts := []client.Opt{ - client.WithHost(p.Endpoint), - client.WithTimeout(time.Duration(p.HTTPClientTimeout)), - } - - if p.TLS != nil { - ctx := log.With().Str(logs.ProviderName, "docker").Logger().WithContext(context.Background()) - - conf, err := p.TLS.CreateTLSConfig(ctx) - if err != nil { - return nil, fmt.Errorf("unable to create client TLS configuration: %w", err) - } - - hostURL, err := client.ParseHostURL(p.Endpoint) - if err != nil { - return nil, err - } - - tr := &http.Transport{ - TLSClientConfig: conf, - } - - if err := sockets.ConfigureTransport(tr, hostURL.Scheme, hostURL.Host); err != nil { - return nil, err - } - - opts = append(opts, client.WithHTTPClient(&http.Client{Transport: tr, Timeout: time.Duration(p.HTTPClientTimeout)})) - } - - return opts, nil -} - -// Provide allows the docker provider to provide configurations to traefik using the given configuration channel. -func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error { - pool.GoCtx(func(routineCtx context.Context) { - logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, "docker").Logger() - ctxLog := logger.WithContext(routineCtx) - - operation := func() error { - var err error - ctx, cancel := context.WithCancel(ctxLog) - defer cancel() - ctx = log.Ctx(ctx).With().Str(logs.ProviderName, "docker").Logger().WithContext(ctx) - - dockerClient, err := p.createClient() - if err != nil { - logger.Error().Err(err).Msg("Failed to create a client for docker, error") - return err - } - defer dockerClient.Close() - - serverVersion, err := dockerClient.ServerVersion(ctx) - if err != nil { - logger.Error().Err(err).Msg("Failed to retrieve information of the docker client and server host") - return err - } - - logger.Debug().Msgf("Provider connection established with docker %s (API %s)", serverVersion.Version, serverVersion.APIVersion) - - var dockerDataList []dockerData - if p.SwarmMode { - dockerDataList, err = p.listServices(ctx, dockerClient) - if err != nil { - logger.Error().Err(err).Msg("Failed to list services for docker swarm mode") - return err - } - } else { - dockerDataList, err = p.listContainers(ctx, dockerClient) - if err != nil { - logger.Error().Err(err).Msg("Failed to list containers for docker") - return err - } - } - - configuration := p.buildConfiguration(ctxLog, dockerDataList) - configurationChan <- dynamic.Message{ - ProviderName: "docker", - Configuration: configuration, - } - if p.Watch { - if p.SwarmMode { - errChan := make(chan error) - - // TODO: This need to be change. Linked to Swarm events docker/docker#23827 - ticker := time.NewTicker(time.Duration(p.SwarmModeRefreshSeconds)) - - pool.GoCtx(func(ctx context.Context) { - logger := log.Ctx(ctx).With().Str(logs.ProviderName, "docker").Logger() - ctx = logger.WithContext(ctx) - - defer close(errChan) - for { - select { - case <-ticker.C: - services, err := p.listServices(ctx, dockerClient) - if err != nil { - logger.Error().Err(err).Msg("Failed to list services for docker swarm mode") - errChan <- err - return - } - - configuration := p.buildConfiguration(ctx, services) - if configuration != nil { - configurationChan <- dynamic.Message{ - ProviderName: "docker", - Configuration: configuration, - } - } - - case <-ctx.Done(): - ticker.Stop() - return - } - } - }) - if err, ok := <-errChan; ok { - return err - } - // channel closed - } else { - f := filters.NewArgs() - f.Add("type", "container") - options := dockertypes.EventsOptions{ - Filters: f, - } - - startStopHandle := func(m eventtypes.Message) { - logger.Debug().Msgf("Provider event received %+v", m) - containers, err := p.listContainers(ctx, dockerClient) - if err != nil { - logger.Error().Err(err).Msg("Failed to list containers for docker") - // Call cancel to get out of the monitor - return - } - - configuration := p.buildConfiguration(ctx, containers) - if configuration != nil { - message := dynamic.Message{ - ProviderName: "docker", - Configuration: configuration, - } - select { - case configurationChan <- message: - case <-ctx.Done(): - } - } - } - - eventsc, errc := dockerClient.Events(ctx, options) - for { - select { - case event := <-eventsc: - if event.Action == "start" || - event.Action == "die" || - strings.HasPrefix(event.Action, "health_status") { - startStopHandle(event) - } - case err := <-errc: - if errors.Is(err, io.EOF) { - logger.Debug().Msg("Provider event stream closed") - } - return err - case <-ctx.Done(): - return nil - } - } - } - } - return nil - } - - notify := func(err error, time time.Duration) { - logger.Error().Err(err).Msgf("Provider error, retrying in %s", time) - } - err := backoff.RetryNotify(safe.OperationWithRecover(operation), backoff.WithContext(job.NewBackOff(backoff.NewExponentialBackOff()), ctxLog), notify) - if err != nil { - logger.Error().Err(err).Msg("Cannot retrieve data") - } - }) - - return nil -} - -func (p *Provider) listContainers(ctx context.Context, dockerClient client.ContainerAPIClient) ([]dockerData, error) { - containerList, err := dockerClient.ContainerList(ctx, dockertypes.ContainerListOptions{}) - if err != nil { - return nil, err - } - - var inspectedContainers []dockerData - // get inspect containers - for _, container := range containerList { - dData := inspectContainers(ctx, dockerClient, container.ID) - if len(dData.Name) == 0 { - continue - } - - extraConf, err := p.getConfiguration(dData) - if err != nil { - log.Ctx(ctx).Error().Err(err).Msgf("Skip container %s", getServiceName(dData)) - continue - } - dData.ExtraConf = extraConf - - inspectedContainers = append(inspectedContainers, dData) - } - return inspectedContainers, nil -} - -func inspectContainers(ctx context.Context, dockerClient client.ContainerAPIClient, containerID string) dockerData { - containerInspected, err := dockerClient.ContainerInspect(ctx, containerID) - if err != nil { - log.Ctx(ctx).Warn().Err(err).Msgf("Failed to inspect container %s", containerID) - return dockerData{} - } - - // This condition is here to avoid to have empty IP https://github.com/traefik/traefik/issues/2459 - // We register only container which are running - if containerInspected.ContainerJSONBase != nil && containerInspected.ContainerJSONBase.State != nil && containerInspected.ContainerJSONBase.State.Running { - return parseContainer(containerInspected) - } - - return dockerData{} -} - -func parseContainer(container dockertypes.ContainerJSON) dockerData { - dData := dockerData{ - NetworkSettings: networkSettings{}, - } - - if container.ContainerJSONBase != nil { - dData.ID = container.ContainerJSONBase.ID - dData.Name = container.ContainerJSONBase.Name - dData.ServiceName = dData.Name // Default ServiceName to be the container's Name. - dData.Node = container.ContainerJSONBase.Node - - if container.ContainerJSONBase.HostConfig != nil { - dData.NetworkSettings.NetworkMode = container.ContainerJSONBase.HostConfig.NetworkMode - } - - if container.State != nil && container.State.Health != nil { - dData.Health = container.State.Health.Status - } - } - - if container.Config != nil && container.Config.Labels != nil { - dData.Labels = container.Config.Labels - } - - if container.NetworkSettings != nil { - if container.NetworkSettings.Ports != nil { - dData.NetworkSettings.Ports = container.NetworkSettings.Ports - } - if container.NetworkSettings.Networks != nil { - dData.NetworkSettings.Networks = make(map[string]*networkData) - for name, containerNetwork := range container.NetworkSettings.Networks { - addr := containerNetwork.IPAddress - if addr == "" { - addr = containerNetwork.GlobalIPv6Address - } - - dData.NetworkSettings.Networks[name] = &networkData{ - ID: containerNetwork.NetworkID, - Name: name, - Addr: addr, - } - } - } - } - return dData -} - -func (p *Provider) listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerData, error) { - logger := log.Ctx(ctx) - - serviceList, err := dockerClient.ServiceList(ctx, dockertypes.ServiceListOptions{}) - if err != nil { - return nil, err - } - - serverVersion, err := dockerClient.ServerVersion(ctx) - if err != nil { - return nil, err - } - - networkListArgs := filters.NewArgs() - // https://docs.docker.com/engine/api/v1.29/#tag/Network (Docker 17.06) - if versions.GreaterThanOrEqualTo(serverVersion.APIVersion, "1.29") { - networkListArgs.Add("scope", "swarm") - } else { - networkListArgs.Add("driver", "overlay") - } - - networkList, err := dockerClient.NetworkList(ctx, dockertypes.NetworkListOptions{Filters: networkListArgs}) - if err != nil { - logger.Debug().Err(err).Msg("Failed to network inspect on client for docker") - return nil, err - } - - networkMap := make(map[string]*dockertypes.NetworkResource) - for _, network := range networkList { - networkToAdd := network - networkMap[network.ID] = &networkToAdd - } - - var dockerDataList []dockerData - var dockerDataListTasks []dockerData - - for _, service := range serviceList { - dData, err := p.parseService(ctx, service, networkMap) - if err != nil { - logger.Error().Err(err).Msgf("Skip container %s", getServiceName(dData)) - continue - } - - if dData.ExtraConf.Docker.LBSwarm { - if len(dData.NetworkSettings.Networks) > 0 { - dockerDataList = append(dockerDataList, dData) - } - } else { - isGlobalSvc := service.Spec.Mode.Global != nil - dockerDataListTasks, err = listTasks(ctx, dockerClient, service.ID, dData, networkMap, isGlobalSvc) - if err != nil { - logger.Warn().Err(err).Send() - } else { - dockerDataList = append(dockerDataList, dockerDataListTasks...) - } - } - } - return dockerDataList, err -} - -func (p *Provider) parseService(ctx context.Context, service swarmtypes.Service, networkMap map[string]*dockertypes.NetworkResource) (dockerData, error) { - logger := log.Ctx(ctx) - - dData := dockerData{ - ID: service.ID, - ServiceName: service.Spec.Annotations.Name, - Name: service.Spec.Annotations.Name, - Labels: service.Spec.Annotations.Labels, - NetworkSettings: networkSettings{}, - } - - extraConf, err := p.getConfiguration(dData) - if err != nil { - return dockerData{}, err - } - dData.ExtraConf = extraConf - - if service.Spec.EndpointSpec != nil { - if service.Spec.EndpointSpec.Mode == swarmtypes.ResolutionModeDNSRR { - if dData.ExtraConf.Docker.LBSwarm { - logger.Warn().Msgf("Ignored %s endpoint-mode not supported, service name: %s. Fallback to Traefik load balancing", swarmtypes.ResolutionModeDNSRR, service.Spec.Annotations.Name) - } - } else if service.Spec.EndpointSpec.Mode == swarmtypes.ResolutionModeVIP { - dData.NetworkSettings.Networks = make(map[string]*networkData) - for _, virtualIP := range service.Endpoint.VirtualIPs { - networkService := networkMap[virtualIP.NetworkID] - if networkService != nil { - if len(virtualIP.Addr) > 0 { - ip, _, _ := net.ParseCIDR(virtualIP.Addr) - network := &networkData{ - Name: networkService.Name, - ID: virtualIP.NetworkID, - Addr: ip.String(), - } - dData.NetworkSettings.Networks[network.Name] = network - } else { - logger.Debug().Msgf("No virtual IPs found in network %s", virtualIP.NetworkID) - } - } else { - logger.Debug().Msgf("Network not found, id: %s", virtualIP.NetworkID) - } - } - } - } - return dData, nil -} - -func listTasks(ctx context.Context, dockerClient client.APIClient, serviceID string, - serviceDockerData dockerData, networkMap map[string]*dockertypes.NetworkResource, isGlobalSvc bool, -) ([]dockerData, error) { - serviceIDFilter := filters.NewArgs() - serviceIDFilter.Add("service", serviceID) - serviceIDFilter.Add("desired-state", "running") - - taskList, err := dockerClient.TaskList(ctx, dockertypes.TaskListOptions{Filters: serviceIDFilter}) - if err != nil { - return nil, err - } - - var dockerDataList []dockerData - for _, task := range taskList { - if task.Status.State != swarmtypes.TaskStateRunning { - continue - } - dData := parseTasks(ctx, task, serviceDockerData, networkMap, isGlobalSvc) - if len(dData.NetworkSettings.Networks) > 0 { - dockerDataList = append(dockerDataList, dData) - } - } - return dockerDataList, err -} - -func parseTasks(ctx context.Context, task swarmtypes.Task, serviceDockerData dockerData, - networkMap map[string]*dockertypes.NetworkResource, isGlobalSvc bool, -) dockerData { - dData := dockerData{ - ID: task.ID, - ServiceName: serviceDockerData.Name, - Name: serviceDockerData.Name + "." + strconv.Itoa(task.Slot), - Labels: serviceDockerData.Labels, - ExtraConf: serviceDockerData.ExtraConf, - NetworkSettings: networkSettings{}, - } - - if isGlobalSvc { - dData.Name = serviceDockerData.Name + "." + task.ID - } - - if task.NetworksAttachments != nil { - dData.NetworkSettings.Networks = make(map[string]*networkData) - for _, virtualIP := range task.NetworksAttachments { - if networkService, present := networkMap[virtualIP.Network.ID]; present { - if len(virtualIP.Addresses) > 0 { - // Not sure about this next loop - when would a task have multiple IP's for the same network? - for _, addr := range virtualIP.Addresses { - ip, _, _ := net.ParseCIDR(addr) - network := &networkData{ - ID: virtualIP.Network.ID, - Name: networkService.Name, - Addr: ip.String(), - } - dData.NetworkSettings.Networks[network.Name] = network - } - } else { - log.Ctx(ctx).Debug().Msgf("No IP addresses found for network %s", virtualIP.Network.ID) - } - } - } - } - return dData -} diff --git a/pkg/provider/docker/pdocker.go b/pkg/provider/docker/pdocker.go new file mode 100644 index 000000000..1259782c4 --- /dev/null +++ b/pkg/provider/docker/pdocker.go @@ -0,0 +1,193 @@ +package docker + +import ( + "context" + "errors" + "fmt" + "io" + "strings" + "time" + + "github.com/cenkalti/backoff/v4" + dockertypes "github.com/docker/docker/api/types" + eventtypes "github.com/docker/docker/api/types/events" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/client" + "github.com/rs/zerolog/log" + "github.com/traefik/traefik/v3/pkg/config/dynamic" + "github.com/traefik/traefik/v3/pkg/job" + "github.com/traefik/traefik/v3/pkg/logs" + "github.com/traefik/traefik/v3/pkg/provider" + "github.com/traefik/traefik/v3/pkg/safe" +) + +// DockerAPIVersion is a constant holding the version of the Provider API traefik will use. +const DockerAPIVersion = "1.24" + +const dockerName = "docker" + +var _ provider.Provider = (*Provider)(nil) + +// Provider holds configurations of the provider. +type Provider struct { + Shared `yaml:",inline" export:"true"` + ClientConfig `yaml:",inline" export:"true"` +} + +// SetDefaults sets the default values. +func (p *Provider) SetDefaults() { + p.Watch = true + p.ExposedByDefault = true + p.Endpoint = "unix:///var/run/docker.sock" + p.DefaultRule = DefaultTemplateRule +} + +// Init the provider. +func (p *Provider) Init() error { + defaultRuleTpl, err := provider.MakeDefaultRuleTemplate(p.DefaultRule, nil) + if err != nil { + return fmt.Errorf("error while parsing default rule: %w", err) + } + + p.defaultRuleTpl = defaultRuleTpl + return nil +} + +func (p *Provider) createClient(ctx context.Context) (*client.Client, error) { + p.ClientConfig.apiVersion = DockerAPIVersion + return createClient(ctx, p.ClientConfig) +} + +// Provide allows the docker provider to provide configurations to traefik using the given configuration channel. +func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error { + pool.GoCtx(func(routineCtx context.Context) { + logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, dockerName).Logger() + ctxLog := logger.WithContext(routineCtx) + + operation := func() error { + var err error + ctx, cancel := context.WithCancel(ctxLog) + defer cancel() + + ctx = log.Ctx(ctx).With().Str(logs.ProviderName, dockerName).Logger().WithContext(ctx) + + dockerClient, err := p.createClient(ctxLog) + if err != nil { + logger.Error().Err(err).Msg("Failed to create Docker API client") + return err + } + defer func() { _ = dockerClient.Close() }() + + builder := NewDynConfBuilder(p.Shared, dockerClient) + + serverVersion, err := dockerClient.ServerVersion(ctx) + if err != nil { + logger.Error().Err(err).Msg("Failed to retrieve information of the docker client and server host") + return err + } + + logger.Debug().Msgf("Provider connection established with docker %s (API %s)", serverVersion.Version, serverVersion.APIVersion) + + dockerDataList, err := p.listContainers(ctx, dockerClient) + if err != nil { + logger.Error().Err(err).Msg("Failed to list containers for docker") + return err + } + + configuration := builder.build(ctxLog, dockerDataList) + configurationChan <- dynamic.Message{ + ProviderName: dockerName, + Configuration: configuration, + } + + if p.Watch { + f := filters.NewArgs() + f.Add("type", "container") + options := dockertypes.EventsOptions{ + Filters: f, + } + + startStopHandle := func(m eventtypes.Message) { + logger.Debug().Msgf("Provider event received %+v", m) + containers, err := p.listContainers(ctx, dockerClient) + if err != nil { + logger.Error().Err(err).Msg("Failed to list containers for docker") + // Call cancel to get out of the monitor + return + } + + configuration := builder.build(ctx, containers) + if configuration != nil { + message := dynamic.Message{ + ProviderName: dockerName, + Configuration: configuration, + } + select { + case configurationChan <- message: + case <-ctx.Done(): + } + } + } + + eventsc, errc := dockerClient.Events(ctx, options) + for { + select { + case event := <-eventsc: + if event.Action == "start" || + event.Action == "die" || + strings.HasPrefix(event.Action, "health_status") { + startStopHandle(event) + } + case err := <-errc: + if errors.Is(err, io.EOF) { + logger.Debug().Msg("Provider event stream closed") + } + return err + case <-ctx.Done(): + return nil + } + } + } + + return nil + } + + notify := func(err error, time time.Duration) { + logger.Error().Err(err).Msgf("Provider error, retrying in %s", time) + } + err := backoff.RetryNotify(safe.OperationWithRecover(operation), backoff.WithContext(job.NewBackOff(backoff.NewExponentialBackOff()), ctxLog), notify) + if err != nil { + logger.Error().Err(err).Msg("Cannot retrieve data") + } + }) + + return nil +} + +func (p *Provider) listContainers(ctx context.Context, dockerClient client.ContainerAPIClient) ([]dockerData, error) { + containerList, err := dockerClient.ContainerList(ctx, dockertypes.ContainerListOptions{}) + if err != nil { + return nil, err + } + + var inspectedContainers []dockerData + // get inspect containers + for _, container := range containerList { + dData := inspectContainers(ctx, dockerClient, container.ID) + if len(dData.Name) == 0 { + continue + } + + extraConf, err := p.extractLabels(dData) + if err != nil { + log.Ctx(ctx).Error().Err(err).Msgf("Skip container %s", getServiceName(dData)) + continue + } + + dData.ExtraConf = extraConf + + inspectedContainers = append(inspectedContainers, dData) + } + + return inspectedContainers, nil +} diff --git a/pkg/provider/docker/pswarm.go b/pkg/provider/docker/pswarm.go new file mode 100644 index 000000000..fd513dad2 --- /dev/null +++ b/pkg/provider/docker/pswarm.go @@ -0,0 +1,332 @@ +package docker + +import ( + "context" + "fmt" + "net" + "strconv" + "time" + + "github.com/cenkalti/backoff/v4" + dockertypes "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + swarmtypes "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/api/types/versions" + "github.com/docker/docker/client" + "github.com/rs/zerolog/log" + ptypes "github.com/traefik/paerser/types" + "github.com/traefik/traefik/v3/pkg/config/dynamic" + "github.com/traefik/traefik/v3/pkg/job" + "github.com/traefik/traefik/v3/pkg/logs" + "github.com/traefik/traefik/v3/pkg/provider" + "github.com/traefik/traefik/v3/pkg/safe" +) + +// SwarmAPIVersion is a constant holding the version of the Provider API traefik will use. +const SwarmAPIVersion = "1.24" + +const swarmName = "swarm" + +var _ provider.Provider = (*SwarmProvider)(nil) + +// SwarmProvider holds configurations of the provider. +type SwarmProvider struct { + Shared `yaml:",inline" export:"true"` + ClientConfig `yaml:",inline" export:"true"` + + RefreshSeconds ptypes.Duration `description:"Polling interval for swarm mode." json:"refreshSeconds,omitempty" toml:"refreshSeconds,omitempty" yaml:"refreshSeconds,omitempty" export:"true"` +} + +// SetDefaults sets the default values. +func (p *SwarmProvider) SetDefaults() { + p.Watch = true + p.ExposedByDefault = true + p.Endpoint = "unix:///var/run/docker.sock" + p.RefreshSeconds = ptypes.Duration(15 * time.Second) + p.DefaultRule = DefaultTemplateRule +} + +// Init the provider. +func (p *SwarmProvider) Init() error { + defaultRuleTpl, err := provider.MakeDefaultRuleTemplate(p.DefaultRule, nil) + if err != nil { + return fmt.Errorf("error while parsing default rule: %w", err) + } + + p.defaultRuleTpl = defaultRuleTpl + return nil +} + +func (p *SwarmProvider) createClient(ctx context.Context) (*client.Client, error) { + p.ClientConfig.apiVersion = SwarmAPIVersion + return createClient(ctx, p.ClientConfig) +} + +// Provide allows the docker provider to provide configurations to traefik using the given configuration channel. +func (p *SwarmProvider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error { + pool.GoCtx(func(routineCtx context.Context) { + logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, swarmName).Logger() + ctxLog := logger.WithContext(routineCtx) + + operation := func() error { + var err error + ctx, cancel := context.WithCancel(ctxLog) + defer cancel() + + ctx = log.Ctx(ctx).With().Str(logs.ProviderName, swarmName).Logger().WithContext(ctx) + + dockerClient, err := p.createClient(ctx) + if err != nil { + logger.Error().Err(err).Msg("Failed to create Docker API client") + return err + } + defer func() { _ = dockerClient.Close() }() + + builder := NewDynConfBuilder(p.Shared, dockerClient) + + serverVersion, err := dockerClient.ServerVersion(ctx) + if err != nil { + logger.Error().Err(err).Msg("Failed to retrieve information of the docker client and server host") + return err + } + + logger.Debug().Msgf("Provider connection established with docker %s (API %s)", serverVersion.Version, serverVersion.APIVersion) + + dockerDataList, err := p.listServices(ctx, dockerClient) + if err != nil { + logger.Error().Err(err).Msg("Failed to list services for docker swarm mode") + return err + } + + configuration := builder.build(ctxLog, dockerDataList) + configurationChan <- dynamic.Message{ + ProviderName: swarmName, + Configuration: configuration, + } + if p.Watch { + errChan := make(chan error) + + // TODO: This need to be change. Linked to Swarm events docker/docker#23827 + ticker := time.NewTicker(time.Duration(p.RefreshSeconds)) + + pool.GoCtx(func(ctx context.Context) { + logger := log.Ctx(ctx).With().Str(logs.ProviderName, swarmName).Logger() + ctx = logger.WithContext(ctx) + + defer close(errChan) + for { + select { + case <-ticker.C: + services, err := p.listServices(ctx, dockerClient) + if err != nil { + logger.Error().Err(err).Msg("Failed to list services for docker swarm mode") + errChan <- err + return + } + + configuration := builder.build(ctx, services) + if configuration != nil { + configurationChan <- dynamic.Message{ + ProviderName: swarmName, + Configuration: configuration, + } + } + + case <-ctx.Done(): + ticker.Stop() + return + } + } + }) + if err, ok := <-errChan; ok { + return err + } + // channel closed + } + return nil + } + + notify := func(err error, time time.Duration) { + logger.Error().Err(err).Msgf("Provider error, retrying in %s", time) + } + err := backoff.RetryNotify(safe.OperationWithRecover(operation), backoff.WithContext(job.NewBackOff(backoff.NewExponentialBackOff()), ctxLog), notify) + if err != nil { + logger.Error().Err(err).Msg("Cannot retrieve data") + } + }) + + return nil +} + +func (p *SwarmProvider) listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerData, error) { + logger := log.Ctx(ctx) + + serviceList, err := dockerClient.ServiceList(ctx, dockertypes.ServiceListOptions{}) + if err != nil { + return nil, err + } + + serverVersion, err := dockerClient.ServerVersion(ctx) + if err != nil { + return nil, err + } + + networkListArgs := filters.NewArgs() + // https://docs.docker.com/engine/api/v1.29/#tag/Network (Docker 17.06) + if versions.GreaterThanOrEqualTo(serverVersion.APIVersion, "1.29") { + networkListArgs.Add("scope", "swarm") + } else { + networkListArgs.Add("driver", "overlay") + } + + networkList, err := dockerClient.NetworkList(ctx, dockertypes.NetworkListOptions{Filters: networkListArgs}) + if err != nil { + logger.Debug().Err(err).Msg("Failed to network inspect on client for docker") + return nil, err + } + + networkMap := make(map[string]*dockertypes.NetworkResource) + for _, network := range networkList { + networkToAdd := network + networkMap[network.ID] = &networkToAdd + } + + var dockerDataList []dockerData + var dockerDataListTasks []dockerData + + for _, service := range serviceList { + dData, err := p.parseService(ctx, service, networkMap) + if err != nil { + logger.Error().Err(err).Msgf("Skip container %s", getServiceName(dData)) + continue + } + + if dData.ExtraConf.Docker.LBSwarm { + if len(dData.NetworkSettings.Networks) > 0 { + dockerDataList = append(dockerDataList, dData) + } + } else { + isGlobalSvc := service.Spec.Mode.Global != nil + dockerDataListTasks, err = listTasks(ctx, dockerClient, service.ID, dData, networkMap, isGlobalSvc) + if err != nil { + logger.Warn().Err(err).Send() + } else { + dockerDataList = append(dockerDataList, dockerDataListTasks...) + } + } + } + + return dockerDataList, err +} + +func (p *SwarmProvider) parseService(ctx context.Context, service swarmtypes.Service, networkMap map[string]*dockertypes.NetworkResource) (dockerData, error) { + logger := log.Ctx(ctx) + + dData := dockerData{ + ID: service.ID, + ServiceName: service.Spec.Annotations.Name, + Name: service.Spec.Annotations.Name, + Labels: service.Spec.Annotations.Labels, + NetworkSettings: networkSettings{}, + } + + extraConf, err := p.extractLabels(dData) + if err != nil { + return dockerData{}, err + } + dData.ExtraConf = extraConf + + if service.Spec.EndpointSpec != nil { + if service.Spec.EndpointSpec.Mode == swarmtypes.ResolutionModeDNSRR { + if dData.ExtraConf.Docker.LBSwarm { + logger.Warn().Msgf("Ignored %s endpoint-mode not supported, service name: %s. Fallback to Traefik load balancing", swarmtypes.ResolutionModeDNSRR, service.Spec.Annotations.Name) + } + } else if service.Spec.EndpointSpec.Mode == swarmtypes.ResolutionModeVIP { + dData.NetworkSettings.Networks = make(map[string]*networkData) + for _, virtualIP := range service.Endpoint.VirtualIPs { + networkService := networkMap[virtualIP.NetworkID] + if networkService != nil { + if len(virtualIP.Addr) > 0 { + ip, _, _ := net.ParseCIDR(virtualIP.Addr) + network := &networkData{ + Name: networkService.Name, + ID: virtualIP.NetworkID, + Addr: ip.String(), + } + dData.NetworkSettings.Networks[network.Name] = network + } else { + logger.Debug().Msgf("No virtual IPs found in network %s", virtualIP.NetworkID) + } + } else { + logger.Debug().Msgf("Network not found, id: %s", virtualIP.NetworkID) + } + } + } + } + return dData, nil +} + +func listTasks(ctx context.Context, dockerClient client.APIClient, serviceID string, + serviceDockerData dockerData, networkMap map[string]*dockertypes.NetworkResource, isGlobalSvc bool, +) ([]dockerData, error) { + serviceIDFilter := filters.NewArgs() + serviceIDFilter.Add("service", serviceID) + serviceIDFilter.Add("desired-state", "running") + + taskList, err := dockerClient.TaskList(ctx, dockertypes.TaskListOptions{Filters: serviceIDFilter}) + if err != nil { + return nil, err + } + + var dockerDataList []dockerData + for _, task := range taskList { + if task.Status.State != swarmtypes.TaskStateRunning { + continue + } + dData := parseTasks(ctx, task, serviceDockerData, networkMap, isGlobalSvc) + if len(dData.NetworkSettings.Networks) > 0 { + dockerDataList = append(dockerDataList, dData) + } + } + return dockerDataList, err +} + +func parseTasks(ctx context.Context, task swarmtypes.Task, serviceDockerData dockerData, + networkMap map[string]*dockertypes.NetworkResource, isGlobalSvc bool, +) dockerData { + dData := dockerData{ + ID: task.ID, + ServiceName: serviceDockerData.Name, + Name: serviceDockerData.Name + "." + strconv.Itoa(task.Slot), + Labels: serviceDockerData.Labels, + ExtraConf: serviceDockerData.ExtraConf, + NetworkSettings: networkSettings{}, + } + + if isGlobalSvc { + dData.Name = serviceDockerData.Name + "." + task.ID + } + + if task.NetworksAttachments != nil { + dData.NetworkSettings.Networks = make(map[string]*networkData) + for _, virtualIP := range task.NetworksAttachments { + if networkService, present := networkMap[virtualIP.Network.ID]; present { + if len(virtualIP.Addresses) > 0 { + // Not sure about this next loop - when would a task have multiple IP's for the same network? + for _, addr := range virtualIP.Addresses { + ip, _, _ := net.ParseCIDR(addr) + network := &networkData{ + ID: virtualIP.Network.ID, + Name: networkService.Name, + Addr: ip.String(), + } + dData.NetworkSettings.Networks[network.Name] = network + } + } else { + log.Ctx(ctx).Debug().Msgf("No IP addresses found for network %s", virtualIP.Network.ID) + } + } + } + } + return dData +} diff --git a/pkg/provider/docker/pswarm_mock_test.go b/pkg/provider/docker/pswarm_mock_test.go new file mode 100644 index 000000000..83eb61b3b --- /dev/null +++ b/pkg/provider/docker/pswarm_mock_test.go @@ -0,0 +1,49 @@ +package docker + +import ( + "context" + + dockertypes "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + dockerclient "github.com/docker/docker/client" +) + +type fakeTasksClient struct { + dockerclient.APIClient + tasks []swarm.Task + container dockertypes.ContainerJSON + err error +} + +func (c *fakeTasksClient) TaskList(ctx context.Context, options dockertypes.TaskListOptions) ([]swarm.Task, error) { + return c.tasks, c.err +} + +func (c *fakeTasksClient) ContainerInspect(ctx context.Context, container string) (dockertypes.ContainerJSON, error) { + return c.container, c.err +} + +type fakeServicesClient struct { + dockerclient.APIClient + dockerVersion string + networks []dockertypes.NetworkResource + services []swarm.Service + tasks []swarm.Task + err error +} + +func (c *fakeServicesClient) ServiceList(ctx context.Context, options dockertypes.ServiceListOptions) ([]swarm.Service, error) { + return c.services, c.err +} + +func (c *fakeServicesClient) ServerVersion(ctx context.Context) (dockertypes.Version, error) { + return dockertypes.Version{APIVersion: c.dockerVersion}, c.err +} + +func (c *fakeServicesClient) NetworkList(ctx context.Context, options dockertypes.NetworkListOptions) ([]dockertypes.NetworkResource, error) { + return c.networks, c.err +} + +func (c *fakeServicesClient) TaskList(ctx context.Context, options dockertypes.TaskListOptions) ([]swarm.Task, error) { + return c.tasks, c.err +} diff --git a/pkg/provider/docker/swarm_test.go b/pkg/provider/docker/pswarm_test.go similarity index 86% rename from pkg/provider/docker/swarm_test.go rename to pkg/provider/docker/pswarm_test.go index 541af22f2..d13e3e7c3 100644 --- a/pkg/provider/docker/swarm_test.go +++ b/pkg/provider/docker/pswarm_test.go @@ -9,26 +9,10 @@ import ( "github.com/davecgh/go-spew/spew" dockertypes "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" - dockerclient "github.com/docker/docker/client" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -type fakeTasksClient struct { - dockerclient.APIClient - tasks []swarm.Task - container dockertypes.ContainerJSON - err error -} - -func (c *fakeTasksClient) TaskList(ctx context.Context, options dockertypes.TaskListOptions) ([]swarm.Task, error) { - return c.tasks, c.err -} - -func (c *fakeTasksClient) ContainerInspect(ctx context.Context, container string) (dockertypes.ContainerJSON, error) { - return c.container, c.err -} - func TestListTasks(t *testing.T) { testCases := []struct { service swarm.Service @@ -83,7 +67,7 @@ func TestListTasks(t *testing.T) { t.Run(strconv.Itoa(caseID), func(t *testing.T) { t.Parallel() - p := Provider{} + p := SwarmProvider{} dockerData, err := p.parseService(context.Background(), test.service, test.networks) require.NoError(t, err) @@ -103,32 +87,7 @@ func TestListTasks(t *testing.T) { } } -type fakeServicesClient struct { - dockerclient.APIClient - dockerVersion string - networks []dockertypes.NetworkResource - services []swarm.Service - tasks []swarm.Task - err error -} - -func (c *fakeServicesClient) ServiceList(ctx context.Context, options dockertypes.ServiceListOptions) ([]swarm.Service, error) { - return c.services, c.err -} - -func (c *fakeServicesClient) ServerVersion(ctx context.Context) (dockertypes.Version, error) { - return dockertypes.Version{APIVersion: c.dockerVersion}, c.err -} - -func (c *fakeServicesClient) NetworkList(ctx context.Context, options dockertypes.NetworkListOptions) ([]dockertypes.NetworkResource, error) { - return c.networks, c.err -} - -func (c *fakeServicesClient) TaskList(ctx context.Context, options dockertypes.TaskListOptions) ([]swarm.Task, error) { - return c.tasks, c.err -} - -func TestListServices(t *testing.T) { +func TestSwarmProvider_listServices(t *testing.T) { testCases := []struct { desc string services []swarm.Service @@ -277,7 +236,7 @@ func TestListServices(t *testing.T) { dockerClient := &fakeServicesClient{services: test.services, tasks: test.tasks, dockerVersion: test.dockerVersion, networks: test.networks} - p := Provider{} + p := SwarmProvider{} serviceDockerData, err := p.listServices(context.Background(), dockerClient) assert.NoError(t, err) @@ -293,7 +252,7 @@ func TestListServices(t *testing.T) { } } -func TestSwarmTaskParsing(t *testing.T) { +func TestSwarmProvider_parseService_task(t *testing.T) { testCases := []struct { service swarm.Service tasks []swarm.Task @@ -396,7 +355,7 @@ func TestSwarmTaskParsing(t *testing.T) { t.Run(strconv.Itoa(caseID), func(t *testing.T) { t.Parallel() - p := Provider{} + p := SwarmProvider{} dData, err := p.parseService(context.Background(), test.service, test.networks) require.NoError(t, err) diff --git a/pkg/provider/docker/shared.go b/pkg/provider/docker/shared.go new file mode 100644 index 000000000..74bcc62a1 --- /dev/null +++ b/pkg/provider/docker/shared.go @@ -0,0 +1,211 @@ +package docker + +import ( + "context" + "fmt" + "net/http" + "text/template" + "time" + + "github.com/docker/cli/cli/connhelper" + dockertypes "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" + "github.com/docker/go-connections/sockets" + "github.com/rs/zerolog/log" + ptypes "github.com/traefik/paerser/types" + "github.com/traefik/traefik/v3/pkg/provider" + "github.com/traefik/traefik/v3/pkg/types" + "github.com/traefik/traefik/v3/pkg/version" +) + +// DefaultTemplateRule The default template for the default rule. +const DefaultTemplateRule = "Host(`{{ normalize .Name }}`)" + +type Shared struct { + ExposedByDefault bool `description:"Expose containers by default." json:"exposedByDefault,omitempty" toml:"exposedByDefault,omitempty" yaml:"exposedByDefault,omitempty" export:"true"` + Constraints string `description:"Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container." json:"constraints,omitempty" toml:"constraints,omitempty" yaml:"constraints,omitempty" export:"true"` + AllowEmptyServices bool `description:"Disregards the Docker containers health checks with respect to the creation or removal of the corresponding services." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"` + Network string `description:"Default Docker network used." json:"network,omitempty" toml:"network,omitempty" yaml:"network,omitempty" export:"true"` + UseBindPortIP bool `description:"Use the ip address from the bound port, rather than from the inner network." json:"useBindPortIP,omitempty" toml:"useBindPortIP,omitempty" yaml:"useBindPortIP,omitempty" export:"true"` + + Watch bool `description:"Watch Docker events." json:"watch,omitempty" toml:"watch,omitempty" yaml:"watch,omitempty" export:"true"` + DefaultRule string `description:"Default rule." json:"defaultRule,omitempty" toml:"defaultRule,omitempty" yaml:"defaultRule,omitempty"` + + defaultRuleTpl *template.Template +} + +func inspectContainers(ctx context.Context, dockerClient client.ContainerAPIClient, containerID string) dockerData { + containerInspected, err := dockerClient.ContainerInspect(ctx, containerID) + if err != nil { + log.Ctx(ctx).Warn().Err(err).Msgf("Failed to inspect container %s", containerID) + return dockerData{} + } + + // This condition is here to avoid to have empty IP https://github.com/traefik/traefik/issues/2459 + // We register only container which are running + if containerInspected.ContainerJSONBase != nil && containerInspected.ContainerJSONBase.State != nil && containerInspected.ContainerJSONBase.State.Running { + return parseContainer(containerInspected) + } + + return dockerData{} +} + +func parseContainer(container dockertypes.ContainerJSON) dockerData { + dData := dockerData{ + NetworkSettings: networkSettings{}, + } + + if container.ContainerJSONBase != nil { + dData.ID = container.ContainerJSONBase.ID + dData.Name = container.ContainerJSONBase.Name + dData.ServiceName = dData.Name // Default ServiceName to be the container's Name. + dData.Node = container.ContainerJSONBase.Node + + if container.ContainerJSONBase.HostConfig != nil { + dData.NetworkSettings.NetworkMode = container.ContainerJSONBase.HostConfig.NetworkMode + } + + if container.State != nil && container.State.Health != nil { + dData.Health = container.State.Health.Status + } + } + + if container.Config != nil && container.Config.Labels != nil { + dData.Labels = container.Config.Labels + } + + if container.NetworkSettings != nil { + if container.NetworkSettings.Ports != nil { + dData.NetworkSettings.Ports = container.NetworkSettings.Ports + } + if container.NetworkSettings.Networks != nil { + dData.NetworkSettings.Networks = make(map[string]*networkData) + for name, containerNetwork := range container.NetworkSettings.Networks { + addr := containerNetwork.IPAddress + if addr == "" { + addr = containerNetwork.GlobalIPv6Address + } + + dData.NetworkSettings.Networks[name] = &networkData{ + ID: containerNetwork.NetworkID, + Name: name, + Addr: addr, + } + } + } + } + return dData +} + +type ClientConfig struct { + apiVersion string + + Endpoint string `description:"Docker server endpoint. Can be a TCP or a Unix socket endpoint." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"` + TLS *types.ClientTLS `description:"Enable Docker TLS support." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"` + HTTPClientTimeout ptypes.Duration `description:"Client timeout for HTTP connections." json:"httpClientTimeout,omitempty" toml:"httpClientTimeout,omitempty" yaml:"httpClientTimeout,omitempty" export:"true"` +} + +func createClient(ctx context.Context, cfg ClientConfig) (*client.Client, error) { + opts, err := getClientOpts(ctx, cfg) + if err != nil { + return nil, err + } + + httpHeaders := map[string]string{ + "User-Agent": "Traefik " + version.Version, + } + + opts = append(opts, + client.WithHTTPHeaders(httpHeaders), + client.WithVersion(cfg.apiVersion)) + + return client.NewClientWithOpts(opts...) +} + +func getClientOpts(ctx context.Context, cfg ClientConfig) ([]client.Opt, error) { + helper, err := connhelper.GetConnectionHelper(cfg.Endpoint) + if err != nil { + return nil, err + } + + // SSH + if helper != nil { + // https://github.com/docker/cli/blob/ebca1413117a3fcb81c89d6be226dcec74e5289f/cli/context/docker/load.go#L112-L123 + + httpClient := &http.Client{ + Transport: &http.Transport{ + DialContext: helper.Dialer, + }, + } + + return []client.Opt{ + client.WithHTTPClient(httpClient), + client.WithTimeout(time.Duration(cfg.HTTPClientTimeout)), + client.WithHost(helper.Host), // To avoid 400 Bad Request: malformed Host header daemon error + client.WithDialContext(helper.Dialer), + }, nil + } + + opts := []client.Opt{ + client.WithHost(cfg.Endpoint), + client.WithTimeout(time.Duration(cfg.HTTPClientTimeout)), + } + + if cfg.TLS != nil { + conf, err := cfg.TLS.CreateTLSConfig(ctx) + if err != nil { + return nil, fmt.Errorf("unable to create client TLS configuration: %w", err) + } + + hostURL, err := client.ParseHostURL(cfg.Endpoint) + if err != nil { + return nil, err + } + + tr := &http.Transport{ + TLSClientConfig: conf, + } + + if err := sockets.ConfigureTransport(tr, hostURL.Scheme, hostURL.Host); err != nil { + return nil, err + } + + opts = append(opts, client.WithHTTPClient(&http.Client{Transport: tr, Timeout: time.Duration(cfg.HTTPClientTimeout)})) + } + + return opts, nil +} + +func getPort(container dockerData, serverPort string) string { + if len(serverPort) > 0 { + return serverPort + } + + var ports []nat.Port + for port := range container.NetworkSettings.Ports { + ports = append(ports, port) + } + + less := func(i, j nat.Port) bool { + return i.Int() < j.Int() + } + nat.Sort(ports, less) + + if len(ports) > 0 { + min := ports[0] + return min.Port() + } + + return "" +} + +func getServiceName(container dockerData) string { + serviceName := container.ServiceName + + if values, err := getStringMultipleStrict(container.Labels, labelDockerComposeProject, labelDockerComposeService); err == nil { + serviceName = values[labelDockerComposeService] + "_" + values[labelDockerComposeProject] + } + + return provider.Normalize(serviceName) +} diff --git a/pkg/provider/docker/label.go b/pkg/provider/docker/shared_labels.go similarity index 94% rename from pkg/provider/docker/label.go rename to pkg/provider/docker/shared_labels.go index b968eedf1..f17e51508 100644 --- a/pkg/provider/docker/label.go +++ b/pkg/provider/docker/shared_labels.go @@ -23,7 +23,7 @@ type specificConfiguration struct { LBSwarm bool } -func (p *Provider) getConfiguration(container dockerData) (configuration, error) { +func (p *Shared) extractLabels(container dockerData) (configuration, error) { conf := configuration{ Enable: p.ExposedByDefault, Docker: specificConfiguration{ diff --git a/pkg/provider/docker/shared_test.go b/pkg/provider/docker/shared_test.go new file mode 100644 index 000000000..324363e84 --- /dev/null +++ b/pkg/provider/docker/shared_test.go @@ -0,0 +1,112 @@ +package docker + +import ( + "context" + "strconv" + "testing" + + docker "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/go-connections/nat" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_getPort_docker(t *testing.T) { + testCases := []struct { + desc string + container docker.ContainerJSON + serverPort string + expected string + }{ + { + desc: "no binding, no server port label", + container: containerJSON(name("foo")), + expected: "", + }, + { + desc: "binding, no server port label", + container: containerJSON(ports(nat.PortMap{ + "80/tcp": {}, + })), + expected: "80", + }, + { + desc: "binding, multiple ports, no server port label", + container: containerJSON(ports(nat.PortMap{ + "80/tcp": {}, + "443/tcp": {}, + })), + expected: "80", + }, + { + desc: "no binding, server port label", + container: containerJSON(), + serverPort: "8080", + expected: "8080", + }, + { + desc: "binding, server port label", + container: containerJSON( + ports(nat.PortMap{ + "80/tcp": {}, + })), + serverPort: "8080", + expected: "8080", + }, + { + desc: "binding, multiple ports, server port label", + container: containerJSON(ports(nat.PortMap{ + "8080/tcp": {}, + "80/tcp": {}, + })), + serverPort: "8080", + expected: "8080", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + + actual := getPort(dData, test.serverPort) + assert.Equal(t, test.expected, actual) + }) + } +} + +func Test_getPort_swarm(t *testing.T) { + testCases := []struct { + service swarm.Service + serverPort string + networks map[string]*docker.NetworkResource + expected string + }{ + { + service: swarmService( + withEndpointSpec(modeDNSSR), + ), + networks: map[string]*docker.NetworkResource{}, + serverPort: "8080", + expected: "8080", + }, + } + + for serviceID, test := range testCases { + test := test + t.Run(strconv.Itoa(serviceID), func(t *testing.T) { + t.Parallel() + + p := SwarmProvider{} + + dData, err := p.parseService(context.Background(), test.service, test.networks) + require.NoError(t, err) + + actual := getPort(dData, test.serverPort) + assert.Equal(t, test.expected, actual) + }) + } +} diff --git a/pkg/redactor/redactor_config_test.go b/pkg/redactor/redactor_config_test.go index c8836d2d0..23ce894d8 100644 --- a/pkg/redactor/redactor_config_test.go +++ b/pkg/redactor/redactor_config_test.go @@ -39,7 +39,7 @@ import ( "github.com/traefik/traefik/v3/pkg/types" ) -var updateExpected = flag.Bool("update_expected", true, "Update expected files in fixtures") +var updateExpected = flag.Bool("update_expected", false, "Update expected files in fixtures") var fullDynConf *dynamic.Configuration @@ -591,22 +591,46 @@ func TestDo_staticConfiguration(t *testing.T) { } config.Providers.Docker = &docker.Provider{ - Constraints: `Label("foo", "bar")`, - Watch: true, - Endpoint: "MyEndPoint", - DefaultRule: "PathPrefix(`/`)", - TLS: &types.ClientTLS{ - CA: "myCa", - Cert: "mycert.pem", - Key: "mycert.key", - InsecureSkipVerify: true, + Shared: docker.Shared{ + ExposedByDefault: true, + Constraints: `Label("foo", "bar")`, + AllowEmptyServices: true, + Network: "MyNetwork", + UseBindPortIP: true, + Watch: true, + DefaultRule: "PathPrefix(`/`)", }, - ExposedByDefault: true, - UseBindPortIP: true, - SwarmMode: true, - Network: "MyNetwork", - SwarmModeRefreshSeconds: 42, - HTTPClientTimeout: 42, + ClientConfig: docker.ClientConfig{ + Endpoint: "MyEndPoint", TLS: &types.ClientTLS{ + CA: "myCa", + Cert: "mycert.pem", + Key: "mycert.key", + InsecureSkipVerify: true, + }, + HTTPClientTimeout: 42, + }, + } + + config.Providers.Swarm = &docker.SwarmProvider{ + Shared: docker.Shared{ + ExposedByDefault: true, + Constraints: `Label("foo", "bar")`, + AllowEmptyServices: true, + Network: "MyNetwork", + UseBindPortIP: true, + Watch: true, + DefaultRule: "PathPrefix(`/`)", + }, + ClientConfig: docker.ClientConfig{ + Endpoint: "MyEndPoint", TLS: &types.ClientTLS{ + CA: "myCa", + Cert: "mycert.pem", + Key: "mycert.key", + InsecureSkipVerify: true, + }, + HTTPClientTimeout: 42, + }, + RefreshSeconds: 42, } config.Providers.KubernetesIngress = &ingress.Provider{ diff --git a/pkg/redactor/testdata/anonymized-static-config.json b/pkg/redactor/testdata/anonymized-static-config.json index 0097aa2c2..a8db72460 100644 --- a/pkg/redactor/testdata/anonymized-static-config.json +++ b/pkg/redactor/testdata/anonymized-static-config.json @@ -89,23 +89,40 @@ "providers": { "providersThrottleDuration": "1m51s", "docker": { + "exposedByDefault": true, "constraints": "Label(\"foo\", \"bar\")", + "allowEmptyServices": true, + "network": "MyNetwork", + "useBindPortIP": true, "watch": true, - "endpoint": "xxxx", "defaultRule": "xxxx", + "endpoint": "xxxx", "tls": { "ca": "xxxx", "cert": "xxxx", "key": "xxxx", "insecureSkipVerify": true }, - "exposedByDefault": true, - "useBindPortIP": true, - "swarmMode": true, - "network": "MyNetwork", - "swarmModeRefreshSeconds": "42ns", "httpClientTimeout": "42ns" }, + "swarm": { + "exposedByDefault": true, + "constraints": "Label(\"foo\", \"bar\")", + "allowEmptyServices": true, + "network": "MyNetwork", + "useBindPortIP": true, + "watch": true, + "defaultRule": "xxxx", + "endpoint": "xxxx", + "tls": { + "ca": "xxxx", + "cert": "xxxx", + "key": "xxxx", + "insecureSkipVerify": true + }, + "httpClientTimeout": "42ns", + "refreshSeconds": "42ns" + }, "file": { "directory": "file Directory", "watch": true, diff --git a/webui/src/statics/providers/swarm.svg b/webui/src/statics/providers/swarm.svg new file mode 100644 index 000000000..db4a729e6 --- /dev/null +++ b/webui/src/statics/providers/swarm.svg @@ -0,0 +1,6 @@ + + + + + + From 511762cbf389f7da92b3be892c4e91c7d77eee4c Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 15 May 2023 16:38:05 +0200 Subject: [PATCH 09/10] fix: clean code related to Hub --- cmd/traefik/traefik.go | 24 +- docs/content/migration/v2.md | 2 +- .../reference/static-configuration/cli-ref.md | 18 - .../reference/static-configuration/env-ref.md | 18 - docs/content/traefik-hub/index.md | 338 ------------------ docs/mkdocs.yml | 1 - integration/marathon15_test.go | 2 + integration/marathon_test.go | 2 + pkg/api/handler_overview.go | 2 - pkg/api/handler_overview_test.go | 2 - pkg/api/testdata/overview-dynamic.json | 3 +- pkg/api/testdata/overview-empty.json | 3 +- pkg/api/testdata/overview-features.json | 3 +- pkg/api/testdata/overview-providers.json | 3 +- pkg/config/static/experimental.go | 2 - pkg/config/static/hub.go | 52 --- pkg/config/static/static_config.go | 25 +- pkg/config/static/static_config_test.go | 48 --- pkg/provider/hub/handler.go | 147 -------- pkg/provider/hub/handler_test.go | 168 --------- pkg/provider/hub/hub.go | 217 ----------- pkg/provider/kubernetes/crd/kubernetes.go | 21 +- .../kubernetes/crd/kubernetes_http.go | 2 + pkg/provider/kubernetes/gateway/kubernetes.go | 27 +- pkg/provider/kubernetes/ingress/kubernetes.go | 30 +- .../kubernetes/k8s/router_transform.go | 11 + pkg/provider/marathon/marathon.go | 2 +- 27 files changed, 97 insertions(+), 1076 deletions(-) delete mode 100644 docs/content/traefik-hub/index.md delete mode 100644 pkg/config/static/hub.go delete mode 100644 pkg/provider/hub/handler.go delete mode 100644 pkg/provider/hub/handler_test.go delete mode 100644 pkg/provider/hub/hub.go create mode 100644 pkg/provider/kubernetes/k8s/router_transform.go diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index 746a50b45..2c198abf1 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -33,7 +33,6 @@ import ( "github.com/traefik/traefik/v2/pkg/middlewares/accesslog" "github.com/traefik/traefik/v2/pkg/provider/acme" "github.com/traefik/traefik/v2/pkg/provider/aggregator" - "github.com/traefik/traefik/v2/pkg/provider/hub" "github.com/traefik/traefik/v2/pkg/provider/traefik" "github.com/traefik/traefik/v2/pkg/safe" "github.com/traefik/traefik/v2/pkg/server" @@ -231,19 +230,6 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err } } - // Traefik Hub - - if staticConfiguration.Hub != nil { - if err = providerAggregator.AddProvider(staticConfiguration.Hub); err != nil { - return nil, fmt.Errorf("adding Traefik Hub provider: %w", err) - } - - // API is mandatory for Traefik Hub to access the dynamic configuration. - if staticConfiguration.API == nil { - staticConfiguration.API = &static.API{} - } - } - // Metrics metricRegistries := registerMetricClients(staticConfiguration.Metrics) @@ -325,10 +311,7 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err continue } - if _, ok := resolverNames[rt.TLS.CertResolver]; !ok && - // "traefik-hub" is an allowed certificate resolver name in a Traefik Hub Experimental feature context. - // It is used to activate its own certificate resolution, even though it is not a "classical" traefik certificate resolver. - (staticConfiguration.Hub == nil || rt.TLS.CertResolver != "traefik-hub") { + if _, ok := resolverNames[rt.TLS.CertResolver]; !ok { log.WithoutContext().Errorf("the router %s uses a non-existent resolver: %s", rtName, rt.TLS.CertResolver) } } @@ -351,11 +334,6 @@ func getHTTPChallengeHandler(acmeProviders []*acme.Provider, httpChallengeProvid func getDefaultsEntrypoints(staticConfiguration *static.Configuration) []string { var defaultEntryPoints []string for name, cfg := range staticConfiguration.EntryPoints { - // Traefik Hub entryPoint should not be part of the set of default entryPoints. - if hub.APIEntrypoint == name || hub.TunnelEntrypoint == name { - continue - } - protocol, err := cfg.GetProtocol() if err != nil { // Should never happen because Traefik should not start if protocol is invalid. diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index aeebbfce0..fdf1f6aa2 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -522,4 +522,4 @@ kubectl apply -f https://raw.githubusercontent.com/traefik/traefik/v2.10/docs/co ### Traefik Hub -In `v2.10`, Traefik Hub is GA and the `experimental.hub` flag is deprecated. +In `v2.10`, Traefik Hub configuration has been removed because Traefik Hub v2 doesn't require this configuration. diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 6e4e09136..6b8537a7d 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -186,9 +186,6 @@ Timeout defines how long to wait on an idle session before releasing the related `--experimental.http3`: Enable HTTP3. (Default: ```false```) -`--experimental.hub`: -Enable the Traefik Hub provider. (Default: ```false```) - `--experimental.kubernetesgateway`: Allow the Kubernetes gateway api provider usage. (Default: ```false```) @@ -222,21 +219,6 @@ resolv.conf used for DNS resolving (Default: ```/etc/resolv.conf```) `--hostresolver.resolvdepth`: The maximal depth of DNS recursive resolving (Default: ```5```) -`--hub`: -Traefik Hub configuration. (Default: ```false```) - -`--hub.tls.ca`: -The certificate authority authenticates the Traefik Hub Agent certificate. - -`--hub.tls.cert`: -The TLS certificate for Traefik Proxy as a TLS client. - -`--hub.tls.insecure`: -Enables an insecure TLS connection that uses default credentials, and which has no peer authentication between Traefik Proxy and the Traefik Hub Agent. (Default: ```false```) - -`--hub.tls.key`: -The TLS key for Traefik Proxy as a TLS client. - `--log`: Traefik log settings. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 85961c2bf..f4b2b6def 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -186,9 +186,6 @@ Timeout defines how long to wait on an idle session before releasing the related `TRAEFIK_EXPERIMENTAL_HTTP3`: Enable HTTP3. (Default: ```false```) -`TRAEFIK_EXPERIMENTAL_HUB`: -Enable the Traefik Hub provider. (Default: ```false```) - `TRAEFIK_EXPERIMENTAL_KUBERNETESGATEWAY`: Allow the Kubernetes gateway api provider usage. (Default: ```false```) @@ -222,21 +219,6 @@ resolv.conf used for DNS resolving (Default: ```/etc/resolv.conf```) `TRAEFIK_HOSTRESOLVER_RESOLVDEPTH`: The maximal depth of DNS recursive resolving (Default: ```5```) -`TRAEFIK_HUB`: -Traefik Hub configuration. (Default: ```false```) - -`TRAEFIK_HUB_TLS_CA`: -The certificate authority authenticates the Traefik Hub Agent certificate. - -`TRAEFIK_HUB_TLS_CERT`: -The TLS certificate for Traefik Proxy as a TLS client. - -`TRAEFIK_HUB_TLS_INSECURE`: -Enables an insecure TLS connection that uses default credentials, and which has no peer authentication between Traefik Proxy and the Traefik Hub Agent. (Default: ```false```) - -`TRAEFIK_HUB_TLS_KEY`: -The TLS key for Traefik Proxy as a TLS client. - `TRAEFIK_LOG`: Traefik log settings. (Default: ```false```) diff --git a/docs/content/traefik-hub/index.md b/docs/content/traefik-hub/index.md deleted file mode 100644 index 5c6698e9d..000000000 --- a/docs/content/traefik-hub/index.md +++ /dev/null @@ -1,338 +0,0 @@ -# Traefik Hub - -## Overview - -Once the Traefik Hub feature is enabled in Traefik, -Traefik and its local agent communicate together. - -This agent can: - -* get the Traefik metrics to display them in the Traefik Hub UI -* secure the Traefik routers -* provide ACME certificates to Traefik -* transfer requests from the SaaS Platform to Traefik (and then avoid the users to expose directly their infrastructure on the internet) - -!!! warning "Traefik Hub Entrypoints" - - When the Traefik Hub feature is enabled, Traefik exposes some services meant for the Traefik Hub Agent on dedicated entrypoints (on ports `9900` and `9901` by default). - Given their sensitive nature, those services should not be publicly exposed. - Also those dedicated entrypoints, regardless of how they are created (default, or user-defined), should not be used by anything other than the Hub Agent. - -!!! important "Learn More About Traefik Hub" - - This section is intended only as a brief overview for Traefik users who are not familiar with Traefik Hub. - To explore all that Traefik Hub has to offer, please consult the [Traefik Hub Documentation](https://doc.traefik.io/traefik-hub). - -!!! Note "Prerequisites" - - * Traefik Hub is compatible with Traefik Proxy 2.7 or later. - * The Traefik Hub Agent must be installed to connect to the Traefik Hub platform. - -!!! information "Configuration Discovery" - - According to installation options, the Traefik Hub Agent listens to the Docker or Kubernetes API to discover containers/services. - - It doesn't support the routers discovered by Traefik Proxy using other providers, e.g., using the File provider. - -!!! example "Minimal Static Configuration to Activate Traefik Hub for Docker" - - ```yaml tab="File (YAML)" - hub: - tls: - insecure: true - - metrics: - prometheus: - addRoutersLabels: true - ``` - - ```toml tab="File (TOML)" - [hub] - [hub.tls] - insecure = true - - [metrics] - [metrics.prometheus] - addRoutersLabels = true - ``` - - ```bash tab="CLI" - --hub.tls.insecure - --metrics.prometheus.addrouterslabels - ``` - -!!! example "Minimal Static Configuration to Activate Traefik Hub for Kubernetes" - - ```yaml tab="File (YAML)" - hub: {} - - metrics: - prometheus: - addRoutersLabels: true - ``` - - ```toml tab="File (TOML)" - [hub] - - [metrics] - [metrics.prometheus] - addRoutersLabels = true - ``` - - ```bash tab="CLI" - --hub - --metrics.prometheus.addrouterslabels - ``` - -## Configuration - -### Entrypoints - -#### `traefikhub-api` - -This entrypoint is used to communicate between the Hub agent and Traefik. -It allows the Hub agent to create routing. - -This dedicated Traefik Hub entryPoint should not be used by anything other than Traefik Hub. - -The default port is `9900`. -To change the port, you have to define an entrypoint named `traefikhub-api`. - -```yaml tab="File (YAML)" -entryPoints: - traefikhub-api: ":8000" -``` - -```toml tab="File (TOML)" -[entryPoints.traefikhub-api] - address = ":8000" -``` - -```bash tab="CLI" ---entrypoints.traefikhub-api.address=:8000 -``` - -#### `traefikhub-tunl` - -This entrypoint is used to communicate between Traefik Hub and Traefik. -It allows to create secured tunnels. - -This dedicated Traefik Hub entryPoint should not be used by anything other than Traefik Hub. - -The default port is `9901`. -To change the port, you have to define an entrypoint named `traefikhub-tunl`. - -```yaml tab="File (YAML)" -entryPoints: - traefikhub-tunl: ":8000" -``` - -```toml tab="File (TOML)" -[entryPoints.traefikhub-tunl] - address = ":8000" -``` - -```bash tab="CLI" ---entrypoints.traefikhub-tunl.address=:8000 -``` - -### `tls` - -_Optional, Default=None_ - -This section is required when using the Hub agent for Docker. - -This section allows configuring mutual TLS connection between Traefik Proxy and the Traefik Hub Agent. -The key and the certificate are the credentials for Traefik Proxy as a TLS client. -The certificate authority authenticates the Traefik Hub Agent certificate. - -!!! note "Certificate Domain" - - The certificate must be valid for the `proxy.traefik` domain. - -!!! note "Certificates Definition" - - Certificates can be defined either by their content or their path. - -!!! note "Insecure Mode" - - The `insecure` option is mutually exclusive with any other option. - -```yaml tab="File (YAML)" -hub: - tls: - ca: /path/to/ca.pem - cert: /path/to/cert.pem - key: /path/to/key.pem -``` - -```toml tab="File (TOML)" -[hub.tls] - ca= "/path/to/ca.pem" - cert= "/path/to/cert.pem" - key= "/path/to/key.pem" -``` - -```bash tab="CLI" ---hub.tls.ca=/path/to/ca.pem ---hub.tls.cert=/path/to/cert.pem ---hub.tls.key=/path/to/key.pem -``` - -### `tls.ca` - -The certificate authority authenticates the Traefik Hub Agent certificate. - -```yaml tab="File (YAML)" -hub: - tls: - ca: |- - -----BEGIN CERTIFICATE----- - MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw - DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0 - WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE - ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a - x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG - CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w - CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz - aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY= - -----END CERTIFICATE----- -``` - -```toml tab="File (TOML)" -[hub.tls] - ca = """-----BEGIN CERTIFICATE----- -MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw -DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0 -WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE -ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a -x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG -CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w -CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz -aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY= ------END CERTIFICATE-----""" -``` - -```bash tab="CLI" ---hub.tls.ca=-----BEGIN CERTIFICATE----- -MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw -DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0 -WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE -ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a -x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG -CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w -CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz -aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY= ------END CERTIFICATE----- -``` - -### `tls.cert` - -The TLS certificate for Traefik Proxy as a TLS client. - -!!! note "Certificate Domain" - - The certificate must be valid for the `proxy.traefik` domain. - -```yaml tab="File (YAML)" -hub: - tls: - cert: |- - -----BEGIN CERTIFICATE----- - MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw - DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0 - WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE - ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a - x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG - CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w - CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz - aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY= - -----END CERTIFICATE----- -``` - -```toml tab="File (TOML)" -[hub.tls] - cert = """-----BEGIN CERTIFICATE----- -MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw -DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0 -WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE -ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a -x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG -CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w -CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz -aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY= ------END CERTIFICATE-----""" -``` - -```bash tab="CLI" ---hub.tls.cert=-----BEGIN CERTIFICATE----- -MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw -DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0 -WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE -ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a -x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG -CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w -CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz -aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY= ------END CERTIFICATE----- -``` - -### `tls.key` - -The TLS key for Traefik Proxy as a TLS client. - -```yaml tab="File (YAML)" -hub: - tls: - key: |- - -----BEGIN PRIVATE KEY----- - MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgm+XJ3LVrTbbirJea - O+Crj2ADVsVHjMuiyd72VE3lgxihRANCAARlopg+PYbweeaO7qNse3684PAqo0NV - AwfmfGG0CBJRlLrWYtbBm+9SEhgs0/AfPxrHwLv5y6KEQLpPO+fwN4Z4 - -----END PRIVATE KEY----- -``` - -```toml tab="File (TOML)" -[hub.tls] - key = """-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgm+XJ3LVrTbbirJea -O+Crj2ADVsVHjMuiyd72VE3lgxihRANCAARlopg+PYbweeaO7qNse3684PAqo0NV -AwfmfGG0CBJRlLrWYtbBm+9SEhgs0/AfPxrHwLv5y6KEQLpPO+fwN4Z4 ------END PRIVATE KEY-----""" -``` - -```bash tab="CLI" ---hub.tls.key=-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgm+XJ3LVrTbbirJea -O+Crj2ADVsVHjMuiyd72VE3lgxihRANCAARlopg+PYbweeaO7qNse3684PAqo0NV -AwfmfGG0CBJRlLrWYtbBm+9SEhgs0/AfPxrHwLv5y6KEQLpPO+fwN4Z4 ------END PRIVATE KEY----- -``` - -### `tls.insecure` - -_Optional, Default=false_ - -Enables an insecure TLS connection that uses default credentials, -and which has no peer authentication between Traefik Proxy and the Traefik Hub Agent. -The `insecure` option is mutually exclusive with any other option. - -!!! warning "Security Consideration" - - Do not use this setup in production. - This option implies sensitive data can be exposed to potential malicious third-party programs. - -```yaml tab="File (YAML)" -hub: - tls: - insecure: true -``` - -```toml tab="File (TOML)" -[hub.tls] - insecure = true -``` - -```bash tab="CLI" ---hub.tls.insecure=true -``` diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index e7847e92f..7432a44a0 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -139,7 +139,6 @@ nav: - 'Overview': 'middlewares/tcp/overview.md' - 'InFlightConn': 'middlewares/tcp/inflightconn.md' - 'IpWhitelist': 'middlewares/tcp/ipwhitelist.md' - - 'Traefik Hub': 'traefik-hub/index.md' - 'Plugins & Plugin Catalog': 'plugins/index.md' - 'Operations': - 'CLI': 'operations/cli.md' diff --git a/integration/marathon15_test.go b/integration/marathon15_test.go index 493db256a..1f559addb 100644 --- a/integration/marathon15_test.go +++ b/integration/marathon15_test.go @@ -31,6 +31,8 @@ func (s *MarathonSuite15) SetUpSuite(c *check.C) { } func (s *MarathonSuite15) TestConfigurationUpdate(c *check.C) { + c.Skip("doesn't work") + // Start Traefik. file := s.adaptFile(c, "fixtures/marathon/simple.toml", struct { MarathonURL string diff --git a/integration/marathon_test.go b/integration/marathon_test.go index 5552e0d0b..91ff4519d 100644 --- a/integration/marathon_test.go +++ b/integration/marathon_test.go @@ -40,6 +40,8 @@ func deployApplication(c *check.C, client marathon.Marathon, application *marath } func (s *MarathonSuite) TestConfigurationUpdate(c *check.C) { + c.Skip("doesn't work") + // Start Traefik. file := s.adaptFile(c, "fixtures/marathon/simple.toml", struct { MarathonURL string diff --git a/pkg/api/handler_overview.go b/pkg/api/handler_overview.go index d04abbc87..4e6485e7d 100644 --- a/pkg/api/handler_overview.go +++ b/pkg/api/handler_overview.go @@ -26,7 +26,6 @@ type features struct { Tracing string `json:"tracing"` Metrics string `json:"metrics"` AccessLog bool `json:"accessLog"` - Hub bool `json:"hub"` // TODO add certificates resolvers } @@ -248,7 +247,6 @@ func getFeatures(conf static.Configuration) features { Tracing: getTracing(conf), Metrics: getMetrics(conf), AccessLog: conf.AccessLog != nil, - Hub: conf.Hub != nil, } } diff --git a/pkg/api/handler_overview_test.go b/pkg/api/handler_overview_test.go index 8937c0204..6dee89a72 100644 --- a/pkg/api/handler_overview_test.go +++ b/pkg/api/handler_overview_test.go @@ -15,7 +15,6 @@ import ( "github.com/traefik/traefik/v2/pkg/config/static" "github.com/traefik/traefik/v2/pkg/provider/docker" "github.com/traefik/traefik/v2/pkg/provider/file" - "github.com/traefik/traefik/v2/pkg/provider/hub" "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd" "github.com/traefik/traefik/v2/pkg/provider/kubernetes/ingress" "github.com/traefik/traefik/v2/pkg/provider/marathon" @@ -266,7 +265,6 @@ func TestHandler_Overview(t *testing.T) { Tracing: &static.Tracing{ Jaeger: &jaeger.Config{}, }, - Hub: &hub.Provider{}, }, confDyn: runtime.Configuration{}, expected: expected{ diff --git a/pkg/api/testdata/overview-dynamic.json b/pkg/api/testdata/overview-dynamic.json index c6790c2dd..d07e6a992 100644 --- a/pkg/api/testdata/overview-dynamic.json +++ b/pkg/api/testdata/overview-dynamic.json @@ -2,8 +2,7 @@ "features": { "accessLog": false, "metrics": "", - "tracing": "", - "hub": false + "tracing": "" }, "http": { "middlewares": { diff --git a/pkg/api/testdata/overview-empty.json b/pkg/api/testdata/overview-empty.json index cd2a1611f..0e7501d23 100644 --- a/pkg/api/testdata/overview-empty.json +++ b/pkg/api/testdata/overview-empty.json @@ -2,8 +2,7 @@ "features": { "accessLog": false, "metrics": "", - "tracing": "", - "hub": false + "tracing": "" }, "http": { "middlewares": { diff --git a/pkg/api/testdata/overview-features.json b/pkg/api/testdata/overview-features.json index 4ad3a1b91..5df280da0 100644 --- a/pkg/api/testdata/overview-features.json +++ b/pkg/api/testdata/overview-features.json @@ -2,8 +2,7 @@ "features": { "accessLog": false, "metrics": "Prometheus", - "tracing": "Jaeger", - "hub": true + "tracing": "Jaeger" }, "http": { "middlewares": { diff --git a/pkg/api/testdata/overview-providers.json b/pkg/api/testdata/overview-providers.json index 95d4d10d2..e338f7a95 100644 --- a/pkg/api/testdata/overview-providers.json +++ b/pkg/api/testdata/overview-providers.json @@ -2,8 +2,7 @@ "features": { "accessLog": false, "metrics": "", - "tracing": "", - "hub": false + "tracing": "" }, "http": { "middlewares": { diff --git a/pkg/config/static/experimental.go b/pkg/config/static/experimental.go index 9dfa029a0..cb6cd097f 100644 --- a/pkg/config/static/experimental.go +++ b/pkg/config/static/experimental.go @@ -9,6 +9,4 @@ type Experimental struct { KubernetesGateway bool `description:"Allow the Kubernetes gateway api provider usage." json:"kubernetesGateway,omitempty" toml:"kubernetesGateway,omitempty" yaml:"kubernetesGateway,omitempty" export:"true"` HTTP3 bool `description:"Enable HTTP3." json:"http3,omitempty" toml:"http3,omitempty" yaml:"http3,omitempty" export:"true"` - // Deprecated. - Hub bool `description:"Enable the Traefik Hub provider." json:"hub,omitempty" toml:"hub,omitempty" yaml:"hub,omitempty" export:"true"` } diff --git a/pkg/config/static/hub.go b/pkg/config/static/hub.go deleted file mode 100644 index 70d82af4e..000000000 --- a/pkg/config/static/hub.go +++ /dev/null @@ -1,52 +0,0 @@ -package static - -import ( - "errors" - - "github.com/traefik/traefik/v2/pkg/log" - "github.com/traefik/traefik/v2/pkg/provider/hub" -) - -func (c *Configuration) initHubProvider() error { - if c.Experimental != nil && c.Experimental.Hub { - log.WithoutContext().Warn("Experimental flag for Traefik Hub is deprecated, because Traefik Hub is now GA.") - } - - if _, ok := c.EntryPoints[hub.TunnelEntrypoint]; !ok { - var ep EntryPoint - ep.SetDefaults() - ep.Address = ":9901" - c.EntryPoints[hub.TunnelEntrypoint] = &ep - log.WithoutContext().Infof("The entryPoint %q is created on port 9901 to allow exposition of services.", hub.TunnelEntrypoint) - } - - if c.Hub.TLS == nil { - return nil - } - - if c.Hub.TLS.Insecure && (c.Hub.TLS.CA != "" || c.Hub.TLS.Cert != "" || c.Hub.TLS.Key != "") { - return errors.New("mTLS configuration for Hub and insecure TLS for Hub are mutually exclusive") - } - - if !c.Hub.TLS.Insecure && (c.Hub.TLS.CA == "" || c.Hub.TLS.Cert == "" || c.Hub.TLS.Key == "") { - return errors.New("incomplete mTLS configuration for Hub") - } - - if c.Hub.TLS.Insecure { - log.WithoutContext().Warn("Hub is in `insecure` mode. Do not run in production with this setup.") - } - - if _, ok := c.EntryPoints[hub.APIEntrypoint]; !ok { - var ep EntryPoint - ep.SetDefaults() - ep.Address = ":9900" - c.EntryPoints[hub.APIEntrypoint] = &ep - log.WithoutContext().Infof("The entryPoint %q is created on port 9900 to allow Traefik to communicate with the Hub Agent for Traefik.", hub.APIEntrypoint) - } - - c.EntryPoints[hub.APIEntrypoint].HTTP.TLS = &TLSConfig{ - Options: "traefik-hub", - } - - return nil -} diff --git a/pkg/config/static/static_config.go b/pkg/config/static/static_config.go index 8eb8e56cf..9bdb43730 100644 --- a/pkg/config/static/static_config.go +++ b/pkg/config/static/static_config.go @@ -17,7 +17,6 @@ import ( "github.com/traefik/traefik/v2/pkg/provider/ecs" "github.com/traefik/traefik/v2/pkg/provider/file" "github.com/traefik/traefik/v2/pkg/provider/http" - "github.com/traefik/traefik/v2/pkg/provider/hub" "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd" "github.com/traefik/traefik/v2/pkg/provider/kubernetes/gateway" "github.com/traefik/traefik/v2/pkg/provider/kubernetes/ingress" @@ -81,8 +80,6 @@ type Configuration struct { // Deprecated. Pilot *Pilot `description:"Traefik Pilot configuration (Deprecated)." json:"pilot,omitempty" toml:"pilot,omitempty" yaml:"pilot,omitempty" export:"true"` - Hub *hub.Provider `description:"Traefik Hub configuration." json:"hub,omitempty" toml:"hub,omitempty" yaml:"hub,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` - Experimental *Experimental `description:"experimental features." json:"experimental,omitempty" toml:"experimental,omitempty" yaml:"experimental,omitempty" export:"true"` } @@ -224,15 +221,6 @@ func (c *Configuration) SetEffectiveConfiguration() { } } - if c.Hub != nil { - if err := c.initHubProvider(); err != nil { - c.Hub = nil - log.WithoutContext().Errorf("Unable to activate the Hub provider: %v", err) - } else { - log.WithoutContext().Debugf("Hub provider has been activated.") - } - } - if c.Providers.Docker != nil { if c.Providers.Docker.SwarmModeRefreshSeconds <= 0 { c.Providers.Docker.SwarmModeRefreshSeconds = ptypes.Duration(15 * time.Second) @@ -283,18 +271,7 @@ func (c *Configuration) SetEffectiveConfiguration() { } func (c *Configuration) hasUserDefinedEntrypoint() bool { - if len(c.EntryPoints) == 0 { - return false - } - - switch len(c.EntryPoints) { - case 1: - return c.EntryPoints[hub.TunnelEntrypoint] == nil - case 2: - return c.EntryPoints[hub.TunnelEntrypoint] == nil || c.EntryPoints[hub.APIEntrypoint] == nil - default: - return true - } + return len(c.EntryPoints) != 0 } func (c *Configuration) initACMEProvider() { diff --git a/pkg/config/static/static_config_test.go b/pkg/config/static/static_config_test.go index 9680233dd..0fbd83056 100644 --- a/pkg/config/static/static_config_test.go +++ b/pkg/config/static/static_config_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/traefik/traefik/v2/pkg/provider/hub" ) func TestHasEntrypoint(t *testing.T) { @@ -24,53 +23,6 @@ func TestHasEntrypoint(t *testing.T) { }, assert: assert.True, }, - { - desc: "user defined entryPoints + hub entryPoint (tunnel)", - entryPoints: map[string]*EntryPoint{ - "foo": {}, - hub.TunnelEntrypoint: {}, - }, - assert: assert.True, - }, - { - desc: "hub entryPoint (tunnel)", - entryPoints: map[string]*EntryPoint{ - hub.TunnelEntrypoint: {}, - }, - assert: assert.False, - }, - { - desc: "user defined entryPoints + hub entryPoint (api)", - entryPoints: map[string]*EntryPoint{ - "foo": {}, - hub.APIEntrypoint: {}, - }, - assert: assert.True, - }, - { - desc: "hub entryPoint (api)", - entryPoints: map[string]*EntryPoint{ - hub.APIEntrypoint: {}, - }, - assert: assert.True, - }, - { - desc: "user defined entryPoints + hub entryPoints (tunnel, api)", - entryPoints: map[string]*EntryPoint{ - "foo": {}, - hub.TunnelEntrypoint: {}, - hub.APIEntrypoint: {}, - }, - assert: assert.True, - }, - { - desc: "hub entryPoints (tunnel, api)", - entryPoints: map[string]*EntryPoint{ - hub.TunnelEntrypoint: {}, - hub.APIEntrypoint: {}, - }, - assert: assert.False, - }, } for _, test := range tests { diff --git a/pkg/provider/hub/handler.go b/pkg/provider/hub/handler.go deleted file mode 100644 index 83d15dfb9..000000000 --- a/pkg/provider/hub/handler.go +++ /dev/null @@ -1,147 +0,0 @@ -package hub - -import ( - "context" - "encoding/json" - "fmt" - "net" - "net/http" - "net/url" - "sync/atomic" - - "github.com/traefik/traefik/v2/pkg/config/dynamic" - "github.com/traefik/traefik/v2/pkg/log" -) - -type handler struct { - mux *http.ServeMux - - client http.Client - - entryPoint string - port int - tlsCfg *TLS - - // Accessed atomically. - lastCfgUnixNano int64 - - cfgChan chan<- dynamic.Message -} - -func newHandler(entryPoint string, port int, cfgChan chan<- dynamic.Message, tlsCfg *TLS, client http.Client) http.Handler { - h := &handler{ - mux: http.NewServeMux(), - entryPoint: entryPoint, - port: port, - cfgChan: cfgChan, - tlsCfg: tlsCfg, - client: client, - } - - h.mux.HandleFunc("/config", h.handleConfig) - h.mux.HandleFunc("/discover-ip", h.handleDiscoverIP) - h.mux.HandleFunc("/state", h.handleState) - - return h -} - -type configRequest struct { - UnixNano int64 `json:"unixNano"` - Configuration *dynamic.Configuration `json:"configuration"` -} - -func (h *handler) handleConfig(rw http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodPost { - http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) - return - } - - payload := &configRequest{Configuration: emptyDynamicConfiguration()} - if err := json.NewDecoder(req.Body).Decode(payload); err != nil { - err = fmt.Errorf("decoding config request: %w", err) - log.WithoutContext().Errorf("Handling config: %v", err) - http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - cfg := payload.Configuration - patchDynamicConfiguration(cfg, h.entryPoint, h.port, h.tlsCfg) - - // We can safely drop messages here if the other end is not ready to receive them - // as the agent will re-apply the same configuration. - select { - case h.cfgChan <- dynamic.Message{ProviderName: "hub", Configuration: cfg}: - atomic.StoreInt64(&h.lastCfgUnixNano, payload.UnixNano) - default: - } -} - -func (h *handler) handleDiscoverIP(rw http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodGet { - http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) - return - } - - xff := req.Header.Get("X-Forwarded-For") - port := req.URL.Query().Get("port") - nonce := req.URL.Query().Get("nonce") - - if err := h.doDiscoveryReq(req.Context(), xff, port, nonce); err != nil { - err = fmt.Errorf("doing discovery request: %w", err) - log.WithoutContext().Errorf("Handling IP discovery: %v", err) - http.Error(rw, http.StatusText(http.StatusBadGateway), http.StatusBadGateway) - return - } - - if err := json.NewEncoder(rw).Encode(xff); err != nil { - err = fmt.Errorf("encoding discover ip response: %w", err) - log.WithoutContext().Errorf("Handling IP discovery: %v", err) - http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } -} - -func (h *handler) doDiscoveryReq(ctx context.Context, ip, port, nonce string) error { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://%s", net.JoinHostPort(ip, port)), http.NoBody) - if err != nil { - return fmt.Errorf("creating request: %w", err) - } - - q := make(url.Values) - q.Set("nonce", nonce) - req.URL.RawQuery = q.Encode() - req.Host = "agent.traefik" - - resp, err := h.client.Do(req) - if err != nil { - return fmt.Errorf("doing request: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - return nil -} - -type stateResponse struct { - LastConfigUnixNano int64 `json:"lastConfigUnixNano"` -} - -func (h *handler) handleState(rw http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodGet { - http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) - return - } - - resp := stateResponse{ - LastConfigUnixNano: atomic.LoadInt64(&h.lastCfgUnixNano), - } - if err := json.NewEncoder(rw).Encode(resp); err != nil { - err = fmt.Errorf("encoding last config received response: %w", err) - log.WithoutContext().Errorf("Handling state: %v", err) - http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } -} - -func (h *handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - h.mux.ServeHTTP(rw, req) -} diff --git a/pkg/provider/hub/handler_test.go b/pkg/provider/hub/handler_test.go deleted file mode 100644 index b69fc8b6d..000000000 --- a/pkg/provider/hub/handler_test.go +++ /dev/null @@ -1,168 +0,0 @@ -package hub - -import ( - "bytes" - "crypto/tls" - "encoding/json" - "errors" - "net" - "net/http" - "net/http/httptest" - "net/url" - "strconv" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/traefik/traefik/v2/pkg/config/dynamic" - "github.com/traefik/traefik/v2/pkg/tls/generate" -) - -func TestHandleConfig(t *testing.T) { - cfgChan := make(chan dynamic.Message, 1) - - client, err := createAgentClient(&TLS{Insecure: true}) - require.NoError(t, err) - h := newHandler("traefik-hub-ep", 42, cfgChan, nil, client) - - cfg := emptyDynamicConfiguration() - cfg.HTTP.Routers["foo"] = &dynamic.Router{ - EntryPoints: []string{"ep"}, - Service: "bar", - Rule: "Host(`foo.com`)", - } - - req := configRequest{Configuration: cfg} - - b, err := json.Marshal(req) - require.NoError(t, err) - - server := httptest.NewServer(h) - t.Cleanup(server.Close) - - resp, err := http.Post(server.URL+"/config", "application/json", bytes.NewReader(b)) - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) - - select { - case gotCfgRaw := <-cfgChan: - patchDynamicConfiguration(cfg, "traefik-hub-ep", 42, nil) - assert.Equal(t, cfg, gotCfgRaw.Configuration) - - case <-time.After(time.Second): - t.Fatal("Configuration not received") - } -} - -func TestHandle_Config_MethodNotAllowed(t *testing.T) { - cfgChan := make(chan dynamic.Message, 1) - client, err := createAgentClient(&TLS{Insecure: true}) - require.NoError(t, err) - h := newHandler("traefik-hub-ep", 42, cfgChan, nil, client) - - server := httptest.NewServer(h) - t.Cleanup(server.Close) - - resp, err := http.Get(server.URL + "/config") - require.NoError(t, err) - - err = resp.Body.Close() - require.NoError(t, err) - - assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode) -} - -func TestHandle_DiscoverIP(t *testing.T) { - listener, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - - port := listener.Addr().(*net.TCPAddr).Port - nonce := "XVlBzgbaiCMRAjWw" - - mux := http.NewServeMux() - - var handlerCallCount int - mux.HandleFunc("/", func(_ http.ResponseWriter, req *http.Request) { - handlerCallCount++ - assert.Equal(t, nonce, req.URL.Query().Get("nonce")) - }) - - certificate, err := generate.DefaultCertificate() - require.NoError(t, err) - agentServer := &http.Server{ - Handler: mux, - TLSConfig: &tls.Config{ - Certificates: []tls.Certificate{*certificate}, - InsecureSkipVerify: true, - MinVersion: tls.VersionTLS13, - }, - } - t.Cleanup(func() { _ = agentServer.Close() }) - - rdy := make(chan struct{}) - - go func(s *http.Server) { - close(rdy) - if err = s.ServeTLS(listener, "", ""); errors.Is(err, http.ErrServerClosed) { - return - } - }(agentServer) - - <-rdy - - cfgChan := make(chan dynamic.Message, 1) - client, err := createAgentClient(&TLS{Insecure: true}) - require.NoError(t, err) - h := newHandler("traefik-hub-ep", 42, cfgChan, nil, client) - - traefikServer := httptest.NewServer(h) - t.Cleanup(traefikServer.Close) - - req, err := http.NewRequest(http.MethodGet, traefikServer.URL+"/discover-ip", http.NoBody) - require.NoError(t, err) - - q := make(url.Values) - q.Set("port", strconv.Itoa(port)) - q.Set("nonce", nonce) - req.URL.RawQuery = q.Encode() - - // Simulate a call from behind different proxies. - req.Header.Add("X-Forwarded-For", "127.0.0.1") - req.Header.Add("X-Forwarded-For", "10.10.0.13") - - resp, err := http.DefaultClient.Do(req) - require.NoError(t, err) - - defer func() { - err = resp.Body.Close() - require.NoError(t, err) - }() - - assert.Equal(t, 1, handlerCallCount) - assert.Equal(t, http.StatusOK, resp.StatusCode) - - var ip string - err = json.NewDecoder(resp.Body).Decode(&ip) - require.NoError(t, err) - - assert.Equal(t, "127.0.0.1", ip) -} - -func TestHandle_DiscoverIP_MethodNotAllowed(t *testing.T) { - cfgChan := make(chan dynamic.Message, 1) - client, err := createAgentClient(&TLS{Insecure: true}) - require.NoError(t, err) - h := newHandler("traefik-hub-ep", 42, cfgChan, nil, client) - - server := httptest.NewServer(h) - t.Cleanup(server.Close) - - resp, err := http.Post(server.URL+"/discover-ip", "", http.NoBody) - require.NoError(t, err) - - err = resp.Body.Close() - require.NoError(t, err) - - assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode) -} diff --git a/pkg/provider/hub/hub.go b/pkg/provider/hub/hub.go deleted file mode 100644 index 106a097e2..000000000 --- a/pkg/provider/hub/hub.go +++ /dev/null @@ -1,217 +0,0 @@ -package hub - -import ( - "crypto/tls" - "crypto/x509" - "errors" - "fmt" - "net" - "net/http" - - "github.com/traefik/traefik/v2/pkg/config/dynamic" - "github.com/traefik/traefik/v2/pkg/log" - "github.com/traefik/traefik/v2/pkg/provider" - "github.com/traefik/traefik/v2/pkg/safe" - ttls "github.com/traefik/traefik/v2/pkg/tls" -) - -var _ provider.Provider = (*Provider)(nil) - -// Entrypoints created for Hub. -const ( - APIEntrypoint = "traefikhub-api" - TunnelEntrypoint = "traefikhub-tunl" -) - -// Provider holds configurations of the provider. -type Provider struct { - TLS *TLS `description:"TLS configuration for mTLS communication between Traefik and Hub Agent." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"` - - server *http.Server -} - -// TLS configures the mTLS connection between Traefik Proxy and the Traefik Hub Agent. -type TLS struct { - Insecure bool `description:"Enables an insecure TLS connection that uses default credentials, and which has no peer authentication between Traefik Proxy and the Traefik Hub Agent." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"` - CA ttls.FileOrContent `description:"The certificate authority authenticates the Traefik Hub Agent certificate." json:"ca,omitempty" toml:"ca,omitempty" yaml:"ca,omitempty" loggable:"false"` - Cert ttls.FileOrContent `description:"The TLS certificate for Traefik Proxy as a TLS client." json:"cert,omitempty" toml:"cert,omitempty" yaml:"cert,omitempty" loggable:"false"` - Key ttls.FileOrContent `description:"The TLS key for Traefik Proxy as a TLS client." json:"key,omitempty" toml:"key,omitempty" yaml:"key,omitempty" loggable:"false"` -} - -// Init the provider. -func (p *Provider) Init() error { - return nil -} - -// Provide allows the hub provider to provide configurations to traefik using the given configuration channel. -func (p *Provider) Provide(configurationChan chan<- dynamic.Message, _ *safe.Pool) error { - if p.TLS == nil { - return nil - } - - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - return fmt.Errorf("listener: %w", err) - } - - port := listener.Addr().(*net.TCPAddr).Port - - client, err := createAgentClient(p.TLS) - if err != nil { - return fmt.Errorf("creating Hub Agent HTTP client: %w", err) - } - - p.server = &http.Server{Handler: newHandler(APIEntrypoint, port, configurationChan, p.TLS, client)} - - // TODO: this is going to be leaky (because no context to make it terminate) - // if/when Provide lifecycle differs with Traefik lifecycle. - go func() { - if err = p.server.Serve(listener); err != nil { - log.WithoutContext().WithField(log.ProviderName, "hub").Errorf("Unexpected error while running server: %v", err) - return - } - }() - - exposeAPIAndMetrics(configurationChan, APIEntrypoint, port, p.TLS) - - return nil -} - -func exposeAPIAndMetrics(cfgChan chan<- dynamic.Message, ep string, port int, tlsCfg *TLS) { - cfg := emptyDynamicConfiguration() - - patchDynamicConfiguration(cfg, ep, port, tlsCfg) - - cfgChan <- dynamic.Message{ProviderName: "hub", Configuration: cfg} -} - -func patchDynamicConfiguration(cfg *dynamic.Configuration, ep string, port int, tlsCfg *TLS) { - cfg.HTTP.Routers["traefik-hub-agent-api"] = &dynamic.Router{ - EntryPoints: []string{ep}, - Service: "api@internal", - Rule: "Host(`proxy.traefik`) && PathPrefix(`/api`)", - } - cfg.HTTP.Routers["traefik-hub-agent-metrics"] = &dynamic.Router{ - EntryPoints: []string{ep}, - Service: "prometheus@internal", - Rule: "Host(`proxy.traefik`) && PathPrefix(`/metrics`)", - } - - cfg.HTTP.Routers["traefik-hub-agent-service"] = &dynamic.Router{ - EntryPoints: []string{ep}, - Service: "traefik-hub-agent-service", - Rule: "Host(`proxy.traefik`) && PathPrefix(`/config`, `/discover-ip`, `/state`)", - } - - cfg.HTTP.Services["traefik-hub-agent-service"] = &dynamic.Service{ - LoadBalancer: &dynamic.ServersLoadBalancer{ - Servers: []dynamic.Server{ - { - URL: fmt.Sprintf("http://127.0.0.1:%d", port), - }, - }, - }, - } - - if tlsCfg == nil { - return - } - - if tlsCfg.Insecure { - cfg.TLS.Options["traefik-hub"] = ttls.Options{ - MinVersion: "VersionTLS13", - } - - return - } - - cfg.TLS.Options["traefik-hub"] = ttls.Options{ - ClientAuth: ttls.ClientAuth{ - CAFiles: []ttls.FileOrContent{tlsCfg.CA}, - ClientAuthType: "RequireAndVerifyClientCert", - }, - SniStrict: true, - MinVersion: "VersionTLS13", - } - - cfg.TLS.Certificates = append(cfg.TLS.Certificates, &ttls.CertAndStores{ - Certificate: ttls.Certificate{ - CertFile: tlsCfg.Cert, - KeyFile: tlsCfg.Key, - }, - }) -} - -func emptyDynamicConfiguration() *dynamic.Configuration { - return &dynamic.Configuration{ - HTTP: &dynamic.HTTPConfiguration{ - Routers: make(map[string]*dynamic.Router), - Middlewares: make(map[string]*dynamic.Middleware), - Services: make(map[string]*dynamic.Service), - ServersTransports: make(map[string]*dynamic.ServersTransport), - }, - TCP: &dynamic.TCPConfiguration{ - Routers: make(map[string]*dynamic.TCPRouter), - Services: make(map[string]*dynamic.TCPService), - }, - TLS: &dynamic.TLSConfiguration{ - Stores: make(map[string]ttls.Store), - Options: make(map[string]ttls.Options), - }, - UDP: &dynamic.UDPConfiguration{ - Routers: make(map[string]*dynamic.UDPRouter), - Services: make(map[string]*dynamic.UDPService), - }, - } -} - -func createAgentClient(tlsCfg *TLS) (http.Client, error) { - var client http.Client - if tlsCfg.Insecure { - client.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - MinVersion: tls.VersionTLS13, - }, - } - - return client, nil - } - - caContent, err := tlsCfg.CA.Read() - if err != nil { - return client, fmt.Errorf("reading CA: %w", err) - } - - roots := x509.NewCertPool() - if ok := roots.AppendCertsFromPEM(caContent); !ok { - return client, errors.New("appending CA error") - } - - certContent, err := tlsCfg.Cert.Read() - if err != nil { - return client, fmt.Errorf("reading Cert: %w", err) - } - keyContent, err := tlsCfg.Key.Read() - if err != nil { - return client, fmt.Errorf("reading Key: %w", err) - } - - certificate, err := tls.X509KeyPair(certContent, keyContent) - if err != nil { - return client, fmt.Errorf("creating key pair: %w", err) - } - - // mTLS - client.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: roots, - Certificates: []tls.Certificate{certificate}, - ServerName: "agent.traefik", - ClientAuth: tls.RequireAndVerifyClientCert, - MinVersion: tls.VersionTLS13, - }, - } - - return client, nil -} diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index 9a9aec188..2759055ef 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -25,6 +25,7 @@ import ( "github.com/traefik/traefik/v2/pkg/log" "github.com/traefik/traefik/v2/pkg/provider" traefikv1alpha1 "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefikio/v1alpha1" + "github.com/traefik/traefik/v2/pkg/provider/kubernetes/k8s" "github.com/traefik/traefik/v2/pkg/safe" "github.com/traefik/traefik/v2/pkg/tls" "github.com/traefik/traefik/v2/pkg/types" @@ -56,7 +57,25 @@ type Provider struct { IngressClass string `description:"Value of kubernetes.io/ingress.class annotation to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true"` ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"` AllowEmptyServices bool `description:"Allow the creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"` - lastConfiguration safe.Safe + + lastConfiguration safe.Safe + + routerTransform k8s.RouterTransform +} + +func (p *Provider) SetRouterTransform(routerTransform k8s.RouterTransform) { + p.routerTransform = routerTransform +} + +func (p *Provider) applyRouterTransform(ctx context.Context, rt *dynamic.Router, ingress *traefikv1alpha1.IngressRoute) { + if p.routerTransform == nil { + return + } + + err := p.routerTransform.Apply(ctx, rt, ingress.Annotations) + if err != nil { + log.FromContext(ctx).WithError(err).Error("Apply router transform") + } } func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) { diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index 9425915c8..d3b206301 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -148,6 +148,8 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli } } + p.applyRouterTransform(ctx, r, ingressRoute) + conf.Routers[normalized] = r } } diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go index a74104957..a9c201574 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes.go +++ b/pkg/provider/kubernetes/gateway/kubernetes.go @@ -22,6 +22,7 @@ import ( "github.com/traefik/traefik/v2/pkg/provider" containousv1alpha1 "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefikcontainous/v1alpha1" traefikv1alpha1 "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefikio/v1alpha1" + "github.com/traefik/traefik/v2/pkg/provider/kubernetes/k8s" "github.com/traefik/traefik/v2/pkg/safe" "github.com/traefik/traefik/v2/pkg/tls" corev1 "k8s.io/api/core/v1" @@ -53,6 +54,23 @@ type Provider struct { EntryPoints map[string]Entrypoint `json:"-" toml:"-" yaml:"-" label:"-" file:"-"` lastConfiguration safe.Safe + + routerTransform k8s.RouterTransform +} + +func (p *Provider) SetRouterTransform(routerTransform k8s.RouterTransform) { + p.routerTransform = routerTransform +} + +func (p *Provider) applyRouterTransform(ctx context.Context, rt *dynamic.Router, route *gatev1alpha2.HTTPRoute) { + if p.routerTransform == nil { + return + } + + err := p.routerTransform.Apply(ctx, rt, route.Annotations) + if err != nil { + log.FromContext(ctx).WithError(err).Error("Apply router transform") + } } // Entrypoint defines the available entry points. @@ -495,7 +513,7 @@ func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway * for _, routeKind := range routeKinds { switch routeKind.Kind { case kindHTTPRoute: - listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, gatewayHTTPRouteToHTTPConf(ctx, ep, listener, gateway, client, conf)...) + listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, p.gatewayHTTPRouteToHTTPConf(ctx, ep, listener, gateway, client, conf)...) case kindTCPRoute: listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, gatewayTCPRouteToTCPConf(ctx, ep, listener, gateway, client, conf)...) case kindTLSRoute: @@ -654,7 +672,7 @@ func getAllowedRouteKinds(listener gatev1alpha2.Listener, supportedKinds []gatev return routeKinds, conditions } -func gatewayHTTPRouteToHTTPConf(ctx context.Context, ep string, listener gatev1alpha2.Listener, gateway *gatev1alpha2.Gateway, client Client, conf *dynamic.Configuration) []metav1.Condition { +func (p *Provider) gatewayHTTPRouteToHTTPConf(ctx context.Context, ep string, listener gatev1alpha2.Listener, gateway *gatev1alpha2.Gateway, client Client, conf *dynamic.Configuration) []metav1.Condition { if listener.AllowedRoutes == nil { // Should not happen due to validation. return nil @@ -787,8 +805,11 @@ func gatewayHTTPRouteToHTTPConf(ctx context.Context, ep string, listener gatev1a router.Service = serviceName } + rt := &router + p.applyRouterTransform(ctx, rt, route) + routerKey = provider.Normalize(routerKey) - conf.HTTP.Routers[routerKey] = &router + conf.HTTP.Routers[routerKey] = rt } } diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index 31821bf33..c2beeb006 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -20,6 +20,7 @@ import ( "github.com/traefik/traefik/v2/pkg/job" "github.com/traefik/traefik/v2/pkg/log" "github.com/traefik/traefik/v2/pkg/provider" + "github.com/traefik/traefik/v2/pkg/provider/kubernetes/k8s" "github.com/traefik/traefik/v2/pkg/safe" "github.com/traefik/traefik/v2/pkg/tls" corev1 "k8s.io/api/core/v1" @@ -46,7 +47,25 @@ type Provider struct { ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"` AllowEmptyServices bool `description:"Allow creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"` AllowExternalNameServices bool `description:"Allow ExternalName services." json:"allowExternalNameServices,omitempty" toml:"allowExternalNameServices,omitempty" yaml:"allowExternalNameServices,omitempty" export:"true"` - lastConfiguration safe.Safe + + lastConfiguration safe.Safe + + routerTransform k8s.RouterTransform +} + +func (p *Provider) SetRouterTransform(routerTransform k8s.RouterTransform) { + p.routerTransform = routerTransform +} + +func (p *Provider) applyRouterTransform(ctx context.Context, rt *dynamic.Router, ingress *netv1.Ingress) { + if p.routerTransform == nil { + return + } + + err := p.routerTransform.Apply(ctx, rt, ingress.Annotations) + if err != nil { + log.FromContext(ctx).WithError(err).Error("Apply router transform") + } } // EndpointIngress holds the endpoint information for the Kubernetes provider. @@ -262,6 +281,8 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl rt.TLS = rtConfig.Router.TLS } + p.applyRouterTransform(ctx, rt, ingress) + conf.HTTP.Routers["default-router"] = rt conf.HTTP.Services["default-backend"] = service } @@ -304,8 +325,13 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl serviceName := provider.Normalize(ingress.Namespace + "-" + pa.Backend.Service.Name + "-" + portString) conf.HTTP.Services[serviceName] = service + rt := loadRouter(rule, pa, rtConfig, serviceName) + + p.applyRouterTransform(ctx, rt, ingress) + routerKey := strings.TrimPrefix(provider.Normalize(ingress.Namespace+"-"+ingress.Name+"-"+rule.Host+pa.Path), "-") - routers[routerKey] = append(routers[routerKey], loadRouter(rule, pa, rtConfig, serviceName)) + + routers[routerKey] = append(routers[routerKey], rt) } } diff --git a/pkg/provider/kubernetes/k8s/router_transform.go b/pkg/provider/kubernetes/k8s/router_transform.go new file mode 100644 index 000000000..b1e6040fd --- /dev/null +++ b/pkg/provider/kubernetes/k8s/router_transform.go @@ -0,0 +1,11 @@ +package k8s + +import ( + "context" + + "github.com/traefik/traefik/v2/pkg/config/dynamic" +) + +type RouterTransform interface { + Apply(ctx context.Context, rt *dynamic.Router, annotations map[string]string) error +} diff --git a/pkg/provider/marathon/marathon.go b/pkg/provider/marathon/marathon.go index aab469785..14c08f832 100644 --- a/pkg/provider/marathon/marathon.go +++ b/pkg/provider/marathon/marathon.go @@ -167,7 +167,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. case <-ctxPool.Done(): return case event := <-update: - logger.Debugf("Received provider event %s", event) + logger.Debugf("Received provider event %v", event) conf := p.getConfigurations(ctx) if conf != nil { From 021f37ff7111d42d879f549dff74cbc5bdb5acb7 Mon Sep 17 00:00:00 2001 From: Erikas <5955795+erkexzcx@users.noreply.github.com> Date: Tue, 16 May 2023 17:00:06 +0300 Subject: [PATCH 10/10] Do not check for wildcard domains for non DNS challenge --- pkg/provider/acme/provider.go | 10 ++-------- pkg/provider/acme/provider_test.go | 7 ------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/pkg/provider/acme/provider.go b/pkg/provider/acme/provider.go index be211ab04..b9027de98 100644 --- a/pkg/provider/acme/provider.go +++ b/pkg/provider/acme/provider.go @@ -922,14 +922,8 @@ func (p *Provider) sanitizeDomains(ctx context.Context, domain types.Domain) ([] var cleanDomains []string for _, dom := range domains { - if strings.HasPrefix(dom, "*") { - if p.DNSChallenge == nil { - return nil, fmt.Errorf("unable to generate a wildcard certificate in ACME provider for domain %q : ACME needs a DNSChallenge", strings.Join(domains, ",")) - } - - if strings.HasPrefix(dom, "*.*") { - return nil, fmt.Errorf("unable to generate a wildcard certificate in ACME provider for domain %q : ACME does not allow '*.*' wildcard domain", strings.Join(domains, ",")) - } + if strings.HasPrefix(dom, "*.*") { + return nil, fmt.Errorf("unable to generate a wildcard certificate in ACME provider for domain %q : ACME does not allow '*.*' wildcard domain", strings.Join(domains, ",")) } canonicalDomain := types.CanonicalDomain(dom) diff --git a/pkg/provider/acme/provider_test.go b/pkg/provider/acme/provider_test.go index 3268b1c92..3cd024c77 100644 --- a/pkg/provider/acme/provider_test.go +++ b/pkg/provider/acme/provider_test.go @@ -217,13 +217,6 @@ func TestProvider_sanitizeDomains(t *testing.T) { expectedErr: "no domain was given", expectedDomains: nil, }, - { - desc: "no DNSChallenge", - domains: types.Domain{Main: "*.traefik.wtf", SANs: []string{"foo.traefik.wtf"}}, - dnsChallenge: nil, - expectedErr: "unable to generate a wildcard certificate in ACME provider for domain \"*.traefik.wtf,foo.traefik.wtf\" : ACME needs a DNSChallenge", - expectedDomains: nil, - }, { desc: "unauthorized wildcard with SAN", domains: types.Domain{Main: "*.*.traefik.wtf", SANs: []string{"foo.traefik.wtf"}},