From aa0b5466a9841c3559c7df2a87b77e2276eea328 Mon Sep 17 00:00:00 2001 From: Seth Hoenig Date: Fri, 10 Jun 2022 11:32:08 -0500 Subject: [PATCH] Implement Traefik provider for Nomad orchestrator --- docs/content/assets/img/providers/nomad.png | Bin 0 -> 13001 bytes docs/content/providers/nomad.md | 544 ++++ docs/content/providers/overview.md | 3 + .../reference/dynamic-configuration/nomad.md | 16 + .../reference/dynamic-configuration/nomad.yml | 1 + .../reference/static-configuration/cli-ref.md | 51 + .../reference/static-configuration/env-ref.md | 51 + .../reference/static-configuration/file.toml | 19 + .../reference/static-configuration/file.yaml | 19 + .../routing/providers/consul-catalog.md | 2 +- docs/content/routing/providers/nomad.md | 466 ++++ docs/mkdocs.yml | 3 + go.mod | 8 +- go.sum | 17 +- pkg/config/static/static_config.go | 2 + pkg/provider/aggregator/aggregator.go | 4 + pkg/provider/nomad/config.go | 267 ++ pkg/provider/nomad/config_test.go | 2280 +++++++++++++++++ pkg/provider/nomad/nomad.go | 278 ++ pkg/provider/nomad/nomad_test.go | 169 ++ pkg/provider/nomad/tag.go | 19 + pkg/provider/nomad/tag_test.go | 109 + webui/src/statics/providers/nomad.svg | 7 + 23 files changed, 4328 insertions(+), 7 deletions(-) create mode 100644 docs/content/assets/img/providers/nomad.png create mode 100644 docs/content/providers/nomad.md create mode 100644 docs/content/reference/dynamic-configuration/nomad.md create mode 100644 docs/content/reference/dynamic-configuration/nomad.yml create mode 100644 docs/content/routing/providers/nomad.md create mode 100644 pkg/provider/nomad/config.go create mode 100644 pkg/provider/nomad/config_test.go create mode 100644 pkg/provider/nomad/nomad.go create mode 100644 pkg/provider/nomad/nomad_test.go create mode 100644 pkg/provider/nomad/tag.go create mode 100644 pkg/provider/nomad/tag_test.go create mode 100755 webui/src/statics/providers/nomad.svg diff --git a/docs/content/assets/img/providers/nomad.png b/docs/content/assets/img/providers/nomad.png new file mode 100644 index 0000000000000000000000000000000000000000..36ccd9cc6db3be1f48f5b89a967813d4888f91d5 GIT binary patch literal 13001 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: Plugins configuration. diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 861ac7c87..fca125102 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -822,6 +822,57 @@ Display additional provider logs. (Default: ```false```) `TRAEFIK_PROVIDERS_MARATHON_WATCH`: Watch provider. (Default: ```true```) +`TRAEFIK_PROVIDERS_NOMAD`: +Enable Nomad backend with default settings. (Default: ```false```) + +`TRAEFIK_PROVIDERS_NOMAD_CONSTRAINTS`: +Constraints is an expression that Traefik matches against the Nomad service's tags to determine whether to create route(s) for that service. + +`TRAEFIK_PROVIDERS_NOMAD_DEFAULTRULE`: +Default rule. (Default: ```Host(`{{ normalize .Name }}`)```) + +`TRAEFIK_PROVIDERS_NOMAD_ENDPOINT_ADDRESS`: +The address of the Nomad server, including scheme and port. + +`TRAEFIK_PROVIDERS_NOMAD_ENDPOINT_ENDPOINTWAITTIME`: +WaitTime limits how long a Watch will block. If not provided, the agent default values will be used (Default: ```0```) + +`TRAEFIK_PROVIDERS_NOMAD_ENDPOINT_REGION`: +Nomad region to use. If not provided, the local agent region is used. + +`TRAEFIK_PROVIDERS_NOMAD_ENDPOINT_TLS_CA`: +TLS CA + +`TRAEFIK_PROVIDERS_NOMAD_ENDPOINT_TLS_CAOPTIONAL`: +TLS CA.Optional (Default: ```false```) + +`TRAEFIK_PROVIDERS_NOMAD_ENDPOINT_TLS_CERT`: +TLS cert + +`TRAEFIK_PROVIDERS_NOMAD_ENDPOINT_TLS_INSECURESKIPVERIFY`: +TLS insecure skip verify (Default: ```false```) + +`TRAEFIK_PROVIDERS_NOMAD_ENDPOINT_TLS_KEY`: +TLS key + +`TRAEFIK_PROVIDERS_NOMAD_ENDPOINT_TOKEN`: +Token is used to provide a per-request ACL token. + +`TRAEFIK_PROVIDERS_NOMAD_EXPOSEDBYDEFAULT`: +Expose Nomad services by default. (Default: ```true```) + +`TRAEFIK_PROVIDERS_NOMAD_NAMESPACE`: +Sets the Nomad namespace used to discover services. + +`TRAEFIK_PROVIDERS_NOMAD_PREFIX`: +Prefix for nomad service tags. (Default: ```traefik```) + +`TRAEFIK_PROVIDERS_NOMAD_REFRESHINTERVAL`: +Interval for polling Nomad API. (Default: ```15```) + +`TRAEFIK_PROVIDERS_NOMAD_STALE`: +Use stale consistency for catalog reads. (Default: ```false```) + `TRAEFIK_PROVIDERS_PLUGIN_`: Plugins configuration. diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index 7f30c61f8..92b3357c6 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -174,6 +174,25 @@ [providers.consulCatalog.endpoint.httpAuth] username = "foobar" password = "foobar" + [providers.nomad] + constraints = "foobar" + prefix = "foobar" + refreshInterval = "42s" + stale = true + exposedByDefault = true + defaultRule = "foobar" + namespace = "foobar" + [providers.nomad.endpoint] + address = "foobar" + region = "foobar" + token = "foobar" + endpointWaitTime = "42s" + [providers.nomad.endpoint.tls] + ca = "foobar" + caOptional = true + cert = "foobar" + key = "foobar" + insecureSkipVerify = true [providers.ecs] constraints = "foobar" exposedByDefault = true diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index 656909d9b..c6f47b78b 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -188,6 +188,25 @@ providers: httpAuth: username: foobar password: foobar + nomad: + constraints: foobar + prefix: foobar + refreshInterval: 42s + stale: true + exposedByDefault: true + defaultRule: foobar + namespace: foobar + endpoint: + address: foobar + region: foobar + token: foobar + endpointWaitTime: 42s + tls: + ca: foobar + caOptional: true + cert: foobar + key: foobar + insecureSkipVerify: true ecs: constraints: foobar exposedByDefault: true diff --git a/docs/content/routing/providers/consul-catalog.md b/docs/content/routing/providers/consul-catalog.md index b7c4801da..7115726ce 100644 --- a/docs/content/routing/providers/consul-catalog.md +++ b/docs/content/routing/providers/consul-catalog.md @@ -8,7 +8,7 @@ description: "Learn how to use Consul Catalog as a provider for routing configur A Story of Tags, Services & Instances {: .subtitle } -![Rancher](../../assets/img/providers/consul.png) +![Consul Catalog](../../assets/img/providers/consul.png) Attach tags to your services and let Traefik do the rest! diff --git a/docs/content/routing/providers/nomad.md b/docs/content/routing/providers/nomad.md new file mode 100644 index 000000000..fcba19523 --- /dev/null +++ b/docs/content/routing/providers/nomad.md @@ -0,0 +1,466 @@ +--- +title: "Traefik Nomad Service Discovery Routing" +description: "Learn how to use Nomad Service Discovery as a provider for routing configurations in Traefik Proxy. Read the technical documentation." +--- + +# Traefik and Nomad Service Discovery + +A story of Tags, Services & Nomads +{: .subtitle } + +![Nomad](../../assets/img/providers/nomad.png) + +Attach tags to your Nomad services and let Traefik do the rest! + +## Routing Configuration + +!!! info "tags" + + - tags are case insensitive. + - The complete list of tags can be found [the reference page](../../reference/dynamic-configuration/nomad.md) + +### General + +Traefik creates, for each Nomad service, a corresponding Traefik [service](../services/index.md) and [router](../routers/index.md). + +The Traefik service automatically gets a server per instance in this Nomad service, and the router gets a default rule attached to it, based on the Nomad service name. + +### Routers + +To update the configuration of the Router automatically attached to the service, add tags starting with `traefik.routers.{name-of-your-choice}.` and followed by the option you want to change. + +For example, to change the rule, you could add the tag ```traefik.http.routers.my-service.rule=Host(`example.com`)```. + +??? 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=web,websecure + ``` + +??? 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 [rule](../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 service, +add tags starting with `traefik.http.services.{name-of-your-choice}.`, followed by the option you want to change. + +For example, to change the `passHostHeader` behavior, +you'd add the tag `traefik.http.services.{name-of-your-choice}.loadbalancer.passhostheader=false`. + +??? info "`traefik.http.services..loadbalancer.server.port`" + + Registers a port. + Useful when the service exposes multiples ports. + + ```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.myservice.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=10 + ``` + +??? 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.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=10 + ``` + +??? 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 tags starting with `traefik.http.middlewares.{name-of-your-choice}.`, 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). + +??? example "Declaring and Referencing a Middleware" + + ```yaml + # ... + # Declaring a middleware + traefik.http.middlewares.my-redirect.redirectscheme.scheme=https + # Referencing a middleware + traefik.http.routers.my-service.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 tags. + +??? example "Declaring TCP Routers and Services" + + ```yaml + 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 Nomad service (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=myoptions + ``` + +??? info "`traefik.tcp.routers..tls.passthrough`" + + See [TLS](../routers/index.md#tls_1) for more information. + + ```yaml + traefik.tcp.routers.mytcprouter.tls.passthrough=true + ``` + +#### 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.terminationdelay`" + + See [termination delay](../services/index.md#termination-delay) for more information. + + ```yaml + traefik.tcp.services.mytcpservice.loadbalancer.terminationdelay=100 + ``` + +??? 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 + ``` + +### UDP + +You can declare UDP Routers and/or Services using tags. + +??? example "Declaring UDP Routers and Services" + + ```yaml + 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 Nomad service (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 service by setting `traefik.enable` to true or false. + +This option overrides the value of `exposedByDefault`. + +#### Port Lookup + +Traefik is capable of detecting the port to use, by following the default Nomad Service Discovery flow. +That means, if you just expose lets say port `:1337` on the Nomad job, traefik will pick up this port and use it. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 08c78cae2..c02fce7c6 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -77,6 +77,7 @@ nav: - 'Kubernetes Ingress': 'providers/kubernetes-ingress.md' - 'Kubernetes Gateway API': 'providers/kubernetes-gateway.md' - 'Consul Catalog': 'providers/consul-catalog.md' + - 'Nomad': 'providers/nomad.md' - 'ECS': 'providers/ecs.md' - 'Marathon': 'providers/marathon.md' - 'Rancher': 'providers/rancher.md' @@ -97,6 +98,7 @@ nav: - 'Kubernetes Ingress': 'routing/providers/kubernetes-ingress.md' - 'Kubernetes Gateway API': 'routing/providers/kubernetes-gateway.md' - 'Consul Catalog': 'routing/providers/consul-catalog.md' + - 'Nomad': 'routing/providers/nomad.md' - 'ECS': 'routing/providers/ecs.md' - 'Marathon': 'routing/providers/marathon.md' - 'Rancher': 'routing/providers/rancher.md' @@ -194,6 +196,7 @@ nav: - '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' + - 'Nomad': "reference/dynamic-configuration/nomad.md" - 'ECS': 'reference/dynamic-configuration/ecs.md' - 'KV': 'reference/dynamic-configuration/kv.md' - 'Marathon': 'reference/dynamic-configuration/marathon.md' diff --git a/go.mod b/go.mod index dd8a68cb5..c8c78965a 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/hashicorp/go-hclog v0.16.1 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.3.0 + github.com/hashicorp/nomad/api v0.0.0-20220506174431-b5665129cd1f github.com/influxdata/influxdb-client-go/v2 v2.7.0 github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d github.com/instana/go-sensor v1.38.3 @@ -41,7 +42,7 @@ require ( github.com/miekg/dns v1.1.47 github.com/mitchellh/copystructure v1.0.0 github.com/mitchellh/hashstructure v1.0.0 - github.com/mitchellh/mapstructure v1.4.2 + github.com/mitchellh/mapstructure v1.4.3 github.com/opentracing/opentracing-go v1.2.0 github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 github.com/openzipkin/zipkin-go v0.2.2 @@ -176,8 +177,9 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware v1.2.0 // indirect github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect github.com/hashicorp/consul/sdk v0.8.0 // indirect + github.com/hashicorp/cronexpr v1.1.1 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect - github.com/hashicorp/go-cleanhttp v0.5.1 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-immutable-radix v1.3.0 // indirect github.com/hashicorp/go-msgpack v0.5.5 // indirect github.com/hashicorp/go-retryablehttp v0.7.1 // indirect @@ -229,7 +231,7 @@ require ( github.com/miekg/pkcs11 v1.0.3 // indirect github.com/mimuret/golang-iij-dpf v0.7.1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/go-testing-interface v1.14.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/reflectwalk v1.0.1 // indirect github.com/moby/buildkit v0.8.2-0.20210401015549-df49b648c8bf // indirect github.com/moby/locker v1.0.1 // indirect diff --git a/go.sum b/go.sum index 1f63cfccd..fea636ab1 100644 --- a/go.sum +++ b/go.sum @@ -887,6 +887,8 @@ github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyN github.com/hashicorp/consul/sdk v0.7.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM= github.com/hashicorp/consul/sdk v0.8.0 h1:OJtKBtEjboEZvG6AOUdh4Z1Zbyu0WcxQ0qatRrZHTVU= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/hashicorp/cronexpr v1.1.1 h1:NJZDd87hGXjoZBdvyCF9mX4DCq5Wy7+A/w+A7q0wn6c= +github.com/hashicorp/cronexpr v1.1.1/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -894,8 +896,9 @@ github.com/hashicorp/go-bexpr v0.1.2 h1:ijMXI4qERbzxbCnkxmfUtwMyjrrk3y+Vt0MxojNC github.com/hashicorp/go-bexpr v0.1.2/go.mod h1:ANbpTX1oAql27TZkKVeW8p1w8NTdnyzPe/0qqPCKohU= github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-connlimit v0.3.0/go.mod h1:OUj9FGL1tPIhl/2RCfzYHrIiWj+VVPGNyVPnUX8AqS0= github.com/hashicorp/go-cty-funcs v0.0.0-20200930094925-2721b1e36840/go.mod h1:Abjk0jbRkDaNCzsRhOv2iDCofYpX1eVsjozoiK63qLA= github.com/hashicorp/go-discover v0.0.0-20200501174627-ad1e96bde088/go.mod h1:vZu6Opqf49xX5lsFAu7iFNewkcVF1sn/wyapZh5ytlg= @@ -963,6 +966,8 @@ github.com/hashicorp/memberlist v0.2.4/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn github.com/hashicorp/memberlist v0.3.0 h1:8+567mCcFDnS5ADl7lrpxPMWiFCElyUEeW0gtj34fMA= github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/net-rpc-msgpackrpc v0.0.0-20151116020338-a14192a58a69/go.mod h1:/z+jUGRBlwVpUZfjute9jWaF6/HuhjuFQuL1YXzVD1Q= +github.com/hashicorp/nomad/api v0.0.0-20220506174431-b5665129cd1f h1:jSBbBJcPca465gK6XfwdXRQnFCd63e0oJmqllZTsawI= +github.com/hashicorp/nomad/api v0.0.0-20220506174431-b5665129cd1f/go.mod h1:b/AoT79m3PEpb6tKCFKva/M+q1rKJNUk5mdu1S8DymM= github.com/hashicorp/raft v1.1.1/go.mod h1:vPAJM8Asw6u8LxC3eJCUZmRP/E4QmUGE1R7g7k8sG/8= github.com/hashicorp/raft v1.2.0/go.mod h1:vPAJM8Asw6u8LxC3eJCUZmRP/E4QmUGE1R7g7k8sG/8= github.com/hashicorp/raft v1.3.2 h1:j2tqHqFnDdWCepLxzuo3b6WzS2krIweBrvEoqBbWMTo= @@ -1099,6 +1104,8 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -1220,8 +1227,9 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.14.0 h1:/x0XQ6h+3U3nAyk1yx+bHPURrKa9sVVvYbuqZ7pIAtI= github.com/mitchellh/go-testing-interface v1.14.0/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed/go.mod h1:3rdaFaCv4AyBgu5ALFM0+tSuHrBh6v692nyQe3ikrq0= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= @@ -1236,8 +1244,9 @@ github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1-0.20210112042008-8ebf2d61a8b4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/mitchellh/pointerstructure v1.0.0 h1:ATSdz4NWrmWPOF1CeCBU4sMCno2hgqdbSrRPFWQSVZI= github.com/mitchellh/pointerstructure v1.0.0/go.mod h1:k4XwG94++jLVsSiTxo7qdIfXA9pj9EAeo0QsNNJOLZ8= @@ -1510,6 +1519,8 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.4.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= diff --git a/pkg/config/static/static_config.go b/pkg/config/static/static_config.go index 095ce08ff..4cc699f22 100644 --- a/pkg/config/static/static_config.go +++ b/pkg/config/static/static_config.go @@ -26,6 +26,7 @@ import ( "github.com/traefik/traefik/v2/pkg/provider/kv/redis" "github.com/traefik/traefik/v2/pkg/provider/kv/zk" "github.com/traefik/traefik/v2/pkg/provider/marathon" + "github.com/traefik/traefik/v2/pkg/provider/nomad" "github.com/traefik/traefik/v2/pkg/provider/rancher" "github.com/traefik/traefik/v2/pkg/provider/rest" "github.com/traefik/traefik/v2/pkg/tls" @@ -184,6 +185,7 @@ type Providers struct { Rest *rest.Provider `description:"Enable Rest backend with default settings." json:"rest,omitempty" toml:"rest,omitempty" yaml:"rest,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"` Rancher *rancher.Provider `description:"Enable Rancher backend with default settings." json:"rancher,omitempty" toml:"rancher,omitempty" yaml:"rancher,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"` 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.Provider `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"` diff --git a/pkg/provider/aggregator/aggregator.go b/pkg/provider/aggregator/aggregator.go index 16a32003f..07d4488fb 100644 --- a/pkg/provider/aggregator/aggregator.go +++ b/pkg/provider/aggregator/aggregator.go @@ -114,6 +114,10 @@ func NewProviderAggregator(conf static.Providers) ProviderAggregator { } } + if conf.Nomad != nil { + p.quietAddProvider(conf.Nomad) + } + if conf.Consul != nil { for _, pvd := range conf.Consul.BuildProviders() { p.quietAddProvider(pvd) diff --git a/pkg/provider/nomad/config.go b/pkg/provider/nomad/config.go new file mode 100644 index 000000000..6ae3ebdfb --- /dev/null +++ b/pkg/provider/nomad/config.go @@ -0,0 +1,267 @@ +package nomad + +import ( + "context" + "errors" + "fmt" + "net" + "strconv" + + "github.com/traefik/traefik/v2/pkg/config/dynamic" + "github.com/traefik/traefik/v2/pkg/config/label" + "github.com/traefik/traefik/v2/pkg/log" + "github.com/traefik/traefik/v2/pkg/provider" + "github.com/traefik/traefik/v2/pkg/provider/constraints" +) + +func (p *Provider) buildConfig(ctx context.Context, items []item) *dynamic.Configuration { + configurations := make(map[string]*dynamic.Configuration) + + for _, i := range items { + svcName := provider.Normalize(i.Node + "-" + i.Name + "-" + i.ID) + ctxSvc := log.With(ctx, log.Str(log.ServiceName, svcName)) + + if !p.keepItem(ctxSvc, i) { + continue + } + + logger := log.FromContext(ctx) + labels := tagsToLabels(i.Tags, p.Prefix) + + config, err := label.DecodeConfiguration(labels) + if err != nil { + logger.Errorf("Failed to decode configuration: %v", err) + continue + } + + var tcpOrUDP bool + + if len(config.TCP.Routers) > 0 || len(config.TCP.Services) > 0 { + tcpOrUDP = true + if err := p.buildTCPConfig(i, config.TCP); err != nil { + logger.Errorf("Failed to build TCP service configuration: %v", err) + continue + } + provider.BuildTCPRouterConfiguration(ctxSvc, config.TCP) + } + + if len(config.UDP.Routers) > 0 || len(config.UDP.Services) > 0 { + tcpOrUDP = true + if err := p.buildUDPConfig(i, config.UDP); err != nil { + logger.Errorf("Failed to build UDP service configuration: %v", err) + continue + } + provider.BuildUDPRouterConfiguration(ctxSvc, config.UDP) + } + + // tcp/udp, skip configuring http service + if tcpOrUDP && len(config.HTTP.Routers) == 0 && + len(config.HTTP.Middlewares) == 0 && + len(config.HTTP.Services) == 0 { + configurations[svcName] = config + continue + } + + // configure http service + if err := p.buildServiceConfig(i, config.HTTP); err != nil { + logger.Errorf("Failed to build HTTP service configuration: %v", err) + continue + } + + model := struct { + Name string + Labels map[string]string + }{ + Name: i.Name, + Labels: labels, + } + + provider.BuildRouterConfiguration(ctx, config.HTTP, provider.Normalize(i.Name), p.defaultRuleTpl, model) + configurations[svcName] = config + } + + return provider.Merge(ctx, configurations) +} + +func (p *Provider) buildTCPConfig(i item, configuration *dynamic.TCPConfiguration) error { + if len(configuration.Services) == 0 { + configuration.Services = make(map[string]*dynamic.TCPService) + + lb := new(dynamic.TCPServersLoadBalancer) + lb.SetDefaults() + + configuration.Services[provider.Normalize(i.Name)] = &dynamic.TCPService{ + LoadBalancer: lb, + } + } + + for _, service := range configuration.Services { + if err := p.addServerTCP(i, service.LoadBalancer); err != nil { + return err + } + } + + return nil +} + +func (p *Provider) buildUDPConfig(i item, configuration *dynamic.UDPConfiguration) error { + if len(configuration.Services) == 0 { + configuration.Services = make(map[string]*dynamic.UDPService) + + configuration.Services[provider.Normalize(i.Name)] = &dynamic.UDPService{ + LoadBalancer: new(dynamic.UDPServersLoadBalancer), + } + } + + for _, service := range configuration.Services { + if err := p.addServerUDP(i, service.LoadBalancer); err != nil { + return err + } + } + + return nil +} + +func (p *Provider) buildServiceConfig(i item, configuration *dynamic.HTTPConfiguration) error { + if len(configuration.Services) == 0 { + configuration.Services = make(map[string]*dynamic.Service) + + lb := new(dynamic.ServersLoadBalancer) + lb.SetDefaults() + + configuration.Services[provider.Normalize(i.Name)] = &dynamic.Service{ + LoadBalancer: lb, + } + } + + for _, service := range configuration.Services { + if err := p.addServer(i, service.LoadBalancer); err != nil { + return err + } + } + + return nil +} + +// TODO: check whether it is mandatory to filter again. +func (p *Provider) keepItem(ctx context.Context, i item) bool { + logger := log.FromContext(ctx) + + if !i.ExtraConf.Enable { + logger.Debug("Filtering disabled item") + return false + } + + matches, err := constraints.MatchTags(i.Tags, p.Constraints) + if err != nil { + logger.Errorf("Error matching constraint expressions: %v", err) + return false + } + if !matches { + logger.Debugf("Filtering out item due to constraints: %q", p.Constraints) + return false + } + + // TODO: filter on health when that information exists (nomad 1.4+) + + return true +} + +func (p *Provider) addServerTCP(i item, lb *dynamic.TCPServersLoadBalancer) error { + if lb == nil { + return errors.New("load-balancer is missing") + } + + var port string + if len(lb.Servers) > 0 { + port = lb.Servers[0].Port + } + + if len(lb.Servers) == 0 { + lb.Servers = []dynamic.TCPServer{{}} + } + + if i.Port != 0 && port == "" { + port = strconv.Itoa(i.Port) + } + lb.Servers[0].Port = "" + + if port == "" { + return errors.New("port is missing") + } + + if i.Address == "" { + return errors.New("address is missing") + } + + lb.Servers[0].Address = net.JoinHostPort(i.Address, port) + return nil +} + +func (p *Provider) addServerUDP(i item, lb *dynamic.UDPServersLoadBalancer) error { + if lb == nil { + return errors.New("load-balancer is missing") + } + + var port string + if len(lb.Servers) > 0 { + port = lb.Servers[0].Port + } + + if len(lb.Servers) == 0 { + lb.Servers = []dynamic.UDPServer{{}} + } + + if i.Port != 0 && port == "" { + port = strconv.Itoa(i.Port) + } + lb.Servers[0].Port = "" + + if port == "" { + return errors.New("port is missing") + } + + if i.Address == "" { + return errors.New("address is missing") + } + + lb.Servers[0].Address = net.JoinHostPort(i.Address, port) + return nil +} + +func (p *Provider) addServer(i item, lb *dynamic.ServersLoadBalancer) error { + if lb == nil { + return errors.New("load-balancer is missing") + } + + var port string + if len(lb.Servers) > 0 { + port = lb.Servers[0].Port + } + + if len(lb.Servers) == 0 { + server := dynamic.Server{} + server.SetDefaults() + + lb.Servers = []dynamic.Server{server} + } + + if i.Port != 0 && port == "" { + port = strconv.Itoa(i.Port) + } + lb.Servers[0].Port = "" + + if port == "" { + return errors.New("port is missing") + } + + if i.Address == "" { + return errors.New("address is missing") + } + + scheme := lb.Servers[0].Scheme + lb.Servers[0].Scheme = "" + lb.Servers[0].URL = fmt.Sprintf("%s://%s", scheme, net.JoinHostPort(i.Address, port)) + + return nil +} diff --git a/pkg/provider/nomad/config_test.go b/pkg/provider/nomad/config_test.go new file mode 100644 index 000000000..53cfc49dd --- /dev/null +++ b/pkg/provider/nomad/config_test.go @@ -0,0 +1,2280 @@ +package nomad + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "github.com/traefik/traefik/v2/pkg/config/dynamic" +) + +func Test_defaultRule(t *testing.T) { + testCases := []struct { + desc string + items []item + rule string + expected *dynamic.Configuration + }{ + { + desc: "default rule with no variable", + items: []item{ + { + ID: "id", + Node: "node1", + Name: "Test", + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + rule: "Host(`example.com`)", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test": { + Service: "Test", + Rule: "Host(`example.com`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "default rule with label", + items: []item{ + { + ID: "id", + Node: "Node1", + Name: "Test", + Address: "127.0.0.1", + Tags: []string{ + "traefik.domain=example.com", + }, + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + rule: `Host("{{ .Name }}.{{ index .Labels "traefik.domain" }}")`, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test": { + Service: "Test", + Rule: `Host("Test.example.com")`, + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "invalid rule", + items: []item{ + { + ID: "id", + Node: "Node1", + Name: "Test", + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + rule: `Host"{{ .Invalid }}")`, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "default template rule", + items: []item{ + { + ID: "id", + Node: "Node1", + Name: "Test", + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + rule: defaultTemplateRule, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test": { + Service: "Test", + Rule: "Host(`Test`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + p := new(Provider) + p.SetDefaults() + p.DefaultRule = test.rule + err := p.Init() + require.NoError(t, err) + + ctx := context.TODO() + config := p.buildConfig(ctx, test.items) + require.Equal(t, test.expected, config) + }) + } +} + +func Test_buildConfig(t *testing.T) { + testCases := []struct { + desc string + items []item + constraints string + expected *dynamic.Configuration + }{ + { + desc: "one service no tags", + items: []item{ + { + ID: "id", + Node: "Node1", + Name: "dev/Test", + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "dev-Test": { + Service: "dev-Test", + Rule: "Host(`dev-Test.traefik.test`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "dev-Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "two services no tags", + items: []item{ + { + ID: "id1", + Node: "Node1", + Name: "Test1", + Address: "192.168.1.101", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + { + ID: "id2", + Node: "Node2", + Name: "Test2", + Address: "192.168.1.102", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test1": { + Service: "Test1", + Rule: "Host(`Test1.traefik.test`)", + }, + "Test2": { + Service: "Test2", + Rule: "Host(`Test2.traefik.test`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test1": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://192.168.1.101:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + "Test2": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://192.168.1.102:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "two services with same name no label", + items: []item{ + { + ID: "id1", + Node: "Node1", + Name: "Test", + Tags: []string{}, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + { + ID: "id2", + Node: "Node2", + Name: "Test", + Tags: []string{}, + Address: "127.0.0.2", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test": { + Service: "Test", + Rule: "Host(`Test.traefik.test`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + { + URL: "http://127.0.0.2:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "two services same name and id no label same node", + items: []item{ + { + ID: "id1", + Node: "Node1", + Name: "Test", + Tags: []string{}, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + { + ID: "id1", + Node: "Node1", + Name: "Test", + Tags: []string{}, + Address: "127.0.0.2", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test": { + Service: "Test", + Rule: "Host(`Test.traefik.test`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.2:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "two services same service name and id no label on different nodes", + items: []item{ + { + ID: "id1", + Node: "Node1", + Name: "Test", + Tags: []string{}, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + { + ID: "id1", + Node: "Node2", + Name: "Test", + Tags: []string{}, + Address: "127.0.0.2", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test": { + Service: "Test", + Rule: "Host(`Test.traefik.test`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + { + URL: "http://127.0.0.2:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "one service with label (not on server)", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.http.services.Service1.loadbalancer.passhostheader=true", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test": { + Service: "Service1", + Rule: "Host(`Test.traefik.test`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Service1": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "one service with labels", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.http.services.Service1.loadbalancer.passhostheader = true", + "traefik.http.routers.Router1.rule = Host(`foo.com`)", + "traefik.http.routers.Router1.service = Service1", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Router1": { + Service: "Service1", + Rule: "Host(`foo.com`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Service1": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "one service with rule label", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.http.routers.Router1.rule = Host(`foo.com`)", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + Routers: map[string]*dynamic.Router{ + "Router1": { + Service: "Test", + Rule: "Host(`foo.com`)", + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "one service with rule label and one traefik service", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.http.routers.Router1.rule = Host(`foo.com`)", + "traefik.http.services.Service1.loadbalancer.passhostheader = true", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Router1": { + Service: "Service1", + Rule: "Host(`foo.com`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Service1": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "one service with rule label and two traefik services", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.http.routers.Router1.rule = Host(`foo.com`)", + "traefik.http.services.Service1.loadbalancer.passhostheader= true", + "traefik.http.services.Service2.loadbalancer.passhostheader = true", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Service1": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + "Service2": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "two services with same traefik service and different passhostheader", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.http.services.Service1.loadbalancer.passhostheader = true", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + { + ID: "id2", + Name: "Test", + Tags: []string{ + "traefik.http.services.Service1.loadbalancer.passhostheader = false", + }, + Address: "127.0.0.2", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test": { + Service: "Service1", + Rule: "Host(`Test.traefik.test`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "three services with same name and different passhostheader", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.http.services.Service1.loadbalancer.passhostheader = false", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + { + ID: "id2", + Name: "Test", + Tags: []string{ + "traefik.http.services.Service1.loadbalancer.passhostheader = true", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + { + ID: "id3", + Name: "Test", + Tags: []string{ + "traefik.http.services.Service1.loadbalancer.passhostheader = true", + }, + Address: "127.0.0.2", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test": { + Service: "Service1", + Rule: "Host(`Test.traefik.test`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "two services with same name and same LB methods", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.http.services.Service1.loadbalancer.passhostheader = true", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + { + ID: "id2", + Name: "Test", + Tags: []string{ + "traefik.http.services.Service1.loadbalancer.passhostheader = true", + }, + Address: "127.0.0.2", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test": { + Service: "Service1", + Rule: "Host(`Test.traefik.test`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Service1": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + { + URL: "http://127.0.0.2:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "one service with InFlightReq in label (default value)", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.http.middlewares.Middleware1.inflightreq.amount = 42", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test": { + Service: "Test", + Rule: "Host(`Test.traefik.test`)", + }, + }, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "Middleware1": { + InFlightReq: &dynamic.InFlightReq{ + Amount: 42, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "two services with same middleware", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.http.middlewares.Middleware1.inflightreq.amount = 42", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + { + ID: "id2", + Name: "Test", + Tags: []string{ + "traefik.http.middlewares.Middleware1.inflightreq.amount = 42", + }, + Address: "127.0.0.2", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test": { + Service: "Test", + Rule: "Host(`Test.traefik.test`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "Middleware1": { + InFlightReq: &dynamic.InFlightReq{ + Amount: 42, + }, + }, + }, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + { + URL: "http://127.0.0.2:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "two services same name with different middleware", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.http.middlewares.Middleware1.inflightreq.amount = 42", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + { + ID: "id2", + Name: "Test", + Tags: []string{ + "traefik.http.middlewares.Middleware1.inflightreq.amount = 41", + }, + Address: "127.0.0.2", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test": { + Service: "Test", + Rule: "Host(`Test.traefik.test`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + { + URL: "http://127.0.0.2:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "two services with different routers with same name", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.http.routers.Router1.rule = Host(`foo.com`)", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + { + ID: "id2", + Name: "Test", + Tags: []string{ + "traefik.http.routers.Router1.rule = Host(`bar.com`)", + }, + Address: "127.0.0.2", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + { + URL: "http://127.0.0.2:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "two services identical routers", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.http.routers.Router1.rule = Host(`foo.com`)", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + { + ID: "id2", + Name: "Test", + Tags: []string{ + "traefik.http.routers.Router1.rule = Host(`foo.com`)", + }, + Address: "127.0.0.2", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Router1": { + Service: "Test", + Rule: "Host(`foo.com`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + { + URL: "http://127.0.0.2:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "one service with bad label", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.wrong.label = 42", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test": { + Service: "Test", + Rule: "Host(`Test.traefik.test`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "one service with label port", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.http.services.Service1.LoadBalancer.server.scheme = h2c", + "traefik.http.services.Service1.LoadBalancer.server.port = 8080", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test": { + Service: "Service1", + Rule: "Host(`Test.traefik.test`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Service1": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "h2c://127.0.0.1:8080", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "one service with label port on two services", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.http.services.Service1.LoadBalancer.server.port = ", + "traefik.http.services.Service2.LoadBalancer.server.port = 8080", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Service1": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + "Service2": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:8080", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "one service without port", + items: []item{ + { + ID: "id1", + Node: "Node1", + Name: "Test", + Tags: []string{}, + Address: "127.0.0.2", + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "one service without port with middleware", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.http.middlewares.Middleware1.inflightreq.amount = 42", + }, + Address: "127.0.0.2", + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "one service with traefik.enable false", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.enable=false", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: false}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "one service with non matching constraints", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.tags=foo", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + constraints: `Tag("traefik.tags=bar")`, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "one service with matching constraints", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.tags=foo", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + constraints: `Tag("traefik.tags=foo")`, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test": { + Service: "Test", + Rule: "Host(`Test.traefik.test`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "middleware used in router", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.http.middlewares.Middleware1.basicauth.users = test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + "traefik.http.routers.Test.middlewares = Middleware1", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test": { + Service: "Test", + Rule: "Host(`Test.traefik.test`)", + Middlewares: []string{"Middleware1"}, + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "Middleware1": { + BasicAuth: &dynamic.BasicAuth{ + Users: []string{ + "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + }, + }, + }, + }, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "middleware used in tcp router", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.tcp.routers.Test.rule = HostSNI(`foo.bar`)", + "traefik.tcp.middlewares.Middleware1.ipwhitelist.sourcerange = foobar, fiibar", + "traefik.tcp.routers.Test.middlewares = Middleware1", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{ + "Test": { + Service: "Test", + Rule: "HostSNI(`foo.bar`)", + Middlewares: []string{"Middleware1"}, + }, + }, + Middlewares: map[string]*dynamic.TCPMiddleware{ + "Middleware1": { + IPWhiteList: &dynamic.TCPIPWhiteList{ + SourceRange: []string{"foobar", "fiibar"}, + }, + }, + }, + Services: map[string]*dynamic.TCPService{ + "Test": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{ + Servers: []dynamic.TCPServer{ + { + Address: "127.0.0.1:9999", + }, + }, + TerminationDelay: Int(100), + }, + }, + }, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "tcp with tags", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.tcp.routers.foo.rule = HostSNI(`foo.bar`)", + "traefik.tcp.routers.foo.tls = true", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{ + "foo": { + Service: "Test", + Rule: "HostSNI(`foo.bar`)", + TLS: &dynamic.RouterTCPTLSConfig{}, + }, + }, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{ + "Test": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{ + Servers: []dynamic.TCPServer{ + { + Address: "127.0.0.1:9999", + }, + }, + TerminationDelay: Int(100), + }, + }, + }, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "udp with tags", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.udp.routers.foo.entrypoints = mydns", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{ + "foo": { + Service: "Test", + EntryPoints: []string{"mydns"}, + }, + }, + Services: map[string]*dynamic.UDPService{ + "Test": { + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "127.0.0.1:9999", + }, + }, + }, + }, + }, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "tcp with label without rule", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.tcp.routers.foo.tls = true", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{ + "Test": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{ + Servers: []dynamic.TCPServer{ + { + Address: "127.0.0.1:9999", + }, + }, + TerminationDelay: Int(100), + }, + }, + }, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "tcp with tags and port", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.tcp.routers.foo.rule = HostSNI(`foo.bar`)", + "traefik.tcp.routers.foo.tls.options = foo", + "traefik.tcp.services.foo.loadbalancer.server.port = 80", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{ + "foo": { + Service: "foo", + Rule: "HostSNI(`foo.bar`)", + TLS: &dynamic.RouterTCPTLSConfig{ + Options: "foo", + }, + }, + }, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{ + "foo": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{ + Servers: []dynamic.TCPServer{ + { + Address: "127.0.0.1:80", + }, + }, + TerminationDelay: Int(100), + }, + }, + }, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "udp with label and port", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.udp.routers.foo.entrypoints = mydns", + "traefik.udp.services.foo.loadbalancer.server.port = 80", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{ + "foo": { + Service: "foo", + EntryPoints: []string{"mydns"}, + }, + }, + Services: map[string]*dynamic.UDPService{ + "foo": { + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "127.0.0.1:80", + }, + }, + }, + }, + }, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "tcp with label and port and http service", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.tcp.routers.foo.rule = HostSNI(`foo.bar`)", + "traefik.tcp.routers.foo.tls = true", + "traefik.tcp.services.foo.loadbalancer.server.port = 80", + "traefik.http.services.Service1.loadbalancer.passhostheader = true", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + { + ID: "id2", + Name: "Test", + Tags: []string{ + "traefik.tcp.routers.foo.rule = HostSNI(`foo.bar`)", + "traefik.tcp.routers.foo.tls = true", + "traefik.tcp.services.foo.loadbalancer.server.port = 80", + "traefik.http.services.Service1.loadbalancer.passhostheader = true", + }, + Address: "127.0.0.2", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{ + "foo": { + Service: "foo", + Rule: "HostSNI(`foo.bar`)", + TLS: &dynamic.RouterTCPTLSConfig{}, + }, + }, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{ + "foo": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{ + Servers: []dynamic.TCPServer{ + { + Address: "127.0.0.1:80", + }, + { + Address: "127.0.0.2:80", + }, + }, + TerminationDelay: Int(100), + }, + }, + }, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test": { + Service: "Service1", + Rule: "Host(`Test.traefik.test`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Service1": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + { + URL: "http://127.0.0.2:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "udp with label and port and http services", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.udp.routers.foo.entrypoints = mydns", + "traefik.udp.services.foo.loadbalancer.server.port = 80", + "traefik.http.services.Service1.loadbalancer.passhostheader = true", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + { + ID: "id2", + Name: "Test", + Tags: []string{ + "traefik.udp.routers.foo.entrypoints = mydns", + "traefik.udp.services.foo.loadbalancer.server.port = 80", + "traefik.http.services.Service1.loadbalancer.passhostheader = true", + }, + Address: "127.0.0.2", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{ + "foo": { + Service: "foo", + EntryPoints: []string{"mydns"}, + }, + }, + Services: map[string]*dynamic.UDPService{ + "foo": { + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "127.0.0.1:80", + }, + { + Address: "127.0.0.2:80", + }, + }, + }, + }, + }, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test": { + Service: "Service1", + Rule: "Host(`Test.traefik.test`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Service1": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:9999", + }, + { + URL: "http://127.0.0.2:9999", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "tcp with tag for tcp service", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.tcp.services.foo.loadbalancer.server.port = 80", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{ + "foo": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{ + Servers: []dynamic.TCPServer{ + { + Address: "127.0.0.1:80", + }, + }, + TerminationDelay: Int(100), + }, + }, + }, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "udp with label for tcp service", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.udp.services.foo.loadbalancer.server.port = 80", + }, + Address: "127.0.0.1", + Port: 9999, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{ + "foo": { + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "127.0.0.1:80", + }, + }, + }, + }, + }, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "tcp with label for tcp service, with termination delay", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.tcp.services.foo.loadbalancer.server.port = 80", + "traefik.tcp.services.foo.loadbalancer.terminationdelay = 200", + }, + Address: "127.0.0.1", + Port: 80, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{ + "foo": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{ + Servers: []dynamic.TCPServer{ + { + Address: "127.0.0.1:80", + }, + }, + TerminationDelay: Int(200), + }, + }, + }, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + p := new(Provider) + p.SetDefaults() + p.DefaultRule = "Host(`{{ normalize .Name }}.traefik.test`)" + p.Constraints = test.constraints + err := p.Init() + require.NoError(t, err) + + ctx := context.TODO() + c := p.buildConfig(ctx, test.items) + require.Equal(t, test.expected, c) + }) + } +} + +func Test_keepItem(t *testing.T) { + testCases := []struct { + name string + i item + constraints string + exp bool + }{ + { + name: "enable true", + i: item{ExtraConf: configuration{Enable: true}}, + exp: true, + }, + { + name: "enable false", + i: item{ExtraConf: configuration{Enable: false}}, + exp: false, + }, + { + name: "constraint matches", + i: item{ + Tags: []string{"traefik.tags=foo"}, + ExtraConf: configuration{Enable: true}, + }, + constraints: `Tag("traefik.tags=foo")`, + exp: true, + }, + { + name: "constraint not match", + i: item{ + Tags: []string{"traefik.tags=foo"}, + ExtraConf: configuration{Enable: true}, + }, + constraints: `Tag("traefik.tags=bar")`, + exp: false, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + p := new(Provider) + p.SetDefaults() + p.Constraints = test.constraints + ctx := context.TODO() + result := p.keepItem(ctx, test.i) + require.Equal(t, test.exp, result) + }) + } +} + +func Int(v int) *int { return &v } +func Bool(v bool) *bool { return &v } diff --git a/pkg/provider/nomad/nomad.go b/pkg/provider/nomad/nomad.go new file mode 100644 index 000000000..c7afa3b1d --- /dev/null +++ b/pkg/provider/nomad/nomad.go @@ -0,0 +1,278 @@ +package nomad + +import ( + "context" + "fmt" + "strings" + "text/template" + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/hashicorp/nomad/api" + ptypes "github.com/traefik/paerser/types" + "github.com/traefik/traefik/v2/pkg/config/dynamic" + "github.com/traefik/traefik/v2/pkg/job" + "github.com/traefik/traefik/v2/pkg/log" + "github.com/traefik/traefik/v2/pkg/provider" + "github.com/traefik/traefik/v2/pkg/provider/constraints" + "github.com/traefik/traefik/v2/pkg/safe" + "github.com/traefik/traefik/v2/pkg/types" +) + +const ( + // providerName is the name of this provider. + providerName = "nomad" + + // defaultTemplateRule is the default template for the default rule. + defaultTemplateRule = "Host(`{{ normalize .Name }}`)" + + // defaultPrefix is the default prefix used in tag values indicating the service + // should be consumed and exposed via traefik. + defaultPrefix = "traefik" +) + +var _ provider.Provider = (*Provider)(nil) + +type item struct { + ID string // service ID + Name string // service name + Namespace string // service namespace + Node string // node ID + Datacenter string // region + Address string // service address + Port int // service port + Tags []string // service tags + + ExtraConf configuration // global options +} + +// Provider holds configurations of the provider. +type Provider struct { + DefaultRule string `description:"Default rule." json:"defaultRule,omitempty" toml:"defaultRule,omitempty" yaml:"defaultRule,omitempty"` + Constraints string `description:"Constraints is an expression that Traefik matches against the Nomad service's tags to determine whether to create route(s) for that service." json:"constraints,omitempty" toml:"constraints,omitempty" yaml:"constraints,omitempty" export:"true"` + Endpoint *EndpointConfig `description:"Nomad endpoint settings" json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty" export:"true"` + Prefix string `description:"Prefix for nomad service tags." json:"prefix,omitempty" toml:"prefix,omitempty" yaml:"prefix,omitempty" export:"true"` + Stale bool `description:"Use stale consistency for catalog reads." json:"stale,omitempty" toml:"stale,omitempty" yaml:"stale,omitempty" export:"true"` + Namespace string `description:"Sets the Nomad namespace used to discover services." json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty" export:"true"` + ExposedByDefault bool `description:"Expose Nomad services by default." json:"exposedByDefault,omitempty" toml:"exposedByDefault,omitempty" yaml:"exposedByDefault,omitempty" export:"true"` + RefreshInterval ptypes.Duration `description:"Interval for polling Nomad API." json:"refreshInterval,omitempty" toml:"refreshInterval,omitempty" yaml:"refreshInterval,omitempty" export:"true"` + + client *api.Client // client for Nomad API + defaultRuleTpl *template.Template // default routing rule +} + +type EndpointConfig struct { + // Address is the Nomad endpoint address, if empty it defaults to NOMAD_ADDR or "http://localhost:4646". + Address string `description:"The address of the Nomad server, including scheme and port." json:"address,omitempty" toml:"address,omitempty" yaml:"address,omitempty"` + // Region is the Nomad region, if empty it defaults to NOMAD_REGION or "global". + Region string `description:"Nomad region to use. If not provided, the local agent region is used." json:"region,omitempty" toml:"region,omitempty" yaml:"region,omitempty"` + // Token is the ACL token to connect with Nomad, if empty it defaults to NOMAD_TOKEN. + Token string `description:"Token is used to provide a per-request ACL token." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty" loggable:"false"` + TLS *types.ClientTLS `description:"Configure TLS." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"` + EndpointWaitTime ptypes.Duration `description:"WaitTime limits how long a Watch will block. If not provided, the agent default values will be used" json:"endpointWaitTime,omitempty" toml:"endpointWaitTime,omitempty" yaml:"endpointWaitTime,omitempty" export:"true"` +} + +// SetDefaults sets the default values for the Nomad Traefik Provider. +func (p *Provider) SetDefaults() { + p.Endpoint = &EndpointConfig{} + p.Prefix = defaultPrefix + p.ExposedByDefault = true + p.RefreshInterval = ptypes.Duration(15 * time.Second) + p.DefaultRule = defaultTemplateRule +} + +// Init the Nomad Traefik 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 +} + +// Provide allows the Nomad Traefik Provider to provide configurations to traefik +// using the given configuration channel. +func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error { + var err error + p.client, err = createClient(p.Namespace, p.Endpoint) + if err != nil { + return fmt.Errorf("failed to create nomad API client: %w", err) + } + + pool.GoCtx(func(routineCtx context.Context) { + ctxLog := log.With(routineCtx, log.Str(log.ProviderName, providerName)) + logger := log.FromContext(ctxLog) + + operation := func() error { + ctx, cancel := context.WithCancel(ctxLog) + defer cancel() + + // load initial configuration + if err := p.loadConfiguration(ctx, configurationChan); err != nil { + return fmt.Errorf("failed to load initial nomad services: %w", err) + } + + // issue periodic refreshes in the background + // (Nomad does not support Watch style observations) + ticker := time.NewTicker(time.Duration(p.RefreshInterval)) + defer ticker.Stop() + + // enter loop where we wait for and respond to notifications + for { + select { + case <-ctx.Done(): + return nil + case <-ticker.C: + } + // load services due to refresh + if err := p.loadConfiguration(ctx, configurationChan); err != nil { + return fmt.Errorf("failed to refresh nomad services: %w", err) + } + } + } + + failure := func(err error, d time.Duration) { + logger.Errorf("Provider connection error %+v, retrying in %s", err, d) + } + + if retryErr := backoff.RetryNotify( + safe.OperationWithRecover(operation), + backoff.WithContext(job.NewBackOff(backoff.NewExponentialBackOff()), ctxLog), + failure, + ); retryErr != nil { + logger.Errorf("Cannot connect to Nomad server %+v", retryErr) + } + }) + + return nil +} + +func (p *Provider) loadConfiguration(ctx context.Context, configurationC chan<- dynamic.Message) error { + items, err := p.getNomadServiceData(ctx) + if err != nil { + return err + } + configurationC <- dynamic.Message{ + ProviderName: providerName, + Configuration: p.buildConfig(ctx, items), + } + + return nil +} + +func createClient(namespace string, endpoint *EndpointConfig) (*api.Client, error) { + config := api.Config{ + Address: endpoint.Address, + Region: endpoint.Region, + WaitTime: time.Duration(endpoint.EndpointWaitTime), + Namespace: namespace, + } + + if endpoint.TLS != nil { + config.TLSConfig = &api.TLSConfig{ + CACert: endpoint.TLS.CA, + ClientCert: endpoint.TLS.Cert, + ClientKey: endpoint.TLS.Key, + Insecure: endpoint.TLS.InsecureSkipVerify, + } + } + + return api.NewClient(&config) +} + +// configuration contains information from the service's tags that are globals +// (not specific to the dynamic configuration). +type configuration struct { + Enable bool // .enable +} + +// globalConfig returns a configuration with settings not specific to the dynamic configuration (i.e. ".enable"). +func (p *Provider) globalConfig(tags []string) configuration { + enabled := p.ExposedByDefault + labels := tagsToLabels(tags, p.Prefix) + + if v, exists := labels["traefik.enable"]; exists { + enabled = strings.EqualFold(v, "true") + } + + return configuration{Enable: enabled} +} + +func (p *Provider) getNomadServiceData(ctx context.Context) ([]item, error) { + // first, get list of service stubs + opts := &api.QueryOptions{AllowStale: p.Stale} + opts = opts.WithContext(ctx) + + stubs, _, err := p.client.Services().List(opts) + if err != nil { + return nil, err + } + + var items []item + + for _, stub := range stubs { + for _, service := range stub.Services { + logger := log.FromContext(log.With(ctx, log.Str("serviceName", service.ServiceName))) + + globalCfg := p.globalConfig(service.Tags) + if !globalCfg.Enable { + logger.Debug("Filter Nomad service that is not enabled") + continue + } + + matches, err := constraints.MatchTags(service.Tags, p.Constraints) + if err != nil { + logger.Errorf("Error matching constraint expressions: %v", err) + continue + } + + if !matches { + logger.Debugf("Filter Nomad service not matching constraints: %q", p.Constraints) + continue + } + + instances, err := p.fetchService(ctx, service.ServiceName) + if err != nil { + return nil, err + } + + for _, i := range instances { + items = append(items, item{ + ID: i.ID, + Name: i.ServiceName, + Namespace: i.Namespace, + Node: i.NodeID, + Datacenter: i.Datacenter, + Address: i.Address, + Port: i.Port, + Tags: i.Tags, + ExtraConf: p.globalConfig(i.Tags), + }) + } + } + } + + return items, nil +} + +// fetchService queries Nomad API for services matching name, +// that also have the .enable=true set in its tags. +func (p *Provider) fetchService(ctx context.Context, name string) ([]*api.ServiceRegistration, error) { + var tagFilter string + if !p.ExposedByDefault { + tagFilter = fmt.Sprintf(`Tags contains %q`, fmt.Sprintf("%s.enable=true", p.Prefix)) + } + + // TODO: Nomad currently (v1.3.0) does not support health checks, + // and as such does not yet return health status information. + // When it does, refactor this section to include health status. + opts := &api.QueryOptions{AllowStale: p.Stale, Filter: tagFilter} + opts = opts.WithContext(ctx) + + services, _, err := p.client.Services().Get(name, opts) + if err != nil { + return nil, fmt.Errorf("failed to fetch services: %w", err) + } + return services, nil +} diff --git a/pkg/provider/nomad/nomad_test.go b/pkg/provider/nomad/nomad_test.go new file mode 100644 index 000000000..3142eb3e9 --- /dev/null +++ b/pkg/provider/nomad/nomad_test.go @@ -0,0 +1,169 @@ +package nomad + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_globalConfig(t *testing.T) { + cases := []struct { + Name string + Prefix string + Tags []string + ExposedByDefault bool + exp configuration + }{ + { + Name: "expose_by_default_no_tags", + Prefix: "traefik", + Tags: nil, + ExposedByDefault: true, + exp: configuration{Enable: true}, + }, + { + Name: "not_expose_by_default_no_tags", + Prefix: "traefik", + Tags: nil, + ExposedByDefault: false, + exp: configuration{Enable: false}, + }, + { + Name: "expose_by_default_tags_enable", + Prefix: "traefik", + Tags: []string{"traefik.enable=true"}, + ExposedByDefault: true, + exp: configuration{Enable: true}, + }, + { + Name: "expose_by_default_tags_disable", + Prefix: "traefik", + Tags: []string{"traefik.enable=false"}, + ExposedByDefault: true, + exp: configuration{Enable: false}, + }, + { + Name: "expose_by_default_tags_enable_custom_prefix", + Prefix: "custom", + Tags: []string{"custom.enable=true"}, + ExposedByDefault: true, + exp: configuration{Enable: true}, + }, + { + Name: "expose_by_default_tags_disable_custom_prefix", + Prefix: "custom", + Tags: []string{"custom.enable=false"}, + ExposedByDefault: true, + exp: configuration{Enable: false}, + }, + } + + for _, test := range cases { + t.Run(test.Name, func(t *testing.T) { + p := Provider{ExposedByDefault: test.ExposedByDefault, Prefix: test.Prefix} + result := p.globalConfig(test.Tags) + require.Equal(t, test.exp, result) + }) + } +} + +func Test_getNomadServiceData(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasSuffix(r.RequestURI, "/v1/services"): + _, _ = w.Write([]byte(services)) + case strings.HasSuffix(r.RequestURI, "/v1/service/redis"): + _, _ = w.Write([]byte(redis)) + case strings.HasSuffix(r.RequestURI, "/v1/service/hello-nomad"): + _, _ = w.Write([]byte(hello)) + } + })) + t.Cleanup(ts.Close) + + p := new(Provider) + p.SetDefaults() + p.Endpoint.Address = ts.URL + err := p.Init() + require.NoError(t, err) + + // fudge client, avoid starting up via Provide + p.client, err = createClient(p.Namespace, p.Endpoint) + require.NoError(t, err) + + // make the query for services + items, err := p.getNomadServiceData(context.TODO()) + require.NoError(t, err) + require.Len(t, items, 2) +} + +const services = ` +[ + { + "Namespace": "default", + "Services": [ + { + "ServiceName": "redis", + "Tags": [ + "traefik.enable=true" + ] + }, + { + "ServiceName": "hello-nomad", + "Tags": [ + "traefik.enable=true", + "traefik.http.routers.hellon.entrypoints=web", + "traefik.http.routers.hellon.service=hello-nomad" + ] + } + ] + } +] +` + +const redis = ` +[ + { + "Address": "127.0.0.1", + "AllocID": "07501480-8175-8071-7da6-133bd1ff890f", + "CreateIndex": 46, + "Datacenter": "dc1", + "ID": "_nomad-task-07501480-8175-8071-7da6-133bd1ff890f-group-redis-redis-redis", + "JobID": "echo", + "ModifyIndex": 46, + "Namespace": "default", + "NodeID": "6d7f412e-e7ff-2e66-d47b-867b0e9d8726", + "Port": 30826, + "ServiceName": "redis", + "Tags": [ + "traefik.enable=true" + ] + } +] +` + +const hello = ` +[ + { + "Address": "127.0.0.1", + "AllocID": "71a63a80-a98a-93ee-4fd7-73b808577c20", + "CreateIndex": 18, + "Datacenter": "dc1", + "ID": "_nomad-task-71a63a80-a98a-93ee-4fd7-73b808577c20-group-hello-nomad-hello-nomad-http", + "JobID": "echo", + "ModifyIndex": 18, + "Namespace": "default", + "NodeID": "6d7f412e-e7ff-2e66-d47b-867b0e9d8726", + "Port": 20627, + "ServiceName": "hello-nomad", + "Tags": [ + "traefik.enable=true", + "traefik.http.routers.hellon.entrypoints=web", + "traefik.http.routers.hellon.service=hello-nomad" + ] + } +] +` diff --git a/pkg/provider/nomad/tag.go b/pkg/provider/nomad/tag.go new file mode 100644 index 000000000..1345091df --- /dev/null +++ b/pkg/provider/nomad/tag.go @@ -0,0 +1,19 @@ +package nomad + +import ( + "strings" +) + +func tagsToLabels(tags []string, prefix string) map[string]string { + labels := make(map[string]string, len(tags)) + for _, tag := range tags { + if strings.HasPrefix(tag, prefix) { + if parts := strings.SplitN(tag, "=", 2); len(parts) == 2 { + left, right := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) + key := "traefik." + strings.TrimPrefix(left, prefix+".") + labels[key] = right + } + } + } + return labels +} diff --git a/pkg/provider/nomad/tag_test.go b/pkg/provider/nomad/tag_test.go new file mode 100644 index 000000000..e15f5486c --- /dev/null +++ b/pkg/provider/nomad/tag_test.go @@ -0,0 +1,109 @@ +package nomad + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_tagsToLabels(t *testing.T) { + testCases := []struct { + desc string + tags []string + prefix string + expected map[string]string + }{ + { + desc: "no tags", + tags: []string{}, + prefix: "traefik", + expected: map[string]string{}, + }, + { + desc: "minimal global config", + tags: []string{"traefik.enable=false"}, + prefix: "traefik", + expected: map[string]string{ + "traefik.enable": "false", + }, + }, + { + desc: "config with domain", + tags: []string{ + "traefik.enable=true", + "traefik.domain=example.com", + }, + prefix: "traefik", + expected: map[string]string{ + "traefik.enable": "true", + "traefik.domain": "example.com", + }, + }, + { + desc: "config with custom prefix", + tags: []string{ + "custom.enable=true", + "custom.domain=example.com", + }, + prefix: "custom", + expected: map[string]string{ + "traefik.enable": "true", + "traefik.domain": "example.com", + }, + }, + { + desc: "config with spaces in tags", + tags: []string{ + "custom.enable = true", + "custom.domain = example.com", + }, + prefix: "custom", + expected: map[string]string{ + "traefik.enable": "true", + "traefik.domain": "example.com", + }, + }, + { + desc: "with a prefix", + prefix: "test", + tags: []string{ + "test.aaa=01", + "test.bbb=02", + "ccc=03", + "test.ddd=04=to", + }, + expected: map[string]string{ + "traefik.aaa": "01", + "traefik.bbb": "02", + "traefik.ddd": "04=to", + }, + }, + { + desc: "with an empty prefix", + prefix: "", + tags: []string{ + "test.aaa=01", + "test.bbb=02", + "ccc=03", + "test.ddd=04=to", + }, + expected: map[string]string{ + "traefik.test.aaa": "01", + "traefik.test.bbb": "02", + "traefik.ccc": "03", + "traefik.test.ddd": "04=to", + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + labels := tagsToLabels(test.tags, test.prefix) + + assert.Equal(t, test.expected, labels) + }) + } +} diff --git a/webui/src/statics/providers/nomad.svg b/webui/src/statics/providers/nomad.svg new file mode 100755 index 000000000..e71d75007 --- /dev/null +++ b/webui/src/statics/providers/nomad.svg @@ -0,0 +1,7 @@ + + + + + +