From 466d7461b76fc1a292591b450e4bfd3e0c96cc3f Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Wed, 10 May 2023 15:28:05 +0200 Subject: [PATCH] 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 @@ + + + + + +