From a6040c623bdb80cd53a28b5f9545c6a435f460d3 Mon Sep 17 00:00:00 2001 From: Traefiker Bot <30906710+traefiker@users.noreply.github.com> Date: Thu, 5 Mar 2020 12:46:05 +0100 Subject: [PATCH] Entry point redirection and default routers configuration Co-authored-by: Julien Salleyron Co-authored-by: Mathieu Lonjaret --- cmd/traefik/traefik.go | 20 +- docs/content/migration/v1-to-v2.md | 25 +- .../reference/static-configuration/cli-ref.md | 30 +++ .../reference/static-configuration/env-ref.md | 30 +++ .../reference/static-configuration/file.toml | 41 +++- .../reference/static-configuration/file.yaml | 28 ++- docs/content/routing/entrypoints.md | 208 +++++++++++++++++ integration/docker_compose_test.go | 2 +- integration/testdata/rawdata-consul.json | 3 + integration/testdata/rawdata-crd.json | 33 +-- integration/testdata/rawdata-etcd.json | 3 + integration/testdata/rawdata-ingress.json | 11 + integration/testdata/rawdata-redis.json | 3 + integration/testdata/rawdata-zk.json | 3 + pkg/api/testdata/entrypoint-bar.json | 1 + .../testdata/entrypoints-many-lastpage.json | 5 + pkg/api/testdata/entrypoints-page2.json | 1 + pkg/api/testdata/entrypoints.json | 2 + pkg/config/dynamic/http_config.go | 11 +- pkg/config/dynamic/zz_generated.deepcopy.go | 51 +++- pkg/config/runtime/runtime_http.go | 8 +- pkg/config/static/entrypoints.go | 33 +++ .../fixtures/api_insecure_with_dashboard.json | 3 +- .../api_insecure_without_dashboard.json | 3 +- .../fixtures/api_secure_with_dashboard.json | 3 +- .../api_secure_without_dashboard.json | 3 +- .../traefik/fixtures/full_configuration.json | 1 + .../fixtures/full_configuration_secure.json | 1 + pkg/provider/traefik/fixtures/models.json | 36 +++ .../traefik/fixtures/ping_custom.json | 1 + .../traefik/fixtures/ping_simple.json | 1 + .../traefik/fixtures/prometheus_custom.json | 1 + .../traefik/fixtures/prometheus_simple.json | 1 + .../traefik/fixtures/redirection.json | 30 +++ .../traefik/fixtures/rest_insecure.json | 1 + .../traefik/fixtures/rest_secure.json | 1 + pkg/provider/traefik/internal.go | 83 +++++++ pkg/provider/traefik/internal_test.go | 41 ++++ pkg/server/aggregator.go | 55 ++++- pkg/server/aggregator_test.go | 221 +++++++++++++++++- pkg/server/configurationwatcher.go | 13 +- pkg/server/configurationwatcher_test.go | 19 +- pkg/server/router/router_test.go | 34 +-- pkg/server/routerfactory_test.go | 28 +-- pkg/server/service/internalhandler.go | 5 + pkg/testhelpers/config.go | 5 +- 46 files changed, 1016 insertions(+), 126 deletions(-) create mode 100644 pkg/provider/traefik/fixtures/models.json create mode 100644 pkg/provider/traefik/fixtures/redirection.json diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index 0f5aab36f..a33c8575f 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -191,7 +191,25 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err managerFactory := service.NewManagerFactory(*staticConfiguration, routinesPool, metricsRegistry) routerFactory := server.NewRouterFactory(*staticConfiguration, managerFactory, tlsManager, chainBuilder) - watcher := server.NewConfigurationWatcher(routinesPool, providerAggregator, time.Duration(staticConfiguration.Providers.ProvidersThrottleDuration)) + var eps []string + for name, cfg := range staticConfiguration.EntryPoints { + protocol, err := cfg.GetProtocol() + if err != nil { + // Should never happen because Traefik should not start if protocol is invalid. + log.WithoutContext().Errorf("Invalid protocol: %v", err) + } + + if protocol != "udp" { + eps = append(eps, name) + } + } + + watcher := server.NewConfigurationWatcher( + routinesPool, + providerAggregator, + time.Duration(staticConfiguration.Providers.ProvidersThrottleDuration), + eps, + ) watcher.AddListener(func(conf dynamic.Configuration) { ctx := context.Background() diff --git a/docs/content/migration/v1-to-v2.md b/docs/content/migration/v1-to-v2.md index 628e9daad..c09f9e243 100644 --- a/docs/content/migration/v1-to-v2.md +++ b/docs/content/migration/v1-to-v2.md @@ -387,10 +387,8 @@ To apply a redirection, one of the redirect middlewares, [RedirectRegex](../midd - match: HostRegexp(`{any:.+}`) kind: Rule services: - # any service in the namespace - # the service will be never called - - name: noop - port: 80 + # the noop service will be never called + - name: noop@internal middlewares: - name: https_redirect # if the Middleware has distinct namespace @@ -431,13 +429,8 @@ To apply a redirection, one of the redirect middlewares, [RedirectRegex](../midd entryPoints = ["web"] middlewares = ["https_redirect"] rule = "HostRegexp(`{any:.+}`)" - service = "noop" - - [http.services] - # noop service, the URL will be never called - [http.services.noop.loadBalancer] - [[http.services.noop.loadBalancer.servers]] - url = "http://192.168.0.1:1337" + # the noop service will be never called + service = "noop@internal" [http.middlewares] [http.middlewares.https_redirect.redirectScheme] @@ -472,14 +465,8 @@ To apply a redirection, one of the redirect middlewares, [RedirectRegex](../midd middlewares: - https_redirect rule: "HostRegexp(`{any:.+}`)" - service: noop - - services: - # noop service, the URL will be never called - noop: - loadBalancer: - servers: - - url: http://192.168.0.1:1337 + # the noop service will be never called + service: noop@internal middlewares: https_redirect: diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index d7f9ba89f..18d816ee0 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -99,6 +99,36 @@ Trust all forwarded headers. (Default: ```false```) `--entrypoints..forwardedheaders.trustedips`: Trust only forwarded headers from selected IPs. +`--entrypoints..http`: +HTTP configuration. + +`--entrypoints..http.middlewares`: +Default middlewares for the routers linked to the entry point. + +`--entrypoints..http.redirections.entrypoint.scheme`: +Scheme used for the redirection. Defaults to https. (Default: ```https```) + +`--entrypoints..http.redirections.entrypoint.to`: +Targeted entry point of the redirection. + +`--entrypoints..http.tls`: +Default TLS configuration for the routers linked to the entry point. (Default: ```false```) + +`--entrypoints..http.tls.certresolver`: +Default certificate resolver for the routers linked to the entry point. + +`--entrypoints..http.tls.domains`: +Default TLS domains for the routers linked to the entry point. + +`--entrypoints..http.tls.domains[n].main`: +Default subject name. + +`--entrypoints..http.tls.domains[n].sans`: +Subject alternative names. + +`--entrypoints..http.tls.options`: +Default TLS options for the routers linked to the entry point. + `--entrypoints..proxyprotocol`: Proxy-Protocol configuration. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 43b113bab..f0fd597ea 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -99,6 +99,36 @@ Trust all forwarded headers. (Default: ```false```) `TRAEFIK_ENTRYPOINTS__FORWARDEDHEADERS_TRUSTEDIPS`: Trust only forwarded headers from selected IPs. +`TRAEFIK_ENTRYPOINTS__HTTP`: +HTTP configuration. + +`TRAEFIK_ENTRYPOINTS__HTTP_MIDDLEWARES`: +Default middlewares for the routers linked to the entry point. + +`TRAEFIK_ENTRYPOINTS__HTTP_REDIRECTIONS_ENTRYPOINT_SCHEME`: +Scheme used for the redirection. Defaults to https. (Default: ```https```) + +`TRAEFIK_ENTRYPOINTS__HTTP_REDIRECTIONS_ENTRYPOINT_TO`: +Targeted entry point of the redirection. + +`TRAEFIK_ENTRYPOINTS__HTTP_TLS`: +Default TLS configuration for the routers linked to the entry point. (Default: ```false```) + +`TRAEFIK_ENTRYPOINTS__HTTP_TLS_CERTRESOLVER`: +Default certificate resolver for the routers linked to the entry point. + +`TRAEFIK_ENTRYPOINTS__HTTP_TLS_DOMAINS`: +Default TLS domains for the routers linked to the entry point. + +`TRAEFIK_ENTRYPOINTS__HTTP_TLS_DOMAINS[n]_MAIN`: +Default subject name. + +`TRAEFIK_ENTRYPOINTS__HTTP_TLS_DOMAINS[n]_SANS`: +Subject alternative names. + +`TRAEFIK_ENTRYPOINTS__HTTP_TLS_OPTIONS`: +Default TLS options for the routers linked to the entry point. + `TRAEFIK_ENTRYPOINTS__PROXYPROTOCOL`: Proxy-Protocol configuration. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index e7487d5b9..43ef4f20d 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -28,6 +28,23 @@ [entryPoints.EntryPoint0.forwardedHeaders] insecure = true trustedIPs = ["foobar", "foobar"] + [entryPoints.EntryPoint0.http] + middlewares = ["foobar", "foobar"] + [entryPoints.EntryPoint0.http.redirections] + [entryPoints.EntryPoint0.http.redirections.entryPoint] + to = "foobar" + scheme = "foobar" + [entryPoints.EntryPoint0.http.tls] + options = "foobar" + certResolver = "foobar" + + [[entryPoints.EntryPoint0.http.tls.domains]] + main = "foobar" + sans = ["foobar", "foobar"] + + [[entryPoints.EntryPoint0.http.tls.domains]] + main = "foobar" + sans = ["foobar", "foobar"] [providers] providersThrottleDuration = 42 @@ -133,10 +150,10 @@ username = "foobar" password = "foobar" [providers.consul] - rootKey = "traefik" + rootKey = "traefik" endpoints = ["foobar", "foobar"] - username = "foobar" - password = "foobar" + username = "foobar" + password = "foobar" [providers.consul.tls] ca = "foobar" caOptional = true @@ -144,10 +161,10 @@ key = "foobar" insecureSkipVerify = true [providers.etcd] - rootKey = "traefik" + rootKey = "traefik" endpoints = ["foobar", "foobar"] - username = "foobar" - password = "foobar" + username = "foobar" + password = "foobar" [providers.etcd.tls] ca = "foobar" caOptional = true @@ -155,10 +172,10 @@ key = "foobar" insecureSkipVerify = true [providers.zooKeeper] - rootKey = "traefik" + rootKey = "traefik" endpoints = ["foobar", "foobar"] - username = "foobar" - password = "foobar" + username = "foobar" + password = "foobar" [providers.zooKeeper.tls] ca = "foobar" caOptional = true @@ -166,10 +183,10 @@ key = "foobar" insecureSkipVerify = true [providers.redis] - rootKey = "traefik" + rootKey = "traefik" endpoints = ["foobar", "foobar"] - username = "foobar" - password = "foobar" + username = "foobar" + password = "foobar" [providers.redis.tls] ca = "foobar" caOptional = true diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index 0548b7909..2dcdb8ce5 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -32,6 +32,26 @@ entryPoints: trustedIPs: - foobar - foobar + http: + redirections: + entryPoint: + to: foobar + scheme: foobar + middlewares: + - foobar + - foobar + tls: + options: foobar + certResolver: foobar + domains: + - main: foobar + sans: + - foobar + - foobar + - main: foobar + sans: + - foobar + - foobar providers: providersThrottleDuration: 42 docker: @@ -142,8 +162,8 @@ providers: consul: rootKey: traefik endpoints: - - foobar - - foobar + - foobar + - foobar username: foobar password: foobar tls: @@ -155,8 +175,8 @@ providers: etcd: rootKey: traefik endpoints: - - foobar - - foobar + - foobar + - foobar username: foobar password: foobar tls: diff --git a/docs/content/routing/entrypoints.md b/docs/content/routing/entrypoints.md index 7eeef3c80..c55c16120 100644 --- a/docs/content/routing/entrypoints.md +++ b/docs/content/routing/entrypoints.md @@ -529,3 +529,211 @@ If the Proxy Protocol header is passed, then the version is determined automatic When queuing Traefik behind another load-balancer, make sure to configure Proxy Protocol on both sides. Not doing so could introduce a security risk in your system (enabling request forgery). + +## HTTP Options + +This whole section is dedicated to options, keyed by entry point, that will apply only to HTTP routing. + +### Redirection + +??? example "HTTPS redirection (80 to 443)" + + ```toml tab="File (TOML)" + [entryPoints.web] + address = ":80" + + [entryPoints.web.http] + [entryPoints.web.http.redirections] + [entryPoints.web.http.redirections.entryPoint] + to = "websecure" + scheme = "https" + + [entryPoints.websecure] + address = ":443" + ``` + + ```yaml tab="File (YAML)" + entryPoints: + web: + address: :80 + http: + redirections: + entryPoint: + to: websecure + https: true + + websecure: + address: :443 + ``` + + ```bash tab="CLI" + --entrypoints.web.address=:80 + --entrypoints.web.http.redirections.entryPoint.to=websecure + --entrypoints.web.http.redirections.entryPoint.https=true + --entrypoints.websecure.address=:443 + ``` + +#### `entryPoint` + +This section is a convenience to enable (permanent) redirecting of all incoming requests on an entry point (e.g. port `80`) to another entry point (e.g. port `443`). + +??? info "`entryPoint.to`" + + _Required_ + + The target entry point. + + ```toml tab="File (TOML)" + [entryPoints.foo] + # ... + [entryPoints.foo.http.redirections] + [entryPoints.foo.http.redirections.entryPoint] + to = "bar" + ``` + + ```yaml tab="File (YAML)" + entryPoints: + foo: + # ... + http: + redirections: + entryPoint: + to: bar + ``` + + ```bash tab="CLI" + --entrypoints.foo.http.redirections.entryPoint.to=websecure + ``` + +??? info "`entryPoint.scheme`" + + _Optional, Default="http"_ + + The redirection target scheme. + + ```toml tab="File (TOML)" + [entryPoints.foo] + # ... + [entryPoints.foo.http.redirections] + [entryPoints.foo.http.redirections.entryPoint] + # ... + scheme = "https" + ``` + + ```yaml tab="File (YAML)" + entryPoints: + foo: + # ... + http: + redirections: + entryPoint: + # ... + scheme: https + ``` + + ```bash tab="CLI" + --entrypoints.foo.http.redirections.entryPoint.scheme=https + ``` + +### Middlewares + +The list of middlewares that are prepended by default to the list of middlewares of each router associated to the named entry point. + +```toml tab="File (TOML)" +[entryPoints.websecure] + address = ":443" + + [entryPoints.websecure.http] + middlewares = ["auth@file", "strip@file"] +``` + +```yaml tab="File (YAML)" +entryPoints: + websecure: + address: ':443' + http: + middlewares: + - auth@file + - strip@file +``` + +```bash tab="CLI" +entrypoints.websecure.address=:443 +entrypoints.websecure.http.middlewares=auth@file,strip@file +``` + +### TLS + +This section is about the default TLS configuration applied to all routers associated with the named entry point. + +If a TLS section (i.e. any of its fields) is user-defined, then the default configuration does not apply at all. + +The TLS section is the same as the [TLS section on HTTP routers](./routers/index.md#tls). + +```toml tab="File (TOML)" +[entryPoints.websecure] + address = ":443" + + [entryPoints.websecure.http.tls] + options = "foobar" + certResolver = "leresolver" + [[entryPoints.websecure.http.tls.domains]] + main = "example.com" + sans = ["foo.example.com", "bar.example.com"] + [[entryPoints.websecure.http.tls.domains]] + main = "test.com" + sans = ["foo.test.com", "bar.test.com"] +``` + +```yaml tab="File (YAML)" +entryPoints: + websecure: + address: ':443' + http: + tls: + options: foobar + certResolver: leresolver + domains: + - main: example.com + sans: + - foo.example.com + - bar.example.com + - main: test.com + sans: + - foo.test.com + - bar.test.com +``` + +```bash tab="CLI" +entrypoints.websecure.address=:443 +entrypoints.websecure.http.tls.options=foobar +entrypoints.websecure.http.tls.certResolver=leresolver +entrypoints.websecure.http.tls.domains[0].main=example.com +entrypoints.websecure.http.tls.domains[0].sans=foo.example.com,bar.example.com +entrypoints.websecure.http.tls.domains[1].main=test.com +entrypoints.websecure.http.tls.domains[1].sans=foo.test.com,bar.test.com +``` + +??? example "Let's Encrypt" + + ```toml tab="File (TOML)" + [entryPoints.websecure] + address = ":443" + + [entryPoints.websecure.http.tls] + certResolver = "leresolver" + ``` + + ```yaml tab="File (YAML)" + entryPoints: + websecure: + address: ':443' + http: + tls: + certResolver: leresolver + ``` + + ```bash tab="CLI" + entrypoints.websecure.address=:443 + entrypoints.websecure.http.tls.certResolver=leresolver + ``` diff --git a/integration/docker_compose_test.go b/integration/docker_compose_test.go index bcf84295c..5c5e1fc5a 100644 --- a/integration/docker_compose_test.go +++ b/integration/docker_compose_test.go @@ -76,7 +76,7 @@ func (s *DockerComposeSuite) TestComposeScale(c *check.C) { // check that we have only one service (not counting the internal ones) with n servers services := rtconf.Services - c.Assert(services, checker.HasLen, 3) + c.Assert(services, checker.HasLen, 4) for name, service := range services { if strings.HasSuffix(name, "@internal") { continue diff --git a/integration/testdata/rawdata-consul.json b/integration/testdata/rawdata-consul.json index 675226673..4f8e547bb 100644 --- a/integration/testdata/rawdata-consul.json +++ b/integration/testdata/rawdata-consul.json @@ -165,6 +165,9 @@ }, "status": "enabled" }, + "noop@internal": { + "status": "enabled" + }, "simplesvc@consul": { "loadBalancer": { "servers": [ diff --git a/integration/testdata/rawdata-crd.json b/integration/testdata/rawdata-crd.json index dcbfd135d..d4ad9550d 100644 --- a/integration/testdata/rawdata-crd.json +++ b/integration/testdata/rawdata-crd.json @@ -98,10 +98,10 @@ "loadBalancer": { "servers": [ { - "url": "http://10.42.0.3:80" + "url": "http://10.42.0.2:80" }, { - "url": "http://10.42.0.5:80" + "url": "http://10.42.0.3:80" } ], "passHostHeader": true @@ -111,18 +111,18 @@ "default-test-route-6b204d94623b3df4370c@kubernetescrd" ], "serverStatus": { - "http://10.42.0.3:80": "UP", - "http://10.42.0.5:80": "UP" + "http://10.42.0.2:80": "UP", + "http://10.42.0.3:80": "UP" } }, "default-test2-route-23c7f4c450289ee29016@kubernetescrd": { "loadBalancer": { "servers": [ { - "url": "http://10.42.0.3:80" + "url": "http://10.42.0.2:80" }, { - "url": "http://10.42.0.5:80" + "url": "http://10.42.0.3:80" } ], "passHostHeader": true @@ -132,26 +132,26 @@ "default-test2-route-23c7f4c450289ee29016@kubernetescrd" ], "serverStatus": { - "http://10.42.0.3:80": "UP", - "http://10.42.0.5:80": "UP" + "http://10.42.0.2:80": "UP", + "http://10.42.0.3:80": "UP" } }, "default-whoami-80@kubernetescrd": { "loadBalancer": { "servers": [ { - "url": "http://10.42.0.3:80" + "url": "http://10.42.0.2:80" }, { - "url": "http://10.42.0.5:80" + "url": "http://10.42.0.3:80" } ], "passHostHeader": true }, "status": "enabled", "serverStatus": { - "http://10.42.0.3:80": "UP", - "http://10.42.0.5:80": "UP" + "http://10.42.0.2:80": "UP", + "http://10.42.0.3:80": "UP" } }, "default-wrr1@kubernetescrd": { @@ -171,6 +171,9 @@ "usedBy": [ "default-test3-route-7d0ac22d3d8db4b82618@kubernetescrd" ] + }, + "noop@internal": { + "status": "enabled" } }, "tcpRouters": { @@ -199,7 +202,7 @@ "address": "10.42.0.4:8080" }, { - "address": "10.42.0.6:8080" + "address": "10.42.0.8:8080" } ] }, @@ -226,10 +229,10 @@ "loadBalancer": { "servers": [ { - "address": "10.42.0.4:8090" + "address": "10.42.0.10:8090" }, { - "address": "10.42.0.6:8090" + "address": "10.42.0.9:8090" } ] }, diff --git a/integration/testdata/rawdata-etcd.json b/integration/testdata/rawdata-etcd.json index bee6543af..2cae91a68 100644 --- a/integration/testdata/rawdata-etcd.json +++ b/integration/testdata/rawdata-etcd.json @@ -165,6 +165,9 @@ }, "status": "enabled" }, + "noop@internal": { + "status": "enabled" + }, "simplesvc@etcd": { "loadBalancer": { "servers": [ diff --git a/integration/testdata/rawdata-ingress.json b/integration/testdata/rawdata-ingress.json index d0f67fa18..ddb569ce0 100644 --- a/integration/testdata/rawdata-ingress.json +++ b/integration/testdata/rawdata-ingress.json @@ -29,6 +29,10 @@ ] }, "test-ingress-default-whoami-test-whoami@kubernetes": { + "entryPoints": [ + "web", + "traefik" + ], "service": "default-whoami-http", "rule": "Host(`whoami.test`) \u0026\u0026 PathPrefix(`/whoami`)", "status": "enabled", @@ -38,6 +42,10 @@ ] }, "test-ingress-https-default-whoami-test-https-whoami@kubernetes": { + "entryPoints": [ + "web", + "traefik" + ], "service": "default-whoami-http", "rule": "Host(`whoami.test.https`) \u0026\u0026 PathPrefix(`/whoami`)", "tls": {}, @@ -107,6 +115,9 @@ "http://10.42.0.3:80": "UP", "http://10.42.0.5:80": "UP" } + }, + "noop@internal": { + "status": "enabled" } } } \ No newline at end of file diff --git a/integration/testdata/rawdata-redis.json b/integration/testdata/rawdata-redis.json index 7f42b22b7..46bf3bcb6 100644 --- a/integration/testdata/rawdata-redis.json +++ b/integration/testdata/rawdata-redis.json @@ -165,6 +165,9 @@ }, "status": "enabled" }, + "noop@internal": { + "status": "enabled" + }, "simplesvc@redis": { "loadBalancer": { "servers": [ diff --git a/integration/testdata/rawdata-zk.json b/integration/testdata/rawdata-zk.json index a8f0d9a76..dd84a60fd 100644 --- a/integration/testdata/rawdata-zk.json +++ b/integration/testdata/rawdata-zk.json @@ -165,6 +165,9 @@ }, "status": "enabled" }, + "noop@internal": { + "status": "enabled" + }, "simplesvc@zookeeper": { "loadBalancer": { "servers": [ diff --git a/pkg/api/testdata/entrypoint-bar.json b/pkg/api/testdata/entrypoint-bar.json index 31177544f..897b16e00 100644 --- a/pkg/api/testdata/entrypoint-bar.json +++ b/pkg/api/testdata/entrypoint-bar.json @@ -1,4 +1,5 @@ { "address": ":81", + "http": {}, "name": "bar" } \ No newline at end of file diff --git a/pkg/api/testdata/entrypoints-many-lastpage.json b/pkg/api/testdata/entrypoints-many-lastpage.json index 588b0e2e3..3e0f438e5 100644 --- a/pkg/api/testdata/entrypoints-many-lastpage.json +++ b/pkg/api/testdata/entrypoints-many-lastpage.json @@ -1,22 +1,27 @@ [ { "address": ":14", + "http": {}, "name": "ep14" }, { "address": ":15", + "http": {}, "name": "ep15" }, { "address": ":16", + "http": {}, "name": "ep16" }, { "address": ":17", + "http": {}, "name": "ep17" }, { "address": ":18", + "http": {}, "name": "ep18" } ] \ No newline at end of file diff --git a/pkg/api/testdata/entrypoints-page2.json b/pkg/api/testdata/entrypoints-page2.json index 142f5b9e7..2d674dc6d 100644 --- a/pkg/api/testdata/entrypoints-page2.json +++ b/pkg/api/testdata/entrypoints-page2.json @@ -1,6 +1,7 @@ [ { "address": ":82", + "http": {}, "name": "web2" } ] \ No newline at end of file diff --git a/pkg/api/testdata/entrypoints.json b/pkg/api/testdata/entrypoints.json index 3f0e5cbef..15c46b787 100644 --- a/pkg/api/testdata/entrypoints.json +++ b/pkg/api/testdata/entrypoints.json @@ -8,6 +8,7 @@ "192.168.1.4" ] }, + "http": {}, "name": "web", "proxyProtocol": { "insecure": true, @@ -37,6 +38,7 @@ "192.168.1.40" ] }, + "http": {}, "name": "websecure", "proxyProtocol": { "insecure": true, diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index 2f743ba55..ad4f3dcc1 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -11,8 +11,17 @@ import ( // HTTPConfiguration contains all the HTTP configuration parameters. type HTTPConfiguration struct { Routers map[string]*Router `json:"routers,omitempty" toml:"routers,omitempty" yaml:"routers,omitempty"` - Middlewares map[string]*Middleware `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty"` Services map[string]*Service `json:"services,omitempty" toml:"services,omitempty" yaml:"services,omitempty"` + Middlewares map[string]*Middleware `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty"` + Models map[string]*Model `json:"models,omitempty" toml:"models,omitempty" yaml:"models,omitempty"` +} + +// +k8s:deepcopy-gen=true + +// Model is a set of default router's values. +type Model struct { + Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty"` + TLS *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty"` } // +k8s:deepcopy-gen=true diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index fbac9a640..21892bcc3 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -375,6 +375,21 @@ func (in *HTTPConfiguration) DeepCopyInto(out *HTTPConfiguration) { (*out)[key] = outVal } } + if in.Services != nil { + in, out := &in.Services, &out.Services + *out = make(map[string]*Service, len(*in)) + for key, val := range *in { + var outVal *Service + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = new(Service) + (*in).DeepCopyInto(*out) + } + (*out)[key] = outVal + } + } if in.Middlewares != nil { in, out := &in.Middlewares, &out.Middlewares *out = make(map[string]*Middleware, len(*in)) @@ -390,16 +405,16 @@ func (in *HTTPConfiguration) DeepCopyInto(out *HTTPConfiguration) { (*out)[key] = outVal } } - if in.Services != nil { - in, out := &in.Services, &out.Services - *out = make(map[string]*Service, len(*in)) + if in.Models != nil { + in, out := &in.Models, &out.Models + *out = make(map[string]*Model, len(*in)) for key, val := range *in { - var outVal *Service + var outVal *Model if val == nil { (*out)[key] = nil } else { in, out := &val, &outVal - *out = new(Service) + *out = new(Model) (*in).DeepCopyInto(*out) } (*out)[key] = outVal @@ -760,6 +775,32 @@ func (in *Mirroring) DeepCopy() *Mirroring { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Model) DeepCopyInto(out *Model) { + *out = *in + if in.Middlewares != nil { + in, out := &in.Middlewares, &out.Middlewares + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(RouterTLSConfig) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Model. +func (in *Model) DeepCopy() *Model { + if in == nil { + return nil + } + out := new(Model) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PassTLSClientCert) DeepCopyInto(out *PassTLSClientCert) { *out = *in diff --git a/pkg/config/runtime/runtime_http.go b/pkg/config/runtime/runtime_http.go index af5062fd9..d93d6e844 100644 --- a/pkg/config/runtime/runtime_http.go +++ b/pkg/config/runtime/runtime_http.go @@ -21,14 +21,8 @@ func (c *Configuration) GetRoutersByEntryPoints(ctx context.Context, entryPoints logger := log.FromContext(log.With(ctx, log.Str(log.RouterName, rtName))) - eps := rt.EntryPoints - if len(eps) == 0 { - logger.Debugf("No entryPoint defined for this router, using the default one(s) instead: %+v", entryPoints) - eps = entryPoints - } - entryPointsCount := 0 - for _, entryPointName := range eps { + for _, entryPointName := range rt.EntryPoints { if !contains(entryPoints, entryPointName) { rt.AddError(fmt.Errorf("entryPoint %q doesn't exist", entryPointName), false) logger.WithField(log.EntryPointName, entryPointName). diff --git a/pkg/config/static/entrypoints.go b/pkg/config/static/entrypoints.go index bbfd22e21..6c12936ab 100644 --- a/pkg/config/static/entrypoints.go +++ b/pkg/config/static/entrypoints.go @@ -3,6 +3,8 @@ package static import ( "fmt" "strings" + + "github.com/containous/traefik/v2/pkg/types" ) // EntryPoint holds the entry point configuration. @@ -11,6 +13,7 @@ type EntryPoint struct { Transport *EntryPointsTransport `description:"Configures communication between clients and Traefik." json:"transport,omitempty" toml:"transport,omitempty" yaml:"transport,omitempty"` ProxyProtocol *ProxyProtocol `description:"Proxy-Protocol configuration." json:"proxyProtocol,omitempty" toml:"proxyProtocol,omitempty" yaml:"proxyProtocol,omitempty" label:"allowEmpty"` ForwardedHeaders *ForwardedHeaders `description:"Trust client forwarding headers." json:"forwardedHeaders,omitempty" toml:"forwardedHeaders,omitempty" yaml:"forwardedHeaders,omitempty"` + HTTP HTTPConfig `description:"HTTP configuration." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty"` } // GetAddress strips any potential protocol part of the address field of the @@ -43,6 +46,36 @@ func (ep *EntryPoint) SetDefaults() { ep.ForwardedHeaders = &ForwardedHeaders{} } +// HTTPConfig is the HTTP configuration of an entry point. +type HTTPConfig struct { + Redirections *Redirections `description:"Set of redirection" json:"redirections,omitempty" toml:"redirections,omitempty" yaml:"redirections,omitempty"` + Middlewares []string `description:"Default middlewares for the routers linked to the entry point." json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty"` + TLS *TLSConfig `description:"Default TLS configuration for the routers linked to the entry point." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty"` +} + +// Redirections is a set of redirection for an entry point. +type Redirections struct { + EntryPoint *RedirectEntryPoint `description:"Set of redirection for an entry point." json:"entryPoint,omitempty" toml:"entryPoint,omitempty" yaml:"entryPoint,omitempty"` +} + +// RedirectEntryPoint is the definition of an entry point redirection. +type RedirectEntryPoint struct { + To string `description:"Targeted entry point of the redirection." json:"to,omitempty" toml:"to,omitempty" yaml:"to,omitempty"` + Scheme string `description:"Scheme used for the redirection. Defaults to https." json:"https,omitempty" toml:"https,omitempty" yaml:"https,omitempty"` +} + +// SetDefaults sets the default values. +func (r *RedirectEntryPoint) SetDefaults() { + r.Scheme = "https" +} + +// TLSConfig is the default TLS configuration for all the routers associated to the concerned entry point. +type TLSConfig struct { + Options string `description:"Default TLS options for the routers linked to the entry point." json:"options,omitempty" toml:"options,omitempty" yaml:"options,omitempty"` + CertResolver string `description:"Default certificate resolver for the routers linked to the entry point." json:"certResolver,omitempty" toml:"certResolver,omitempty" yaml:"certResolver,omitempty"` + Domains []types.Domain `description:"Default TLS domains for the routers linked to the entry point." json:"domains,omitempty" toml:"domains,omitempty" yaml:"domains,omitempty"` +} + // ForwardedHeaders Trust client forwarding headers. type ForwardedHeaders struct { Insecure bool `description:"Trust all forwarded headers." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"` diff --git a/pkg/provider/traefik/fixtures/api_insecure_with_dashboard.json b/pkg/provider/traefik/fixtures/api_insecure_with_dashboard.json index e4a1e2ee4..c4a306889 100644 --- a/pkg/provider/traefik/fixtures/api_insecure_with_dashboard.json +++ b/pkg/provider/traefik/fixtures/api_insecure_with_dashboard.json @@ -41,7 +41,8 @@ }, "services": { "api": {}, - "dashboard": {} + "dashboard": {}, + "noop": {} } }, "tcp": {}, diff --git a/pkg/provider/traefik/fixtures/api_insecure_without_dashboard.json b/pkg/provider/traefik/fixtures/api_insecure_without_dashboard.json index 11c035942..994fcf3af 100644 --- a/pkg/provider/traefik/fixtures/api_insecure_without_dashboard.json +++ b/pkg/provider/traefik/fixtures/api_insecure_without_dashboard.json @@ -11,7 +11,8 @@ } }, "services": { - "api": {} + "api": {}, + "noop": {} } }, "tcp": {}, diff --git a/pkg/provider/traefik/fixtures/api_secure_with_dashboard.json b/pkg/provider/traefik/fixtures/api_secure_with_dashboard.json index 4cafeae55..3e146bd46 100644 --- a/pkg/provider/traefik/fixtures/api_secure_with_dashboard.json +++ b/pkg/provider/traefik/fixtures/api_secure_with_dashboard.json @@ -2,7 +2,8 @@ "http": { "services": { "api": {}, - "dashboard": {} + "dashboard": {}, + "noop": {} } }, "tcp": {}, diff --git a/pkg/provider/traefik/fixtures/api_secure_without_dashboard.json b/pkg/provider/traefik/fixtures/api_secure_without_dashboard.json index ec900d1b3..d5f990413 100644 --- a/pkg/provider/traefik/fixtures/api_secure_without_dashboard.json +++ b/pkg/provider/traefik/fixtures/api_secure_without_dashboard.json @@ -1,7 +1,8 @@ { "http": { "services": { - "api": {} + "api": {}, + "noop": {} } }, "tcp": {}, diff --git a/pkg/provider/traefik/fixtures/full_configuration.json b/pkg/provider/traefik/fixtures/full_configuration.json index f4852fcb5..bd08c9f0b 100644 --- a/pkg/provider/traefik/fixtures/full_configuration.json +++ b/pkg/provider/traefik/fixtures/full_configuration.json @@ -74,6 +74,7 @@ "services": { "api": {}, "dashboard": {}, + "noop": {}, "ping": {}, "prometheus": {}, "rest": {} diff --git a/pkg/provider/traefik/fixtures/full_configuration_secure.json b/pkg/provider/traefik/fixtures/full_configuration_secure.json index 99e3f3429..fbc780ab8 100644 --- a/pkg/provider/traefik/fixtures/full_configuration_secure.json +++ b/pkg/provider/traefik/fixtures/full_configuration_secure.json @@ -3,6 +3,7 @@ "services": { "api": {}, "dashboard": {}, + "noop": {}, "ping": {}, "prometheus": {}, "rest": {} diff --git a/pkg/provider/traefik/fixtures/models.json b/pkg/provider/traefik/fixtures/models.json new file mode 100644 index 000000000..005b6bf9a --- /dev/null +++ b/pkg/provider/traefik/fixtures/models.json @@ -0,0 +1,36 @@ +{ + "http": { + "services": { + "noop": {} + }, + "models": { + "websecure": { + "middlewares": [ + "test" + ], + "tls": { + "options": "opt", + "certResolver": "le", + "domains": [ + { + "main": "mainA", + "sans": [ + "sanA1", + "sanA2" + ] + }, + { + "main": "mainB", + "sans": [ + "sanB1", + "sanB2" + ] + } + ] + } + } + } + }, + "tcp": {}, + "tls": {} +} \ No newline at end of file diff --git a/pkg/provider/traefik/fixtures/ping_custom.json b/pkg/provider/traefik/fixtures/ping_custom.json index a378f761d..7be4a2bf4 100644 --- a/pkg/provider/traefik/fixtures/ping_custom.json +++ b/pkg/provider/traefik/fixtures/ping_custom.json @@ -1,6 +1,7 @@ { "http": { "services": { + "noop": {}, "ping": {} } }, diff --git a/pkg/provider/traefik/fixtures/ping_simple.json b/pkg/provider/traefik/fixtures/ping_simple.json index da48afb34..3ee3b4050 100644 --- a/pkg/provider/traefik/fixtures/ping_simple.json +++ b/pkg/provider/traefik/fixtures/ping_simple.json @@ -11,6 +11,7 @@ } }, "services": { + "noop": {}, "ping": {} } }, diff --git a/pkg/provider/traefik/fixtures/prometheus_custom.json b/pkg/provider/traefik/fixtures/prometheus_custom.json index 06a63857e..65275e850 100644 --- a/pkg/provider/traefik/fixtures/prometheus_custom.json +++ b/pkg/provider/traefik/fixtures/prometheus_custom.json @@ -1,6 +1,7 @@ { "http": { "services": { + "noop": {}, "prometheus": {} } }, diff --git a/pkg/provider/traefik/fixtures/prometheus_simple.json b/pkg/provider/traefik/fixtures/prometheus_simple.json index 9699120b1..3c856804f 100644 --- a/pkg/provider/traefik/fixtures/prometheus_simple.json +++ b/pkg/provider/traefik/fixtures/prometheus_simple.json @@ -11,6 +11,7 @@ } }, "services": { + "noop": {}, "prometheus": {} } }, diff --git a/pkg/provider/traefik/fixtures/redirection.json b/pkg/provider/traefik/fixtures/redirection.json new file mode 100644 index 000000000..4ffbb756c --- /dev/null +++ b/pkg/provider/traefik/fixtures/redirection.json @@ -0,0 +1,30 @@ +{ + "http": { + "routers": { + "web-to-websecure": { + "entryPoints": [ + "web" + ], + "middlewares": [ + "redirect-web-to-websecure" + ], + "service": "noop@internal", + "rule": "HostRegexp(`{host:.+}`)" + } + }, + "middlewares": { + "redirect-web-to-websecure": { + "redirectScheme": { + "scheme": "https", + "port": "443", + "permanent": true + } + } + }, + "services": { + "noop": {} + } + }, + "tcp": {}, + "tls": {} +} \ No newline at end of file diff --git a/pkg/provider/traefik/fixtures/rest_insecure.json b/pkg/provider/traefik/fixtures/rest_insecure.json index d37115cc3..7952359f3 100644 --- a/pkg/provider/traefik/fixtures/rest_insecure.json +++ b/pkg/provider/traefik/fixtures/rest_insecure.json @@ -11,6 +11,7 @@ } }, "services": { + "noop": {}, "rest": {} } }, diff --git a/pkg/provider/traefik/fixtures/rest_secure.json b/pkg/provider/traefik/fixtures/rest_secure.json index 1085b5719..8645a04e5 100644 --- a/pkg/provider/traefik/fixtures/rest_secure.json +++ b/pkg/provider/traefik/fixtures/rest_secure.json @@ -1,6 +1,7 @@ { "http": { "services": { + "noop": {}, "rest": {} } }, diff --git a/pkg/provider/traefik/internal.go b/pkg/provider/traefik/internal.go index 091d2b13f..4094ad449 100644 --- a/pkg/provider/traefik/internal.go +++ b/pkg/provider/traefik/internal.go @@ -1,10 +1,14 @@ package traefik import ( + "context" + "fmt" "math" + "net" "github.com/containous/traefik/v2/pkg/config/dynamic" "github.com/containous/traefik/v2/pkg/config/static" + "github.com/containous/traefik/v2/pkg/log" "github.com/containous/traefik/v2/pkg/provider" "github.com/containous/traefik/v2/pkg/safe" "github.com/containous/traefik/v2/pkg/tls" @@ -43,6 +47,7 @@ func (i *Provider) createConfiguration() *dynamic.Configuration { Routers: make(map[string]*dynamic.Router), Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), + Models: make(map[string]*dynamic.Model), }, TCP: &dynamic.TCPConfiguration{ Routers: make(map[string]*dynamic.TCPRouter), @@ -58,10 +63,73 @@ func (i *Provider) createConfiguration() *dynamic.Configuration { i.pingConfiguration(cfg) i.restConfiguration(cfg) i.prometheusConfiguration(cfg) + i.entryPointModels(cfg) + i.redirection(cfg) + + cfg.HTTP.Services["noop"] = &dynamic.Service{} return cfg } +func (i *Provider) redirection(cfg *dynamic.Configuration) { + for name, ep := range i.staticCfg.EntryPoints { + if ep.HTTP.Redirections == nil || ep.HTTP.Redirections.EntryPoint == nil { + continue + } + + def := ep.HTTP.Redirections + rtName := provider.Normalize(name + "-to-" + def.EntryPoint.To) + mdName := "redirect-" + rtName + + rt := &dynamic.Router{ + Rule: "HostRegexp(`{host:.+}`)", + EntryPoints: []string{name}, + Middlewares: []string{mdName}, + Service: "noop@internal", + } + + port, err := i.getEntryPointPort(name, def) + if err != nil { + log.FromContext(context.Background()).WithField(log.EntryPointName, name).Error(err) + continue + } + + cfg.HTTP.Routers[rtName] = rt + + rs := &dynamic.Middleware{ + RedirectScheme: &dynamic.RedirectScheme{ + Scheme: def.EntryPoint.Scheme, + Port: port, + Permanent: true, + }, + } + + cfg.HTTP.Middlewares[mdName] = rs + } +} + +func (i *Provider) entryPointModels(cfg *dynamic.Configuration) { + for name, ep := range i.staticCfg.EntryPoints { + if len(ep.HTTP.Middlewares) == 0 && ep.HTTP.TLS == nil { + continue + } + + m := &dynamic.Model{ + Middlewares: ep.HTTP.Middlewares, + } + + if ep.HTTP.TLS != nil { + m.TLS = &dynamic.RouterTLSConfig{ + Options: ep.HTTP.TLS.Options, + CertResolver: ep.HTTP.TLS.CertResolver, + Domains: ep.HTTP.TLS.Domains, + } + } + + cfg.HTTP.Models[name] = m + } +} + func (i *Provider) apiConfiguration(cfg *dynamic.Configuration) { if i.staticCfg.API == nil { return @@ -163,3 +231,18 @@ func (i *Provider) prometheusConfiguration(cfg *dynamic.Configuration) { cfg.HTTP.Services["prometheus"] = &dynamic.Service{} } + +func (i *Provider) getEntryPointPort(name string, def *static.Redirections) (string, error) { + dst, ok := i.staticCfg.EntryPoints[def.EntryPoint.To] + if !ok { + return "", fmt.Errorf("'to' entry point field references a non-existing entry point: %s", name) + } + + _, port, err := net.SplitHostPort(dst.Address) + if err != nil { + return "", fmt.Errorf("invalid entry point %q address %q: %v", + name, i.staticCfg.EntryPoints[def.EntryPoint.To].Address, err) + } + + return port, nil +} diff --git a/pkg/provider/traefik/internal_test.go b/pkg/provider/traefik/internal_test.go index 6e43a531f..943f0f5af 100644 --- a/pkg/provider/traefik/internal_test.go +++ b/pkg/provider/traefik/internal_test.go @@ -167,6 +167,47 @@ func Test_createConfiguration(t *testing.T) { }, }, }, + { + desc: "models.json", + staticCfg: static.Configuration{ + EntryPoints: map[string]*static.EntryPoint{ + "websecure": { + HTTP: static.HTTPConfig{ + Middlewares: []string{"test"}, + TLS: &static.TLSConfig{ + Options: "opt", + CertResolver: "le", + Domains: []types.Domain{ + {Main: "mainA", SANs: []string{"sanA1", "sanA2"}}, + {Main: "mainB", SANs: []string{"sanB1", "sanB2"}}, + }, + }, + }, + }, + }, + }, + }, + { + desc: "redirection.json", + staticCfg: static.Configuration{ + EntryPoints: map[string]*static.EntryPoint{ + "web": { + Address: ":80", + HTTP: static.HTTPConfig{ + Redirections: &static.Redirections{ + EntryPoint: &static.RedirectEntryPoint{ + To: "websecure", + Scheme: "https", + }, + }, + }, + }, + "websecure": { + Address: ":443", + }, + }, + }, + }, } for _, test := range testCases { diff --git a/pkg/server/aggregator.go b/pkg/server/aggregator.go index c2e00c5f9..a325f6e57 100644 --- a/pkg/server/aggregator.go +++ b/pkg/server/aggregator.go @@ -7,12 +7,13 @@ import ( "github.com/containous/traefik/v2/pkg/tls" ) -func mergeConfiguration(configurations dynamic.Configurations) dynamic.Configuration { +func mergeConfiguration(configurations dynamic.Configurations, entryPoints []string) dynamic.Configuration { conf := dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: make(map[string]*dynamic.Router), Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), + Models: make(map[string]*dynamic.Model), }, TCP: &dynamic.TCPConfiguration{ Routers: make(map[string]*dynamic.TCPRouter), @@ -33,6 +34,13 @@ func mergeConfiguration(configurations dynamic.Configurations) dynamic.Configura for pvd, configuration := range configurations { if configuration.HTTP != nil { for routerName, router := range configuration.HTTP.Routers { + if len(router.EntryPoints) == 0 { + log.WithoutContext(). + WithField(log.RouterName, routerName). + Debugf("No entryPoint defined for this router, using the default one(s) instead: %+v", entryPoints) + router.EntryPoints = entryPoints + } + conf.HTTP.Routers[provider.MakeQualifiedName(pvd, routerName)] = router } for middlewareName, middleware := range configuration.HTTP.Middlewares { @@ -41,6 +49,9 @@ func mergeConfiguration(configurations dynamic.Configurations) dynamic.Configura for serviceName, service := range configuration.HTTP.Services { conf.HTTP.Services[provider.MakeQualifiedName(pvd, serviceName)] = service } + for modelName, model := range configuration.HTTP.Models { + conf.HTTP.Models[provider.MakeQualifiedName(pvd, modelName)] = model + } } if configuration.TCP != nil { @@ -101,3 +112,45 @@ func mergeConfiguration(configurations dynamic.Configurations) dynamic.Configura return conf } + +func applyModel(cfg dynamic.Configuration) dynamic.Configuration { + if cfg.HTTP == nil || len(cfg.HTTP.Models) == 0 { + return cfg + } + + rts := make(map[string]*dynamic.Router) + + for name, router := range cfg.HTTP.Routers { + eps := router.EntryPoints + router.EntryPoints = nil + + for _, epName := range eps { + m, ok := cfg.HTTP.Models[epName+"@internal"] + if ok { + cp := router.DeepCopy() + + cp.EntryPoints = []string{epName} + + if cp.TLS == nil { + cp.TLS = m.TLS + } + + cp.Middlewares = append(m.Middlewares, cp.Middlewares...) + + rtName := name + if len(eps) > 1 { + rtName = epName + "-" + name + } + rts[rtName] = cp + } else { + router.EntryPoints = append(router.EntryPoints, epName) + + rts[name] = router + } + } + } + + cfg.HTTP.Routers = rts + + return cfg +} diff --git a/pkg/server/aggregator_test.go b/pkg/server/aggregator_test.go index 98c5b00f9..21d224916 100644 --- a/pkg/server/aggregator_test.go +++ b/pkg/server/aggregator_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestAggregator(t *testing.T) { +func Test_mergeConfiguration(t *testing.T) { testCases := []struct { desc string given dynamic.Configurations @@ -21,6 +21,7 @@ func TestAggregator(t *testing.T) { Routers: make(map[string]*dynamic.Router), Middlewares: make(map[string]*dynamic.Middleware), Services: make(map[string]*dynamic.Service), + Models: make(map[string]*dynamic.Model), }, }, { @@ -42,7 +43,9 @@ func TestAggregator(t *testing.T) { }, expected: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "router-1@provider-1": {}, + "router-1@provider-1": { + EntryPoints: []string{"defaultEP"}, + }, }, Middlewares: map[string]*dynamic.Middleware{ "middleware-1@provider-1": {}, @@ -50,6 +53,7 @@ func TestAggregator(t *testing.T) { Services: map[string]*dynamic.Service{ "service-1@provider-1": {}, }, + Models: make(map[string]*dynamic.Model), }, }, { @@ -84,8 +88,12 @@ func TestAggregator(t *testing.T) { }, expected: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "router-1@provider-1": {}, - "router-1@provider-2": {}, + "router-1@provider-1": { + EntryPoints: []string{"defaultEP"}, + }, + "router-1@provider-2": { + EntryPoints: []string{"defaultEP"}, + }, }, Middlewares: map[string]*dynamic.Middleware{ "middleware-1@provider-1": {}, @@ -95,6 +103,7 @@ func TestAggregator(t *testing.T) { "service-1@provider-1": {}, "service-1@provider-2": {}, }, + Models: make(map[string]*dynamic.Model), }, }, } @@ -104,13 +113,13 @@ func TestAggregator(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - actual := mergeConfiguration(test.given) + actual := mergeConfiguration(test.given, []string{"defaultEP"}) assert.Equal(t, test.expected, actual.HTTP) }) } } -func TestAggregator_tlsoptions(t *testing.T) { +func Test_mergeConfiguration_tlsOptions(t *testing.T) { testCases := []struct { desc string given dynamic.Configurations @@ -289,13 +298,13 @@ func TestAggregator_tlsoptions(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - actual := mergeConfiguration(test.given) + actual := mergeConfiguration(test.given, []string{"defaultEP"}) assert.Equal(t, test.expected, actual.TLS.Options) }) } } -func TestAggregator_tlsStore(t *testing.T) { +func Test_mergeConfiguration_tlsStore(t *testing.T) { testCases := []struct { desc string given dynamic.Configurations @@ -381,8 +390,202 @@ func TestAggregator_tlsStore(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - actual := mergeConfiguration(test.given) + actual := mergeConfiguration(test.given, []string{"defaultEP"}) assert.Equal(t, test.expected, actual.TLS.Stores) }) } } + +func Test_applyModel(t *testing.T) { + testCases := []struct { + desc string + input dynamic.Configuration + expected dynamic.Configuration + }{ + { + desc: "empty configuration", + input: dynamic.Configuration{}, + expected: dynamic.Configuration{}, + }, + { + desc: "without model", + input: dynamic.Configuration{ + HTTP: &dynamic.HTTPConfiguration{ + Routers: make(map[string]*dynamic.Router), + Middlewares: make(map[string]*dynamic.Middleware), + Services: make(map[string]*dynamic.Service), + Models: make(map[string]*dynamic.Model), + }, + }, + expected: dynamic.Configuration{ + HTTP: &dynamic.HTTPConfiguration{ + Routers: make(map[string]*dynamic.Router), + Middlewares: make(map[string]*dynamic.Middleware), + Services: make(map[string]*dynamic.Service), + Models: make(map[string]*dynamic.Model), + }, + }, + }, + { + desc: "with model, not used", + input: dynamic.Configuration{ + HTTP: &dynamic.HTTPConfiguration{ + Routers: make(map[string]*dynamic.Router), + Middlewares: make(map[string]*dynamic.Middleware), + Services: make(map[string]*dynamic.Service), + Models: map[string]*dynamic.Model{ + "ep@internal": { + Middlewares: []string{"test"}, + TLS: &dynamic.RouterTLSConfig{}, + }, + }, + }, + }, + expected: dynamic.Configuration{ + HTTP: &dynamic.HTTPConfiguration{ + Routers: make(map[string]*dynamic.Router), + Middlewares: make(map[string]*dynamic.Middleware), + Services: make(map[string]*dynamic.Service), + Models: map[string]*dynamic.Model{ + "ep@internal": { + Middlewares: []string{"test"}, + TLS: &dynamic.RouterTLSConfig{}, + }, + }, + }, + }, + }, + { + desc: "with model, one entry point", + input: dynamic.Configuration{ + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "test": { + EntryPoints: []string{"websecure"}, + }, + }, + Middlewares: make(map[string]*dynamic.Middleware), + Services: make(map[string]*dynamic.Service), + Models: map[string]*dynamic.Model{ + "websecure@internal": { + Middlewares: []string{"test"}, + TLS: &dynamic.RouterTLSConfig{}, + }, + }, + }, + }, + expected: dynamic.Configuration{ + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "test": { + EntryPoints: []string{"websecure"}, + Middlewares: []string{"test"}, + TLS: &dynamic.RouterTLSConfig{}, + }, + }, + Middlewares: make(map[string]*dynamic.Middleware), + Services: make(map[string]*dynamic.Service), + Models: map[string]*dynamic.Model{ + "websecure@internal": { + Middlewares: []string{"test"}, + TLS: &dynamic.RouterTLSConfig{}, + }, + }, + }, + }, + }, + { + desc: "with model, one entry point, and router with tls", + input: dynamic.Configuration{ + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "test": { + EntryPoints: []string{"websecure"}, + TLS: &dynamic.RouterTLSConfig{CertResolver: "router"}, + }, + }, + Middlewares: make(map[string]*dynamic.Middleware), + Services: make(map[string]*dynamic.Service), + Models: map[string]*dynamic.Model{ + "websecure@internal": { + Middlewares: []string{"test"}, + TLS: &dynamic.RouterTLSConfig{CertResolver: "ep"}, + }, + }, + }, + }, + expected: dynamic.Configuration{ + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "test": { + EntryPoints: []string{"websecure"}, + Middlewares: []string{"test"}, + TLS: &dynamic.RouterTLSConfig{CertResolver: "router"}, + }, + }, + Middlewares: make(map[string]*dynamic.Middleware), + Services: make(map[string]*dynamic.Service), + Models: map[string]*dynamic.Model{ + "websecure@internal": { + Middlewares: []string{"test"}, + TLS: &dynamic.RouterTLSConfig{CertResolver: "ep"}, + }, + }, + }, + }, + }, + { + desc: "with model, two entry points", + input: dynamic.Configuration{ + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "test": { + EntryPoints: []string{"websecure", "web"}, + }, + }, + Middlewares: make(map[string]*dynamic.Middleware), + Services: make(map[string]*dynamic.Service), + Models: map[string]*dynamic.Model{ + "websecure@internal": { + Middlewares: []string{"test"}, + TLS: &dynamic.RouterTLSConfig{}, + }, + }, + }, + }, + expected: dynamic.Configuration{ + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "test": { + EntryPoints: []string{"web"}, + }, + "websecure-test": { + EntryPoints: []string{"websecure"}, + Middlewares: []string{"test"}, + TLS: &dynamic.RouterTLSConfig{}, + }, + }, + Middlewares: make(map[string]*dynamic.Middleware), + Services: make(map[string]*dynamic.Service), + Models: map[string]*dynamic.Model{ + "websecure@internal": { + Middlewares: []string{"test"}, + TLS: &dynamic.RouterTLSConfig{}, + }, + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := applyModel(test.input) + + assert.Equal(t, test.expected, actual) + }) + } +} diff --git a/pkg/server/configurationwatcher.go b/pkg/server/configurationwatcher.go index 7291127d4..4d457beb5 100644 --- a/pkg/server/configurationwatcher.go +++ b/pkg/server/configurationwatcher.go @@ -18,6 +18,8 @@ import ( type ConfigurationWatcher struct { provider provider.Provider + entryPoints []string + providersThrottleDuration time.Duration currentConfigurations safe.Safe @@ -32,7 +34,12 @@ type ConfigurationWatcher struct { } // NewConfigurationWatcher creates a new ConfigurationWatcher. -func NewConfigurationWatcher(routinesPool *safe.Pool, pvd provider.Provider, providersThrottleDuration time.Duration) *ConfigurationWatcher { +func NewConfigurationWatcher( + routinesPool *safe.Pool, + pvd provider.Provider, + providersThrottleDuration time.Duration, + entryPoints []string, +) *ConfigurationWatcher { watcher := &ConfigurationWatcher{ provider: pvd, configurationChan: make(chan dynamic.Message, 100), @@ -40,6 +47,7 @@ func NewConfigurationWatcher(routinesPool *safe.Pool, pvd provider.Provider, pro providerConfigUpdateMap: make(map[string]chan dynamic.Message), providersThrottleDuration: providersThrottleDuration, routinesPool: routinesPool, + entryPoints: entryPoints, } currentConfigurations := make(dynamic.Configurations) @@ -135,7 +143,8 @@ func (c *ConfigurationWatcher) loadMessage(configMsg dynamic.Message) { c.currentConfigurations.Set(newConfigurations) - conf := mergeConfiguration(newConfigurations) + conf := mergeConfiguration(newConfigurations, c.entryPoints) + conf = applyModel(conf) for _, listener := range c.configurationListeners { listener(conf) diff --git a/pkg/server/configurationwatcher_test.go b/pkg/server/configurationwatcher_test.go index 83dd7a947..78bab9769 100644 --- a/pkg/server/configurationwatcher_test.go +++ b/pkg/server/configurationwatcher_test.go @@ -55,7 +55,7 @@ func TestNewConfigurationWatcher(t *testing.T) { }}, } - watcher := NewConfigurationWatcher(routinesPool, pvd, time.Second) + watcher := NewConfigurationWatcher(routinesPool, pvd, time.Second, []string{}) run := make(chan struct{}) @@ -112,7 +112,7 @@ func TestListenProvidersThrottleProviderConfigReload(t *testing.T) { }) } - watcher := NewConfigurationWatcher(routinesPool, pvd, 30*time.Millisecond) + watcher := NewConfigurationWatcher(routinesPool, pvd, 30*time.Millisecond, []string{}) publishedConfigCount := 0 watcher.AddListener(func(_ dynamic.Configuration) { @@ -136,7 +136,7 @@ func TestListenProvidersSkipsEmptyConfigs(t *testing.T) { messages: []dynamic.Message{{ProviderName: "mock"}}, } - watcher := NewConfigurationWatcher(routinesPool, pvd, time.Second) + watcher := NewConfigurationWatcher(routinesPool, pvd, time.Second, []string{}) watcher.AddListener(func(_ dynamic.Configuration) { t.Error("An empty configuration was published but it should not") }) @@ -162,7 +162,7 @@ func TestListenProvidersSkipsSameConfigurationForProvider(t *testing.T) { messages: []dynamic.Message{message, message}, } - watcher := NewConfigurationWatcher(routinesPool, pvd, 0) + watcher := NewConfigurationWatcher(routinesPool, pvd, 0, []string{}) alreadyCalled := false watcher.AddListener(func(_ dynamic.Configuration) { @@ -205,7 +205,7 @@ func TestListenProvidersDoesNotSkipFlappingConfiguration(t *testing.T) { }, } - watcher := NewConfigurationWatcher(routinesPool, pvd, 15*time.Millisecond) + watcher := NewConfigurationWatcher(routinesPool, pvd, 15*time.Millisecond, []string{"defaultEP"}) var lastConfig dynamic.Configuration watcher.AddListener(func(conf dynamic.Configuration) { @@ -220,7 +220,7 @@ func TestListenProvidersDoesNotSkipFlappingConfiguration(t *testing.T) { expected := dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithRouters(th.WithRouter("foo@mock")), + th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("defaultEP"))), th.WithLoadBalancerServices(th.WithService("bar@mock")), th.WithMiddlewares(), ), @@ -260,7 +260,7 @@ func TestListenProvidersPublishesConfigForEachProvider(t *testing.T) { }, } - watcher := NewConfigurationWatcher(routinesPool, pvd, 0) + watcher := NewConfigurationWatcher(routinesPool, pvd, 0, []string{"defaultEP"}) var publishedProviderConfig dynamic.Configuration @@ -276,7 +276,10 @@ func TestListenProvidersPublishesConfigForEachProvider(t *testing.T) { expected := dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithRouters(th.WithRouter("foo@mock"), th.WithRouter("foo@mock2")), + th.WithRouters( + th.WithRouter("foo@mock", th.WithEntryPoints("defaultEP")), + th.WithRouter("foo@mock2", th.WithEntryPoints("defaultEP")), + ), th.WithLoadBalancerServices(th.WithService("bar@mock"), th.WithService("bar@mock2")), th.WithMiddlewares(), ), diff --git a/pkg/server/router/router_test.go b/pkg/server/router/router_test.go index 5b30e3395..f7b17164c 100644 --- a/pkg/server/router/router_test.go +++ b/pkg/server/router/router_test.go @@ -76,28 +76,6 @@ func TestRouterManager_Get(t *testing.T) { entryPoints: []string{"web"}, expected: expectedResult{StatusCode: http.StatusNotFound}, }, - { - desc: "no middleware, default entry point", - routersConfig: map[string]*dynamic.Router{ - "foo": { - Service: "foo-service", - Rule: "Host(`foo.bar`)", - }, - }, - serviceConfig: map[string]*dynamic.Service{ - "foo-service": { - LoadBalancer: &dynamic.ServersLoadBalancer{ - Servers: []dynamic.Server{ - { - URL: server.URL, - }, - }, - }, - }, - }, - entryPoints: []string{"web"}, - expected: expectedResult{StatusCode: http.StatusOK}, - }, { desc: "no middleware, no matching", routersConfig: map[string]*dynamic.Router{ @@ -735,6 +713,14 @@ func TestRuntimeConfiguration(t *testing.T) { func TestProviderOnMiddlewares(t *testing.T) { entryPoints := []string{"web"} + staticCfg := static.Configuration{ + EntryPoints: map[string]*static.EntryPoint{ + "web": { + Address: ":80", + }, + }, + } + rtConf := runtime.NewConfig(dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Services: map[string]*dynamic.Service{ @@ -746,11 +732,13 @@ func TestProviderOnMiddlewares(t *testing.T) { }, Routers: map[string]*dynamic.Router{ "router@file": { + EntryPoints: []string{"web"}, Rule: "Host(`test`)", Service: "test@file", Middlewares: []string{"chain@file", "m1"}, }, "router@docker": { + EntryPoints: []string{"web"}, Rule: "Host(`test`)", Service: "test@file", Middlewares: []string{"chain", "m1@file"}, @@ -774,7 +762,7 @@ func TestProviderOnMiddlewares(t *testing.T) { serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport, nil, nil) middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager) responseModifierFactory := responsemodifiers.NewBuilder(map[string]*runtime.MiddlewareInfo{}) - chainBuilder := middleware.NewChainBuilder(static.Configuration{}, nil, nil) + chainBuilder := middleware.NewChainBuilder(staticCfg, nil, nil) routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory, chainBuilder) diff --git a/pkg/server/routerfactory_test.go b/pkg/server/routerfactory_test.go index acabdd94a..54ea8c73b 100644 --- a/pkg/server/routerfactory_test.go +++ b/pkg/server/routerfactory_test.go @@ -23,17 +23,18 @@ func TestReuseService(t *testing.T) { staticConfig := static.Configuration{ EntryPoints: map[string]*static.EntryPoint{ - "http": {}, + "web": {}, }, } dynamicConfigs := th.BuildConfiguration( th.WithRouters( th.WithRouter("foo", + th.WithEntryPoints("web"), th.WithServiceName("bar"), th.WithRule("Path(`/ok`)")), th.WithRouter("foo2", - th.WithEntryPoints("http"), + th.WithEntryPoints("web"), th.WithRule("Path(`/unauthorized`)"), th.WithServiceName("bar"), th.WithRouterMiddlewares("basicauth")), @@ -56,7 +57,7 @@ func TestReuseService(t *testing.T) { // Test that the /ok path returns a status 200. responseRecorderOk := &httptest.ResponseRecorder{} requestOk := httptest.NewRequest(http.MethodGet, testServer.URL+"/ok", nil) - entryPointsHandlers["http"].GetHTTPHandler().ServeHTTP(responseRecorderOk, requestOk) + entryPointsHandlers["web"].GetHTTPHandler().ServeHTTP(responseRecorderOk, requestOk) assert.Equal(t, http.StatusOK, responseRecorderOk.Result().StatusCode, "status code") @@ -64,7 +65,7 @@ func TestReuseService(t *testing.T) { // the basic authentication defined on the frontend. responseRecorderUnauthorized := &httptest.ResponseRecorder{} requestUnauthorized := httptest.NewRequest(http.MethodGet, testServer.URL+"/unauthorized", nil) - entryPointsHandlers["http"].GetHTTPHandler().ServeHTTP(responseRecorderUnauthorized, requestUnauthorized) + entryPointsHandlers["web"].GetHTTPHandler().ServeHTTP(responseRecorderUnauthorized, requestUnauthorized) assert.Equal(t, http.StatusUnauthorized, responseRecorderUnauthorized.Result().StatusCode, "status code") } @@ -83,7 +84,7 @@ func TestServerResponseEmptyBackend(t *testing.T) { config: func(testServerURL string) *dynamic.HTTPConfiguration { return th.BuildConfiguration( th.WithRouters(th.WithRouter("foo", - th.WithEntryPoints("http"), + th.WithEntryPoints("web"), th.WithServiceName("bar"), th.WithRule(routeRule)), ), @@ -106,7 +107,7 @@ func TestServerResponseEmptyBackend(t *testing.T) { config: func(testServerURL string) *dynamic.HTTPConfiguration { return th.BuildConfiguration( th.WithRouters(th.WithRouter("foo", - th.WithEntryPoints("http"), + th.WithEntryPoints("web"), th.WithServiceName("bar"), th.WithRule(routeRule)), ), @@ -120,7 +121,7 @@ func TestServerResponseEmptyBackend(t *testing.T) { config: func(testServerURL string) *dynamic.HTTPConfiguration { return th.BuildConfiguration( th.WithRouters(th.WithRouter("foo", - th.WithEntryPoints("http"), + th.WithEntryPoints("web"), th.WithServiceName("bar"), th.WithRule(routeRule)), ), @@ -136,7 +137,7 @@ func TestServerResponseEmptyBackend(t *testing.T) { config: func(testServerURL string) *dynamic.HTTPConfiguration { return th.BuildConfiguration( th.WithRouters(th.WithRouter("foo", - th.WithEntryPoints("http"), + th.WithEntryPoints("web"), th.WithServiceName("bar"), th.WithRule(routeRule)), ), @@ -150,7 +151,7 @@ func TestServerResponseEmptyBackend(t *testing.T) { config: func(testServerURL string) *dynamic.HTTPConfiguration { return th.BuildConfiguration( th.WithRouters(th.WithRouter("foo", - th.WithEntryPoints("http"), + th.WithEntryPoints("web"), th.WithServiceName("bar"), th.WithRule(routeRule)), ), @@ -176,7 +177,7 @@ func TestServerResponseEmptyBackend(t *testing.T) { staticConfig := static.Configuration{ EntryPoints: map[string]*static.EntryPoint{ - "http": {}, + "web": {}, }, } @@ -190,7 +191,7 @@ func TestServerResponseEmptyBackend(t *testing.T) { responseRecorder := &httptest.ResponseRecorder{} request := httptest.NewRequest(http.MethodGet, testServer.URL+requestPath, nil) - entryPointsHandlers["http"].GetHTTPHandler().ServeHTTP(responseRecorder, request) + entryPointsHandlers["web"].GetHTTPHandler().ServeHTTP(responseRecorder, request) assert.Equal(t, test.expectedStatusCode, responseRecorder.Result().StatusCode, "status code") }) @@ -206,13 +207,14 @@ func TestInternalServices(t *testing.T) { staticConfig := static.Configuration{ API: &static.API{}, EntryPoints: map[string]*static.EntryPoint{ - "http": {}, + "web": {}, }, } dynamicConfigs := th.BuildConfiguration( th.WithRouters( th.WithRouter("foo", + th.WithEntryPoints("web"), th.WithServiceName("api@internal"), th.WithRule("PathPrefix(`/api`)")), ), @@ -228,7 +230,7 @@ func TestInternalServices(t *testing.T) { // Test that the /ok path returns a status 200. responseRecorderOk := &httptest.ResponseRecorder{} requestOk := httptest.NewRequest(http.MethodGet, testServer.URL+"/api/rawdata", nil) - entryPointsHandlers["http"].GetHTTPHandler().ServeHTTP(responseRecorderOk, requestOk) + entryPointsHandlers["web"].GetHTTPHandler().ServeHTTP(responseRecorderOk, requestOk) assert.Equal(t, http.StatusOK, responseRecorderOk.Result().StatusCode, "status code") } diff --git a/pkg/server/service/internalhandler.go b/pkg/server/service/internalhandler.go index 56b9e66d9..2beb0578e 100644 --- a/pkg/server/service/internalhandler.go +++ b/pkg/server/service/internalhandler.go @@ -53,6 +53,11 @@ func (m *InternalHandlers) BuildHTTP(rootCtx context.Context, serviceName string func (m *InternalHandlers) get(serviceName string) (http.Handler, error) { switch serviceName { + case "noop@internal": + return http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { + rw.WriteHeader(http.StatusTeapot) + }), nil + case "api@internal": if m.api == nil { return nil, errors.New("api is not enabled") diff --git a/pkg/testhelpers/config.go b/pkg/testhelpers/config.go index 5f6c48165..3a2c00ca6 100644 --- a/pkg/testhelpers/config.go +++ b/pkg/testhelpers/config.go @@ -6,7 +6,10 @@ import ( // BuildConfiguration is a helper to create a configuration. func BuildConfiguration(dynamicConfigBuilders ...func(*dynamic.HTTPConfiguration)) *dynamic.HTTPConfiguration { - conf := &dynamic.HTTPConfiguration{} + conf := &dynamic.HTTPConfiguration{ + Models: map[string]*dynamic.Model{}, + } + for _, build := range dynamicConfigBuilders { build(conf) }