From 285ded6e49f622b0e2eca8ce67d846c0484e7a6c Mon Sep 17 00:00:00 2001 From: Alessandro Chitolina Date: Wed, 15 Jul 2020 16:28:04 +0200 Subject: [PATCH] Add AWS ECS provider * add ecs provider * add ecs docs * fix test after rebase * add provider icon * add missing addProvider call * Fix for review * Fix documentation * Fix for review * Fix documentation * fix ctx usage * autoDiscoverClusters setDefaults false * Fix for review * review: doc. * Fix for review: add ctx in backoff retry * review: linter. Co-authored-by: Michael Co-authored-by: romain Co-authored-by: Fernandez Ludovic --- docs/content/providers/ecs.md | 194 ++ .../reference/dynamic-configuration/ecs.md | 11 + .../reference/dynamic-configuration/ecs.yml | 1 + .../reference/static-configuration/cli-ref.md | 27 + .../reference/static-configuration/env-ref.md | 27 + .../reference/static-configuration/file.toml | 11 + .../reference/static-configuration/file.yaml | 13 + docs/content/routing/providers/ecs.md | 445 +++ docs/mkdocs.yml | 2 + go.mod | 1 + pkg/config/static/static_config.go | 2 + pkg/provider/aggregator/aggregator.go | 4 + pkg/provider/ecs/builder_test.go | 79 + pkg/provider/ecs/config.go | 339 +++ pkg/provider/ecs/config_test.go | 2576 +++++++++++++++++ pkg/provider/ecs/ecs.go | 469 +++ pkg/provider/ecs/ecs_test.go | 88 + pkg/provider/ecs/label.go | 23 + webui/src/statics/providers/ecs.svg | 36 + 19 files changed, 4348 insertions(+) create mode 100644 docs/content/providers/ecs.md create mode 100644 docs/content/reference/dynamic-configuration/ecs.md create mode 100644 docs/content/reference/dynamic-configuration/ecs.yml create mode 100644 docs/content/routing/providers/ecs.md create mode 100644 pkg/provider/ecs/builder_test.go create mode 100644 pkg/provider/ecs/config.go create mode 100644 pkg/provider/ecs/config_test.go create mode 100644 pkg/provider/ecs/ecs.go create mode 100644 pkg/provider/ecs/ecs_test.go create mode 100644 pkg/provider/ecs/label.go create mode 100644 webui/src/statics/providers/ecs.svg diff --git a/docs/content/providers/ecs.md b/docs/content/providers/ecs.md new file mode 100644 index 000000000..f4720640c --- /dev/null +++ b/docs/content/providers/ecs.md @@ -0,0 +1,194 @@ +# Traefik & AWS ECS + +A Story of Labels & Elastic Containers +{: .subtitle } + +Attach labels to your ECS containers and let Traefik do the rest! + +## Configuration Examples + +??? example "Configuring ECS provider" + + Enabling the ECS provider: + + ```toml tab="File (TOML)" + [providers.ecs] + clusters = ["default"] + ``` + + ```yaml tab="File (YAML)" + providers: + ecs: + clusters: + - default + ``` + + ```bash tab="CLI" + --providers.ecs.clusters=default + ``` + +## Policy + +Traefik needs the following policy to read ECS information: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "TraefikECSReadAccess", + "Effect": "Allow", + "Action": [ + "ecs:ListClusters", + "ecs:DescribeClusters", + "ecs:ListTasks", + "ecs:DescribeTasks", + "ecs:DescribeContainerInstances", + "ecs:DescribeTaskDefinition", + "ec2:DescribeInstances" + ], + "Resource": [ + "*" + ] + } + ] +} +``` + +## Provider configuration + +### `autoDiscoverClusters` + +_Optional, Default=false_ + +```toml tab="File (TOML)" +[providers.ecs] + autoDiscoverClusters = true + # ... +``` + +```yaml tab="File (YAML)" +providers: + ecs: + autoDiscoverClusters: true + # ... +``` + +```bash tab="CLI" +--providers.ecs.autoDiscoverClusters=true +# ... +``` + +Search for services in all clusters. +If set to true the configured clusters will be ignored and the clusters will be discovered. +If set to false the services will be discovered only in configured clusters. + +### `exposedByDefault` + +_Optional, Default=true_ + +```toml tab="File (TOML)" +[providers.ecs] + exposedByDefault = false + # ... +``` + +```yaml tab="File (YAML)" +providers: + ecs: + exposedByDefault: false + # ... +``` + +```bash tab="CLI" +--providers.ecs.exposedByDefault=false +# ... +``` + +Expose ECS services by default in Traefik. +If set to false, services that don't have a `traefik.enable=true` label will be ignored from the resulting routing configuration. + +### `defaultRule` + +_Optional, Default=```Host(`{{ normalize .Name }}`)```_ + +```toml tab="File (TOML)" +[providers.ecs] + defaultRule = "Host(`{{ .Name }}.{{ index .Labels \"customLabel\"}}`)" + # ... +``` + +```yaml tab="File (YAML)" +providers: + ecs: + defaultRule: "Host(`{{ .Name }}.{{ index .Labels \"customLabel\"}}`)" + # ... +``` + +```bash tab="CLI" +--providers.ecs.defaultRule=Host(`{{ .Name }}.{{ index .Labels \"customLabel\"}}`) +# ... +``` + +For a given container if no routing rule was defined by a label, it is defined by this defaultRule instead. +It must be a valid [Go template](https://golang.org/pkg/text/template/), +augmented with the [sprig template functions](http://masterminds.github.io/sprig/). +The service name can be accessed as the `Name` identifier, +and the template has access to all the labels defined on this container. + +### `refreshSeconds` + +_Optional, Default=15_ + +```toml tab="File (TOML)" +[providers.ecs] + refreshSeconds = 15 + # ... +``` + +```yaml tab="File (YAML)" +providers: + ecs: + refreshSeconds: 15 + # ... +``` + +```bash tab="CLI" +--providers.ecs.refreshSeconds=15 +# ... +``` + +Polling interval (in seconds). + +### Credentials + +_Optional_ + +```toml tab="File (TOML)" +[providers.ecs] + region = "us-east-1" + accessKeyID = "abc" + secretAccessKey = "123" +``` + +```yaml tab="File (YAML)" +providers: + ecs: + region: us-east-1 + accessKeyID: "abc" + secretAccessKey: "123" + # ... +``` + +```bash tab="CLI" +--providers.ecs.region="us-east-1" +--providers.ecs.accessKeyID="abc" +--providers.ecs.secretAccessKey="123" +# ... +``` + +If `accessKeyID` / `secretAccessKey` is not provided credentials will be resolved in the following order: + +- From environment variables `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN`. +- Shared credentials, determined by `AWS_PROFILE` and `AWS_SHARED_CREDENTIALS_FILE`, defaults to default and `~/.aws/credentials`. +- EC2 instance role or ECS task role diff --git a/docs/content/reference/dynamic-configuration/ecs.md b/docs/content/reference/dynamic-configuration/ecs.md new file mode 100644 index 000000000..aac748b75 --- /dev/null +++ b/docs/content/reference/dynamic-configuration/ecs.md @@ -0,0 +1,11 @@ +# ECS Configuration Reference + +Dynamic configuration with ECS provider +{: .subtitle } + +The labels are case insensitive. + +```yaml +--8<-- "content/reference/dynamic-configuration/ecs.yml" +--8<-- "content/reference/dynamic-configuration/docker-labels.yml" +``` diff --git a/docs/content/reference/dynamic-configuration/ecs.yml b/docs/content/reference/dynamic-configuration/ecs.yml new file mode 100644 index 000000000..23efc00c6 --- /dev/null +++ b/docs/content/reference/dynamic-configuration/ecs.yml @@ -0,0 +1 @@ +- "traefik.enable=true" diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 7afe56d75..e4acb8c1e 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -417,6 +417,33 @@ Use the ip address from the bound port, rather than from the inner network. (Def `--providers.docker.watch`: Watch Docker Swarm events. (Default: ```true```) +`--providers.ecs.accesskeyid`: +The AWS credentials access key to use for making requests + +`--providers.ecs.autodiscoverclusters`: +Auto discover cluster (Default: ```false```) + +`--providers.ecs.clusters`: +ECS Clusters name (Default: ```default```) + +`--providers.ecs.constraints`: +Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container. + +`--providers.ecs.defaultrule`: +Default rule. (Default: ```Host(`{{ normalize .Name }}`)```) + +`--providers.ecs.exposedbydefault`: +Expose services by default (Default: ```true```) + +`--providers.ecs.refreshseconds`: +Polling interval (in seconds) (Default: ```15```) + +`--providers.ecs.region`: +The AWS region to use for requests + +`--providers.ecs.secretaccesskey`: +The AWS credentials access key to use for making requests + `--providers.etcd`: Enable Etcd backend with default settings. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index bafef49ad..7056392cc 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -417,6 +417,33 @@ Use the ip address from the bound port, rather than from the inner network. (Def `TRAEFIK_PROVIDERS_DOCKER_WATCH`: Watch Docker Swarm events. (Default: ```true```) +`TRAEFIK_PROVIDERS_ECS_ACCESSKEYID`: +The AWS credentials access key to use for making requests + +`TRAEFIK_PROVIDERS_ECS_AUTODISCOVERCLUSTERS`: +Auto discover cluster (Default: ```false```) + +`TRAEFIK_PROVIDERS_ECS_CLUSTERS`: +ECS Clusters name (Default: ```default```) + +`TRAEFIK_PROVIDERS_ECS_CONSTRAINTS`: +Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container. + +`TRAEFIK_PROVIDERS_ECS_DEFAULTRULE`: +Default rule. (Default: ```Host(`{{ normalize .Name }}`)```) + +`TRAEFIK_PROVIDERS_ECS_EXPOSEDBYDEFAULT`: +Expose services by default (Default: ```true```) + +`TRAEFIK_PROVIDERS_ECS_REFRESHSECONDS`: +Polling interval (in seconds) (Default: ```15```) + +`TRAEFIK_PROVIDERS_ECS_REGION`: +The AWS region to use for requests + +`TRAEFIK_PROVIDERS_ECS_SECRETACCESSKEY`: +The AWS credentials access key to use for making requests + `TRAEFIK_PROVIDERS_ETCD`: Enable Etcd backend with default settings. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index 2e689e038..50df6eb02 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -151,6 +151,16 @@ [providers.consulCatalog.endpoint.httpAuth] username = "foobar" password = "foobar" + [providers.ecs] + constraints = "foobar" + exposedByDefault = true + refreshSeconds = 42 + defaultRule = "foobar" + clusters = ["foobar", "foobar"] + autoDiscoverClusters = true + region = "foobar" + accessKeyID = "foobar" + secretAccessKey = "foobar" [providers.consul] rootKey = "foobar" endpoints = ["foobar", "foobar"] @@ -233,6 +243,7 @@ [ping] entryPoint = "foobar" manualRouting = true + terminatingStatusCode = 42 [log] level = "foobar" diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index f8eea1ef5..65c46210f 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -161,6 +161,18 @@ providers: httpAuth: username: foobar password: foobar + ecs: + constraints: foobar + exposedByDefault: true + refreshSeconds: 42 + defaultRule: foobar + clusters: + - foobar + - foobar + autoDiscoverClusters: true + region: foobar + accessKeyID: foobar + secretAccessKey: foobar consul: rootKey: foobar endpoints: @@ -250,6 +262,7 @@ metrics: ping: entryPoint: foobar manualRouting: true + terminatingStatusCode: 42 log: level: foobar filePath: foobar diff --git a/docs/content/routing/providers/ecs.md b/docs/content/routing/providers/ecs.md new file mode 100644 index 000000000..23a6ae1a9 --- /dev/null +++ b/docs/content/routing/providers/ecs.md @@ -0,0 +1,445 @@ +# Traefik & ECS + +A Story of Labels & Elastic Containers +{: .subtitle } + +Attach labels to your containers and let Traefik do the rest! + +## Routing Configuration + +!!! info "labels" + + - labels are case insensitive. + - The complete list of labels can be found [the reference page](../../reference/dynamic-configuration/ecs.md) + +### General + +Traefik creates, for each elastic service, a corresponding [service](../services/index.md) and [router](../routers/index.md). + +The Service automatically gets a server per elastic container, and the router gets a default rule attached to it, based on the service name. + +### Routers + +To update the configuration of the Router automatically attached to the service, add labels 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 label ```traefik.http.routers.my-service.rule=Host(`example.com`)```. + +!!! warning "The character `@` is not authorized in the router name ``." + +??? info "`traefik.http.routers..rule`" + + See [rule](../routers/index.md#rule) for more information. + + ```yaml + traefik.http.routers.myrouter.rule=Host(`example.com`) + ``` + +??? info "`traefik.http.routers..entrypoints`" + + See [entry points](../routers/index.md#entrypoints) for more information. + + ```yaml + traefik.http.routers.myrouter.entrypoints=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 labels 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 label `traefik.http.services.{name-of-your-choice}.loadbalancer.passhostheader=false`. + +!!! warning "The character `@` is not authorized in the service name ``." + +??? info "`traefik.http.services..loadbalancer.server.port`" + + Registers a port. + Useful when the 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.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`" + + See [sticky sessions](../services/index.md#sticky-sessions) for more information. + + ```yaml + traefik.http.services.myservice.loadbalancer.sticky=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. + + FlushInterval specifies the flush interval to flush to the client while copying the response body. + + ```yaml + traefik.http.services.myservice.loadbalancer.responseforwarding.flushinterval=10 + ``` + +### Middleware + +You can declare pieces of middleware using labels starting with `traefik.http.middlewares.{name-of-your-choice}.`, followed by the middleware type/options. + +For example, to declare a middleware [`redirectscheme`](../../middlewares/redirectscheme.md) named `my-redirect`, you'd write `traefik.http.middlewares.my-redirect.redirectscheme.scheme: https`. + +More information about available middlewares in the dedicated [middlewares section](../../middlewares/overview.md). + +!!! warning "The character `@` is not authorized in the middleware name." + +??? example "Declaring and Referencing a Middleware" + + ```yaml + # ... + # 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 labels. + +??? 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 elastic 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=mysoptions + ``` + +??? info "`traefik.tcp.routers..tls.passthrough`" + + See [TLS](../routers/index.md#tls_1) for more information. + + ```yaml + traefik.tcp.routers.mytcprouter.tls.passthrough=true + ``` + +#### 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 + ``` + +### 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 elastic 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 ECS service by setting `traefik.enable` to true or false. + +This option overrides the value of `exposedByDefault`. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 986dc19a8..4f54c100c 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -77,6 +77,7 @@ nav: - 'Kubernetes IngressRoute': 'providers/kubernetes-crd.md' - 'Kubernetes Ingress': 'providers/kubernetes-ingress.md' - 'Consul Catalog': 'providers/consul-catalog.md' + - 'ECS': 'providers/ecs.md' - 'Marathon': 'providers/marathon.md' - 'Rancher': 'providers/rancher.md' - 'File': 'providers/file.md' @@ -94,6 +95,7 @@ nav: - 'Kubernetes IngressRoute': 'routing/providers/kubernetes-crd.md' - 'Kubernetes Ingress': 'routing/providers/kubernetes-ingress.md' - 'Consul Catalog': 'routing/providers/consul-catalog.md' + - 'ECS': 'routing/providers/ecs.md' - 'Marathon': 'routing/providers/marathon.md' - 'Rancher': 'routing/providers/rancher.md' - 'KV': 'routing/providers/kv.md' diff --git a/go.mod b/go.mod index 9ab2f801b..762072196 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/VividCortex/gohistogram v1.0.0 // indirect github.com/abbot/go-http-auth v0.0.0-00010101000000-000000000000 github.com/abronan/valkeyrie v0.0.0-20200127174252-ef4277a138cd + github.com/aws/aws-sdk-go v1.30.20 github.com/c0va23/go-proxyprotocol v0.9.1 github.com/cenkalti/backoff/v4 v4.0.0 github.com/containerd/containerd v1.3.2 // indirect diff --git a/pkg/config/static/static_config.go b/pkg/config/static/static_config.go index 91f5e68c6..8ab85aec5 100644 --- a/pkg/config/static/static_config.go +++ b/pkg/config/static/static_config.go @@ -11,6 +11,7 @@ import ( acmeprovider "github.com/containous/traefik/v2/pkg/provider/acme" "github.com/containous/traefik/v2/pkg/provider/consulcatalog" "github.com/containous/traefik/v2/pkg/provider/docker" + "github.com/containous/traefik/v2/pkg/provider/ecs" "github.com/containous/traefik/v2/pkg/provider/file" "github.com/containous/traefik/v2/pkg/provider/kubernetes/crd" "github.com/containous/traefik/v2/pkg/provider/kubernetes/ingress" @@ -170,6 +171,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.Provider `description:"Enable ConsulCatalog backend with default settings." json:"consulCatalog,omitempty" toml:"consulCatalog,omitempty" yaml:"consulCatalog,omitempty"` + Ecs *ecs.Provider `description:"Enable AWS ECS backend with default settings." json:"ecs,omitempty" toml:"ecs,omitempty" yaml:"ecs,omitempty"` Consul *consul.Provider `description:"Enable Consul backend with default settings." json:"consul,omitempty" toml:"consul,omitempty" yaml:"consul,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"` Etcd *etcd.Provider `description:"Enable Etcd backend with default settings." json:"etcd,omitempty" toml:"etcd,omitempty" yaml:"etcd,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"` diff --git a/pkg/provider/aggregator/aggregator.go b/pkg/provider/aggregator/aggregator.go index faa5e784d..8cef74235 100644 --- a/pkg/provider/aggregator/aggregator.go +++ b/pkg/provider/aggregator/aggregator.go @@ -49,6 +49,10 @@ func NewProviderAggregator(conf static.Providers) ProviderAggregator { p.quietAddProvider(conf.Rancher) } + if conf.Ecs != nil { + p.quietAddProvider(conf.Ecs) + } + if conf.ConsulCatalog != nil { p.quietAddProvider(conf.ConsulCatalog) } diff --git a/pkg/provider/ecs/builder_test.go b/pkg/provider/ecs/builder_test.go new file mode 100644 index 000000000..9dd17a995 --- /dev/null +++ b/pkg/provider/ecs/builder_test.go @@ -0,0 +1,79 @@ +package ecs + +import "github.com/aws/aws-sdk-go/service/ecs" + +func instance(ops ...func(*ecsInstance)) ecsInstance { + e := &ecsInstance{ + containerDefinition: &ecs.ContainerDefinition{}, + } + + for _, op := range ops { + op(e) + } + + return *e +} + +func name(name string) func(*ecsInstance) { + return func(e *ecsInstance) { + e.Name = name + } +} + +func id(id string) func(*ecsInstance) { + return func(e *ecsInstance) { + e.ID = id + } +} + +func iMachine(opts ...func(*machine)) func(*ecsInstance) { + return func(e *ecsInstance) { + e.machine = &machine{} + + for _, opt := range opts { + opt(e.machine) + } + } +} + +func mState(state string) func(*machine) { + return func(m *machine) { + m.state = state + } +} + +func mPrivateIP(ip string) func(*machine) { + return func(m *machine) { + m.privateIP = ip + } +} + +func mHealthStatus(status string) func(*machine) { + return func(m *machine) { + m.healthStatus = status + } +} + +func mPorts(opts ...func(*portMapping)) func(*machine) { + return func(m *machine) { + for _, opt := range opts { + p := &portMapping{} + opt(p) + m.ports = append(m.ports, *p) + } + } +} + +func mPort(containerPort, hostPort int32, protocol string) func(*portMapping) { + return func(pm *portMapping) { + pm.containerPort = int64(containerPort) + pm.hostPort = int64(hostPort) + pm.protocol = protocol + } +} + +func labels(labels map[string]string) func(*ecsInstance) { + return func(c *ecsInstance) { + c.Labels = labels + } +} diff --git a/pkg/provider/ecs/config.go b/pkg/provider/ecs/config.go new file mode 100644 index 000000000..c274082b6 --- /dev/null +++ b/pkg/provider/ecs/config.go @@ -0,0 +1,339 @@ +package ecs + +import ( + "context" + "errors" + "fmt" + "net" + "strconv" + "strings" + + "github.com/aws/aws-sdk-go/service/ec2" + + "github.com/containous/traefik/v2/pkg/config/dynamic" + "github.com/containous/traefik/v2/pkg/config/label" + "github.com/containous/traefik/v2/pkg/log" + "github.com/containous/traefik/v2/pkg/provider" + "github.com/containous/traefik/v2/pkg/provider/constraints" + "github.com/docker/go-connections/nat" +) + +func (p *Provider) buildConfiguration(ctx context.Context, instances []ecsInstance) *dynamic.Configuration { + configurations := make(map[string]*dynamic.Configuration) + + for _, instance := range instances { + instanceName := getServiceName(instance) + "-" + instance.ID + ctxContainer := log.With(ctx, log.Str("ecs-instance", instanceName)) + + if !p.filterInstance(ctxContainer, instance) { + continue + } + + logger := log.FromContext(ctxContainer) + + confFromLabel, err := label.DecodeConfiguration(instance.Labels) + if err != nil { + logger.Error(err) + continue + } + + var tcpOrUDP bool + if len(confFromLabel.TCP.Routers) > 0 || len(confFromLabel.TCP.Services) > 0 { + tcpOrUDP = true + + err := p.buildTCPServiceConfiguration(instance, confFromLabel.TCP) + if err != nil { + logger.Error(err) + continue + } + provider.BuildTCPRouterConfiguration(ctxContainer, confFromLabel.TCP) + } + + if len(confFromLabel.UDP.Routers) > 0 || len(confFromLabel.UDP.Services) > 0 { + tcpOrUDP = true + + err := p.buildUDPServiceConfiguration(instance, confFromLabel.UDP) + if err != nil { + logger.Error(err) + continue + } + provider.BuildUDPRouterConfiguration(ctxContainer, confFromLabel.UDP) + } + + if tcpOrUDP && len(confFromLabel.HTTP.Routers) == 0 && + len(confFromLabel.HTTP.Middlewares) == 0 && + len(confFromLabel.HTTP.Services) == 0 { + configurations[instanceName] = confFromLabel + continue + } + + err = p.buildServiceConfiguration(ctxContainer, instance, confFromLabel.HTTP) + if err != nil { + logger.Error(err) + continue + } + + serviceName := getServiceName(instance) + + model := struct { + Name string + Labels map[string]string + }{ + Name: serviceName, + Labels: instance.Labels, + } + + provider.BuildRouterConfiguration(ctx, confFromLabel.HTTP, serviceName, p.defaultRuleTpl, model) + + configurations[instanceName] = confFromLabel + } + + return provider.Merge(ctx, configurations) +} + +func (p *Provider) buildTCPServiceConfiguration(instance ecsInstance, configuration *dynamic.TCPConfiguration) error { + serviceName := getServiceName(instance) + + if len(configuration.Services) == 0 { + configuration.Services = make(map[string]*dynamic.TCPService) + lb := &dynamic.TCPServersLoadBalancer{} + lb.SetDefaults() + configuration.Services[serviceName] = &dynamic.TCPService{ + LoadBalancer: lb, + } + } + + for name, service := range configuration.Services { + err := p.addServerTCP(instance, service.LoadBalancer) + if err != nil { + return fmt.Errorf("service %q error: %w", name, err) + } + } + + return nil +} + +func (p *Provider) buildUDPServiceConfiguration(instance ecsInstance, configuration *dynamic.UDPConfiguration) error { + serviceName := getServiceName(instance) + + if len(configuration.Services) == 0 { + configuration.Services = make(map[string]*dynamic.UDPService) + lb := &dynamic.UDPServersLoadBalancer{} + configuration.Services[serviceName] = &dynamic.UDPService{ + LoadBalancer: lb, + } + } + + for name, service := range configuration.Services { + err := p.addServerUDP(instance, service.LoadBalancer) + if err != nil { + return fmt.Errorf("service %q error: %w", name, err) + } + } + + return nil +} + +func (p *Provider) buildServiceConfiguration(_ context.Context, instance ecsInstance, configuration *dynamic.HTTPConfiguration) error { + serviceName := getServiceName(instance) + + if len(configuration.Services) == 0 { + configuration.Services = make(map[string]*dynamic.Service) + lb := &dynamic.ServersLoadBalancer{} + lb.SetDefaults() + configuration.Services[serviceName] = &dynamic.Service{ + LoadBalancer: lb, + } + } + + for name, service := range configuration.Services { + err := p.addServer(instance, service.LoadBalancer) + if err != nil { + return fmt.Errorf("service %q error: %w", name, err) + } + } + + return nil +} + +func (p *Provider) filterInstance(ctx context.Context, instance ecsInstance) bool { + logger := log.FromContext(ctx) + + if instance.machine == nil { + logger.Debug("Filtering ecs instance with nil machine") + return false + } + + if strings.ToLower(instance.machine.state) != ec2.InstanceStateNameRunning { + logger.Debugf("Filtering ecs instance with an incorrect state %s (%s) (state = %s)", instance.Name, instance.ID, instance.machine.state) + return false + } + + if instance.machine.healthStatus == "UNHEALTHY" { + logger.Debugf("Filtering unhealthy ecs instance %s (%s)", instance.Name, instance.ID) + return false + } + + if len(instance.machine.privateIP) == 0 { + logger.Debugf("Filtering ecs instance without an ip address %s (%s)", instance.Name, instance.ID) + return false + } + + if !instance.ExtraConf.Enable { + logger.Debugf("Filtering disabled ecs instance %s (%s)", instance.Name, instance.ID) + return false + } + + matches, err := constraints.MatchLabels(instance.Labels, p.Constraints) + if err != nil { + logger.Errorf("Error matching constraints expression: %v", err) + return false + } + if !matches { + logger.Debugf("Container pruned by constraint expression: %q", p.Constraints) + return false + } + + return true +} + +func (p *Provider) addServerTCP(instance ecsInstance, loadBalancer *dynamic.TCPServersLoadBalancer) error { + if loadBalancer == nil { + return errors.New("load-balancer is not defined") + } + + var serverPort string + if len(loadBalancer.Servers) > 0 { + serverPort = loadBalancer.Servers[0].Port + loadBalancer.Servers[0].Port = "" + } + + ip, port, err := p.getIPPort(instance, serverPort) + if err != nil { + return err + } + + if len(loadBalancer.Servers) == 0 { + server := dynamic.TCPServer{} + + loadBalancer.Servers = []dynamic.TCPServer{server} + } + + if port == "" { + return errors.New("port is missing") + } + + loadBalancer.Servers[0].Address = net.JoinHostPort(ip, port) + return nil +} + +func (p *Provider) addServerUDP(instance ecsInstance, loadBalancer *dynamic.UDPServersLoadBalancer) error { + if loadBalancer == nil { + return errors.New("load-balancer is not defined") + } + + var serverPort string + if len(loadBalancer.Servers) > 0 { + serverPort = loadBalancer.Servers[0].Port + loadBalancer.Servers[0].Port = "" + } + + ip, port, err := p.getIPPort(instance, serverPort) + if err != nil { + return err + } + + if len(loadBalancer.Servers) == 0 { + server := dynamic.UDPServer{} + + loadBalancer.Servers = []dynamic.UDPServer{server} + } + + if port == "" { + return errors.New("port is missing") + } + + loadBalancer.Servers[0].Address = net.JoinHostPort(ip, port) + return nil +} + +func (p *Provider) addServer(instance ecsInstance, loadBalancer *dynamic.ServersLoadBalancer) error { + if loadBalancer == nil { + return errors.New("load-balancer is not defined") + } + + var serverPort string + if len(loadBalancer.Servers) > 0 { + serverPort = loadBalancer.Servers[0].Port + loadBalancer.Servers[0].Port = "" + } + + ip, port, err := p.getIPPort(instance, serverPort) + if err != nil { + return err + } + + if len(loadBalancer.Servers) == 0 { + server := dynamic.Server{} + server.SetDefaults() + + loadBalancer.Servers = []dynamic.Server{server} + } + + if port == "" { + return errors.New("port is missing") + } + + loadBalancer.Servers[0].URL = fmt.Sprintf("%s://%s", loadBalancer.Servers[0].Scheme, net.JoinHostPort(ip, port)) + loadBalancer.Servers[0].Scheme = "" + + return nil +} + +func (p *Provider) getIPPort(instance ecsInstance, serverPort string) (string, string, error) { + var ip, port string + + ip = p.getIPAddress(instance) + port = getPort(instance, serverPort) + if len(ip) == 0 { + return "", "", fmt.Errorf("unable to find the IP address for the instance %q: the server is ignored", instance.Name) + } + + return ip, port, nil +} + +func (p Provider) getIPAddress(instance ecsInstance) string { + return instance.machine.privateIP +} + +func getPort(instance ecsInstance, serverPort string) string { + if len(serverPort) > 0 { + return serverPort + } + + var ports []nat.Port + for _, port := range instance.machine.ports { + natPort, err := nat.NewPort(port.protocol, strconv.FormatInt(port.hostPort, 10)) + if err != nil { + continue + } + + ports = append(ports, natPort) + } + + less := func(i, j nat.Port) bool { + return i.Int() < j.Int() + } + nat.Sort(ports, less) + + if len(ports) > 0 { + min := ports[0] + return min.Port() + } + + return "" +} + +func getServiceName(instance ecsInstance) string { + return provider.Normalize(instance.Name) +} diff --git a/pkg/provider/ecs/config_test.go b/pkg/provider/ecs/config_test.go new file mode 100644 index 000000000..89bd8195e --- /dev/null +++ b/pkg/provider/ecs/config_test.go @@ -0,0 +1,2576 @@ +package ecs + +import ( + "context" + "testing" + + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/containous/traefik/v2/pkg/config/dynamic" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Int(v int) *int { return &v } +func Bool(v bool) *bool { return &v } + +func TestDefaultRule(t *testing.T) { + testCases := []struct { + desc string + instances []ecsInstance + defaultRule string + expected *dynamic.Configuration + }{ + { + desc: "default rule with no variable", + instances: []ecsInstance{ + instance( + name("Test"), + id("1"), + labels(map[string]string{}), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("10.0.0.1"), + mPorts( + mPort(0, 1337, "TCP"), + ), + ), + ), + }, + defaultRule: "Host(`foo.bar`)", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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(`foo.bar`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.0.0.1:1337", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "default rule with service name", + instances: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{}), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + defaultRule: "Host(`{{ .Name }}.foo.bar`)", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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.foo.bar`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "default rule with label", + instances: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.domain": "foo.bar", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + defaultRule: `Host("{{ .Name }}.{{ index .Labels "traefik.domain" }}")`, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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.foo.bar")`, + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "invalid rule", + instances: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{}), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + defaultRule: `Host("{{ .Toto }}")`, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "undefined rule", + instances: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{}), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + defaultRule: ``, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "default template rule", + instances: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{}), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + defaultRule: DefaultTemplateRule, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + p := Provider{ + ExposedByDefault: true, + DefaultRule: test.defaultRule, + defaultRuleTpl: nil, + } + + err := p.Init() + require.NoError(t, err) + + for i := 0; i < len(test.instances); i++ { + var err error + test.instances[i].ExtraConf, err = p.getConfiguration(test.instances[i]) + require.NoError(t, err) + } + + configuration := p.buildConfiguration(context.Background(), test.instances) + + assert.Equal(t, test.expected, configuration) + }) + } +} + +func Test_buildConfiguration(t *testing.T) { + testCases := []struct { + desc string + containers []ecsInstance + constraints string + expected *dynamic.Configuration + }{ + { + desc: "invalid HTTP service definition", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.http.services.test": "", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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{}, + }, + }, + }, + { + desc: "invalid TCP service definition", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.tcp.services.test": "", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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{}, + }, + }, + }, + { + desc: "invalid UDP service definition", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.udp.services.test": "", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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{}, + }, + }, + }, + { + desc: "one container no label", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{}), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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.wtf`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "two containers no label", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{}), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + instance( + name("Test2"), + labels(map[string]string{}), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.2"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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.wtf`)", + }, + "Test2": { + Service: "Test2", + Rule: "Host(`Test2.traefik.wtf`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + "Test2": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.2:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "two containers with same service name no label", + containers: []ecsInstance{ + instance( + id("1"), + name("Test"), + labels(map[string]string{}), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + instance( + id("2"), + name("Test"), + labels(map[string]string{}), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.2"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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.wtf`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:80", + }, + { + URL: "http://127.0.0.2:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "one container with label (not on server)", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.http.services.Service1.loadbalancer.passhostheader": "true", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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.wtf`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Service1": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "one container with labels", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.http.services.Service1.loadbalancer.passhostheader": "true", + "traefik.http.routers.Router1.rule": "Host(`foo.com`)", + "traefik.http.routers.Router1.service": "Service1", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "one container with rule label", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.http.routers.Router1.rule": "Host(`foo.com`)", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + Routers: map[string]*dynamic.Router{ + "Router1": { + Service: "Test", + Rule: "Host(`foo.com`)", + }, + }, + }, + }, + }, + { + desc: "one container with rule label and one service", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.http.routers.Router1.rule": "Host(`foo.com`)", + "traefik.http.services.Service1.loadbalancer.passhostheader": "true", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "one container with rule label and two services", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.http.routers.Router1.rule": "Host(`foo.com`)", + "traefik.http.services.Service1.loadbalancer.passhostheader": "true", + "traefik.http.services.Service2.loadbalancer.passhostheader": "true", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + "Service2": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "one router, one specified but undefined service -> specified one is assigned, but automatic is created instead", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.http.routers.Router1.rule": "Host(`foo.com`)", + "traefik.http.routers.Router1.service": "Service1", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "two containers with same service name and different passhostheader", + containers: []ecsInstance{ + instance( + name("Test"), + id("1"), + labels(map[string]string{ + "traefik.http.services.Service1.loadbalancer.passhostheader": "true", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + instance( + name("Test"), + id("2"), + labels(map[string]string{ + "traefik.http.services.Service1.loadbalancer.passhostheader": "false", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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.wtf`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + }, + }, + }, + { + desc: "three containers with same service name and different passhostheader", + containers: []ecsInstance{ + instance( + name("Test"), + id("1"), + labels(map[string]string{ + "traefik.http.services.Service1.loadbalancer.passhostheader": "false", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + instance( + name("Test"), + id("2"), + labels(map[string]string{ + "traefik.http.services.Service1.loadbalancer.passhostheader": "true", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + instance( + name("Test"), + id("3"), + labels(map[string]string{ + "traefik.http.services.Service1.loadbalancer.passhostheader": "true", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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.wtf`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + }, + }, + }, + { + desc: "two containers with same service name and same LB methods", + containers: []ecsInstance{ + instance( + name("Test"), + id("1"), + labels(map[string]string{ + "traefik.http.services.Service1.loadbalancer.passhostheader": "true", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + instance( + name("Test"), + id("2"), + labels(map[string]string{ + "traefik.http.services.Service1.loadbalancer.passhostheader": "true", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.2"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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.wtf`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Service1": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:80", + }, + { + URL: "http://127.0.0.2:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "one container with InFlightReq in label (default value)", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.http.middlewares.Middleware1.inflightreq.amount": "42", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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.wtf`)", + }, + }, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "Middleware1": { + InFlightReq: &dynamic.InFlightReq{ + Amount: 42, + }, + }, + }, + }, + }, + }, + { + desc: "two containers with two identical middlewares", + containers: []ecsInstance{ + instance( + name("Test"), + id("1"), + labels(map[string]string{ + "traefik.http.middlewares.Middleware1.inflightreq.amount": "42", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + instance( + name("Test"), + id("2"), + labels(map[string]string{ + "traefik.http.middlewares.Middleware1.inflightreq.amount": "42", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.2"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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.wtf`)", + }, + }, + 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:80", + }, + { + URL: "http://127.0.0.2:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "two containers with two different middlewares with same name", + containers: []ecsInstance{ + instance( + name("Test"), + id("1"), + labels(map[string]string{ + "traefik.http.middlewares.Middleware1.inflightreq.amount": "42", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + instance( + name("Test"), + id("2"), + labels(map[string]string{ + "traefik.http.middlewares.Middleware1.inflightreq.amount": "41", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.2"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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.wtf`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:80", + }, + { + URL: "http://127.0.0.2:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "three containers with different middlewares with same name", + containers: []ecsInstance{ + instance( + name("Test"), + id("1"), + labels(map[string]string{ + "traefik.http.middlewares.Middleware1.inflightreq.amount": "42", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + instance( + name("Test"), + id("2"), + labels(map[string]string{ + "traefik.http.middlewares.Middleware1.inflightreq.amount": "41", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.2"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + instance( + name("Test"), + id("3"), + labels(map[string]string{ + "traefik.http.middlewares.Middleware1.inflightreq.amount": "40", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.3"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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.wtf`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:80", + }, + { + URL: "http://127.0.0.2:80", + }, + { + URL: "http://127.0.0.3:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "two containers with two different routers with same name", + containers: []ecsInstance{ + instance( + name("Test"), + id("1"), + labels(map[string]string{ + "traefik.http.routers.Router1.rule": "Host(`foo.com`)", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + instance( + name("Test"), + id("2"), + labels(map[string]string{ + "traefik.http.routers.Router1.rule": "Host(`bar.com`)", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.2"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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:80", + }, + { + URL: "http://127.0.0.2:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "three containers with different routers with same name", + containers: []ecsInstance{ + instance( + name("Test"), + id("1"), + labels(map[string]string{ + "traefik.http.routers.Router1.rule": "Host(`foo.com`)", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + instance( + name("Test"), + id("2"), + labels(map[string]string{ + "traefik.http.routers.Router1.rule": "Host(`bar.com`)", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.2"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + instance( + name("Test"), + id("3"), + labels(map[string]string{ + "traefik.http.routers.Router1.rule": "Host(`foobar.com`)", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.3"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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:80", + }, + { + URL: "http://127.0.0.2:80", + }, + { + URL: "http://127.0.0.3:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "two containers with two identical routers", + containers: []ecsInstance{ + instance( + name("Test"), + id("1"), + labels(map[string]string{ + "traefik.http.routers.Router1.rule": "Host(`foo.com`)", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + + instance( + name("Test"), + id("2"), + labels(map[string]string{ + "traefik.http.routers.Router1.rule": "Host(`foo.com`)", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.2"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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:80", + }, + { + URL: "http://127.0.0.2:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "two containers with two identical router rules and different service names", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.http.routers.Router1.rule": "Host(`foo.com`)", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + instance( + name("Test2"), + labels(map[string]string{ + "traefik.http.routers.Router1.rule": "Host(`foo.com`)", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.2"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + "Test2": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.2:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "one container with bad label", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.wrong.label": "42", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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.wtf`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "one container with label port", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.http.services.Service1.LoadBalancer.server.scheme": "h2c", + "traefik.http.services.Service1.LoadBalancer.server.port": "8080", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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.wtf`)", + }, + }, + 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), + }, + }, + }, + }, + }, + }, + { + desc: "one container with label port on two services", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.http.services.Service1.LoadBalancer.server.port": "", + "traefik.http.services.Service2.LoadBalancer.server.port": "8080", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + "Service2": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:8080", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "one container without port", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{}), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts(), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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{}, + }, + }, + }, + { + desc: "one container without port with middleware", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.http.middlewares.Middleware1.inflightreq.amount": "42", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts(), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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{}, + }, + }, + }, + { + desc: "one container with traefik.enable false", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.enable": "false", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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{}, + }, + }, + }, + { + desc: "one container not healthy", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.enable": "false", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mHealthStatus("UNHEALTHY"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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{}, + }, + }, + }, + { + desc: "one container not running", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.enable": "false", + }), + iMachine( + mState(ec2.InstanceStateNamePending), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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{}, + }, + }, + }, + { + desc: "one container with non matching constraints", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.tags": "foo", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + constraints: `Label("traefik.tags", "bar")`, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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{}, + }, + }, + }, + { + desc: "one container with matching constraints", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.tags": "foo", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + constraints: `Label("traefik.tags", "foo")`, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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.wtf`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "Middlewares used in router", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.http.middlewares.Middleware1.basicauth.users": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + "traefik.http.routers.Test.middlewares": "Middleware1", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + 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.wtf`)", + 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:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "tcp with label", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.tcp.routers.foo.rule": "HostSNI(`foo.bar`)", + "traefik.tcp.routers.foo.tls": "true", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{ + "foo": { + Service: "Test", + Rule: "HostSNI(`foo.bar`)", + TLS: &dynamic.RouterTCPTLSConfig{}, + }, + }, + Services: map[string]*dynamic.TCPService{ + "Test": { + 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{}, + }, + }, + }, + { + desc: "udp with label", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.udp.routers.foo.entrypoints": "mydns", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "udp"), + ), + ), + ), + }, + 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:80", + }, + }, + }, + }, + }, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + }, + }, + }, + { + desc: "tcp with label without rule", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.tcp.routers.foo.tls": "true", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{ + "Test": { + 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{}, + }, + }, + }, + { + desc: "tcp with label and port", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.tcp.routers.foo.rule": "HostSNI(`foo.bar`)", + "traefik.tcp.routers.foo.tls.options": "foo", + "traefik.tcp.services.foo.loadbalancer.server.port": "8080", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{ + "foo": { + Service: "foo", + Rule: "HostSNI(`foo.bar`)", + TLS: &dynamic.RouterTCPTLSConfig{ + Options: "foo", + }, + }, + }, + Services: map[string]*dynamic.TCPService{ + "foo": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{ + Servers: []dynamic.TCPServer{ + { + Address: "127.0.0.1:8080", + }, + }, + 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{}, + }, + }, + }, + { + desc: "udp with label and port", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.udp.routers.foo.entrypoints": "mydns", + "traefik.udp.services.foo.loadbalancer.server.port": "8080", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "udp"), + ), + ), + ), + }, + 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:8080", + }, + }, + }, + }, + }, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + }, + }, + }, + { + desc: "udp with label and port and http service", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.udp.routers.foo.entrypoints": "mydns", + "traefik.udp.services.foo.loadbalancer.server.port": "8080", + "traefik.http.services.Service1.loadbalancer.passhostheader": "true", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + instance( + name("Test"), + id("2"), + labels(map[string]string{ + "traefik.udp.routers.foo.entrypoints": "mydns", + "traefik.udp.services.foo.loadbalancer.server.port": "8080", + "traefik.http.services.Service1.loadbalancer.passhostheader": "true", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.2"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + 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:8080", + }, + { + Address: "127.0.0.2:8080", + }, + }, + }, + }, + }, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test": { + Service: "Service1", + Rule: "Host(`Test.traefik.wtf`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Service1": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1:80", + }, + { + URL: "http://127.0.0.2:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "udp with label for tcp service", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.udp.services.foo.loadbalancer.server.port": "8080", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + 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:8080", + }, + }, + }, + }, + }, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + }, + }, + }, + { + desc: "tcp with label for tcp service, with termination delay", + containers: []ecsInstance{ + instance( + name("Test"), + labels(map[string]string{ + "traefik.tcp.services.foo.loadbalancer.server.port": "8080", + "traefik.tcp.services.foo.loadbalancer.terminationdelay": "200", + }), + iMachine( + mState(ec2.InstanceStateNameRunning), + mPrivateIP("127.0.0.1"), + mPorts( + mPort(0, 80, "tcp"), + ), + ), + ), + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{ + "foo": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{ + Servers: []dynamic.TCPServer{ + { + Address: "127.0.0.1:8080", + }, + }, + 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{}, + }, + }, + }, + } + + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + p := Provider{ + ExposedByDefault: true, + DefaultRule: "Host(`{{ normalize .Name }}.traefik.wtf`)", + } + p.Constraints = test.constraints + + err := p.Init() + require.NoError(t, err) + + for i := 0; i < len(test.containers); i++ { + var err error + test.containers[i].ExtraConf, err = p.getConfiguration(test.containers[i]) + require.NoError(t, err) + } + + configuration := p.buildConfiguration(context.Background(), test.containers) + + assert.Equal(t, test.expected, configuration) + }) + } +} diff --git a/pkg/provider/ecs/ecs.go b/pkg/provider/ecs/ecs.go new file mode 100644 index 000000000..08549d398 --- /dev/null +++ b/pkg/provider/ecs/ecs.go @@ -0,0 +1,469 @@ +package ecs + +import ( + "context" + "fmt" + "strings" + "text/template" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/defaults" + "github.com/aws/aws-sdk-go/aws/ec2metadata" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ecs" + + "github.com/cenkalti/backoff/v4" + "github.com/patrickmn/go-cache" + + "github.com/containous/traefik/v2/pkg/config/dynamic" + "github.com/containous/traefik/v2/pkg/job" + "github.com/containous/traefik/v2/pkg/log" + "github.com/containous/traefik/v2/pkg/provider" + "github.com/containous/traefik/v2/pkg/safe" +) + +// Provider holds configurations of the provider. +type Provider struct { + Constraints string `description:"Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container." json:"constraints,omitempty" toml:"constraints,omitempty" yaml:"constraints,omitempty" export:"true"` + ExposedByDefault bool `description:"Expose services by default" json:"exposedByDefault,omitempty" toml:"exposedByDefault,omitempty" yaml:"exposedByDefault,omitempty" export:"true"` + RefreshSeconds int `description:"Polling interval (in seconds)" json:"refreshSeconds,omitempty" toml:"refreshSeconds,omitempty" yaml:"refreshSeconds,omitempty" export:"true"` + DefaultRule string `description:"Default rule." json:"defaultRule,omitempty" toml:"defaultRule,omitempty" yaml:"defaultRule,omitempty"` + + // Provider lookup parameters. + Clusters []string `description:"ECS Clusters name" json:"clusters,omitempty" toml:"clusters,omitempty" yaml:"clusters,omitempty" export:"true"` + AutoDiscoverClusters bool `description:"Auto discover cluster" json:"autoDiscoverClusters,omitempty" toml:"autoDiscoverClusters,omitempty" yaml:"autoDiscoverClusters,omitempty" export:"true"` + Region string `description:"The AWS region to use for requests" json:"region,omitempty" toml:"region,omitempty" yaml:"region,omitempty" export:"true"` + AccessKeyID string `description:"The AWS credentials access key to use for making requests" json:"accessKeyID,omitempty" toml:"accessKeyID,omitempty" yaml:"accessKeyID,omitempty"` + SecretAccessKey string `description:"The AWS credentials access key to use for making requests" json:"secretAccessKey,omitempty" toml:"secretAccessKey,omitempty" yaml:"secretAccessKey,omitempty"` + defaultRuleTpl *template.Template +} + +type ecsInstance struct { + Name string + ID string + containerDefinition *ecs.ContainerDefinition + machine *machine + Labels map[string]string + ExtraConf configuration +} + +type portMapping struct { + containerPort int64 + hostPort int64 + protocol string +} + +type machine struct { + state string + privateIP string + ports []portMapping + healthStatus string +} + +type awsClient struct { + ecs *ecs.ECS + ec2 *ec2.EC2 +} + +// DefaultTemplateRule The default template for the default rule. +const DefaultTemplateRule = "Host(`{{ normalize .Name }}`)" + +var ( + _ provider.Provider = (*Provider)(nil) + existingTaskDefCache = cache.New(30*time.Minute, 5*time.Minute) +) + +// SetDefaults sets the default values. +func (p *Provider) SetDefaults() { + p.Clusters = []string{"default"} + p.AutoDiscoverClusters = false + p.ExposedByDefault = true + p.RefreshSeconds = 15 + p.DefaultRule = DefaultTemplateRule +} + +// Init the provider. +func (p *Provider) Init() error { + defaultRuleTpl, err := provider.MakeDefaultRuleTemplate(p.DefaultRule, nil) + if err != nil { + return fmt.Errorf("error while parsing default rule: %w", err) + } + + p.defaultRuleTpl = defaultRuleTpl + return nil +} + +func (p *Provider) createClient(logger log.Logger) (*awsClient, error) { + sess, err := session.NewSession() + if err != nil { + return nil, err + } + + ec2meta := ec2metadata.New(sess) + if p.Region == "" { + logger.Infoln("No EC2 region provided, querying instance metadata endpoint...") + identity, err := ec2meta.GetInstanceIdentityDocument() + if err != nil { + return nil, err + } + p.Region = identity.Region + } + + cfg := &aws.Config{ + Region: &p.Region, + Credentials: credentials.NewChainCredentials( + []credentials.Provider{ + &credentials.StaticProvider{ + Value: credentials.Value{ + AccessKeyID: p.AccessKeyID, + SecretAccessKey: p.SecretAccessKey, + }, + }, + &credentials.EnvProvider{}, + &credentials.SharedCredentialsProvider{}, + defaults.RemoteCredProvider(*(defaults.Config()), defaults.Handlers()), + }), + } + + cfg.WithLogger(aws.LoggerFunc(func(args ...interface{}) { + logger.Debug(args...) + })) + + return &awsClient{ + ecs.New(sess, cfg), + ec2.New(sess, cfg), + }, nil +} + +// Provide configuration to traefik from ECS. +func (p Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error { + pool.GoCtx(func(routineCtx context.Context) { + ctxLog := log.With(routineCtx, log.Str(log.ProviderName, "ecs")) + logger := log.FromContext(ctxLog) + + operation := func() error { + awsClient, err := p.createClient(logger) + if err != nil { + return err + } + + configuration, err := p.loadECSConfig(ctxLog, awsClient) + if err != nil { + return err + } + + configurationChan <- dynamic.Message{ + ProviderName: "ecs", + Configuration: configuration, + } + + reload := time.NewTicker(time.Second * time.Duration(p.RefreshSeconds)) + defer reload.Stop() + + for { + select { + case <-reload.C: + configuration, err := p.loadECSConfig(ctxLog, awsClient) + if err != nil { + logger.Errorf("Failed to load ECS configuration, error %s", err) + return err + } + + configurationChan <- dynamic.Message{ + ProviderName: "ecs", + Configuration: configuration, + } + case <-routineCtx.Done(): + return nil + } + } + } + + notify := func(err error, time time.Duration) { + logger.Errorf("Provider connection error %+v, retrying in %s", err, time) + } + err := backoff.RetryNotify(safe.OperationWithRecover(operation), backoff.WithContext(job.NewBackOff(backoff.NewExponentialBackOff()), routineCtx), notify) + if err != nil { + logger.Errorf("Cannot connect to Provider api %+v", err) + } + }) + + return nil +} + +// Find all running Provider tasks in a cluster, also collect the task definitions (for docker labels) +// and the EC2 instance data. +func (p *Provider) listInstances(ctx context.Context, client *awsClient) ([]ecsInstance, error) { + logger := log.FromContext(ctx) + + var clustersArn []*string + var clusters []string + + if p.AutoDiscoverClusters { + input := &ecs.ListClustersInput{} + for { + result, err := client.ecs.ListClusters(input) + if err != nil { + return nil, err + } + if result != nil { + clustersArn = append(clustersArn, result.ClusterArns...) + input.NextToken = result.NextToken + if result.NextToken == nil { + break + } + } else { + break + } + } + for _, cArn := range clustersArn { + clusters = append(clusters, *cArn) + } + } else { + clusters = p.Clusters + } + + var instances []ecsInstance + + logger.Debugf("ECS Clusters: %s", clusters) + for _, c := range clusters { + input := &ecs.ListTasksInput{ + Cluster: &c, + DesiredStatus: aws.String(ecs.DesiredStatusRunning), + } + + tasks := make(map[string]*ecs.Task) + err := client.ecs.ListTasksPagesWithContext(ctx, input, func(page *ecs.ListTasksOutput, lastPage bool) bool { + if len(page.TaskArns) > 0 { + resp, err := client.ecs.DescribeTasksWithContext(ctx, &ecs.DescribeTasksInput{ + Tasks: page.TaskArns, + Cluster: &c, + }) + if err != nil { + logger.Errorf("Unable to describe tasks for %v", page.TaskArns) + } else { + for _, t := range resp.Tasks { + if aws.StringValue(t.LastStatus) == ecs.DesiredStatusRunning { + tasks[aws.StringValue(t.TaskArn)] = t + } + } + } + } + return !lastPage + }) + if err != nil { + logger.Error("Unable to list tasks") + return nil, err + } + + // Skip to the next cluster if there are no tasks found on + // this cluster. + if len(tasks) == 0 { + continue + } + + ec2Instances, err := p.lookupEc2Instances(ctx, client, &c, tasks) + if err != nil { + return nil, err + } + + taskDefinitions, err := p.lookupTaskDefinitions(ctx, client, tasks) + if err != nil { + return nil, err + } + + for key, task := range tasks { + containerInstance := ec2Instances[aws.StringValue(task.ContainerInstanceArn)] + taskDef := taskDefinitions[key] + + for _, container := range task.Containers { + var containerDefinition *ecs.ContainerDefinition + for _, def := range taskDef.ContainerDefinitions { + if aws.StringValue(container.Name) == aws.StringValue(def.Name) { + containerDefinition = def + break + } + } + + if containerDefinition == nil { + logger.Debugf("Unable to find container definition for %s", aws.StringValue(container.Name)) + continue + } + + var mach *machine + if len(task.Attachments) != 0 { + var ports []portMapping + for _, mapping := range containerDefinition.PortMappings { + if mapping != nil { + protocol := "TCP" + if aws.StringValue(mapping.Protocol) == "udp" { + protocol = "UDP" + } + + ports = append(ports, portMapping{ + hostPort: aws.Int64Value(mapping.HostPort), + containerPort: aws.Int64Value(mapping.ContainerPort), + protocol: protocol, + }) + } + } + mach = &machine{ + privateIP: aws.StringValue(container.NetworkInterfaces[0].PrivateIpv4Address), + ports: ports, + state: aws.StringValue(task.LastStatus), + healthStatus: aws.StringValue(task.HealthStatus), + } + } else { + if containerInstance == nil { + logger.Errorf("Unable to find container instance information for %s", aws.StringValue(container.Name)) + continue + } + + var ports []portMapping + for _, mapping := range container.NetworkBindings { + if mapping != nil { + ports = append(ports, portMapping{ + hostPort: aws.Int64Value(mapping.HostPort), + containerPort: aws.Int64Value(mapping.ContainerPort), + }) + } + } + mach = &machine{ + privateIP: aws.StringValue(containerInstance.PrivateIpAddress), + ports: ports, + state: aws.StringValue(containerInstance.State.Name), + } + } + + instance := ecsInstance{ + Name: fmt.Sprintf("%s-%s", strings.Replace(aws.StringValue(task.Group), ":", "-", 1), *container.Name), + ID: key[len(key)-12:], + containerDefinition: containerDefinition, + machine: mach, + Labels: aws.StringValueMap(containerDefinition.DockerLabels), + } + + extraConf, err := p.getConfiguration(instance) + if err != nil { + log.FromContext(ctx).Errorf("Skip container %s: %w", getServiceName(instance), err) + continue + } + instance.ExtraConf = extraConf + + instances = append(instances, instance) + } + } + } + + return instances, nil +} + +func (p *Provider) loadECSConfig(ctx context.Context, client *awsClient) (*dynamic.Configuration, error) { + instances, err := p.listInstances(ctx, client) + if err != nil { + return nil, err + } + + return p.buildConfiguration(ctx, instances), nil +} + +func (p *Provider) lookupEc2Instances(ctx context.Context, client *awsClient, clusterName *string, ecsDatas map[string]*ecs.Task) (map[string]*ec2.Instance, error) { + logger := log.FromContext(ctx) + instanceIds := make(map[string]string) + ec2Instances := make(map[string]*ec2.Instance) + + var containerInstancesArns []*string + var instanceArns []*string + + for _, task := range ecsDatas { + if task.ContainerInstanceArn != nil { + containerInstancesArns = append(containerInstancesArns, task.ContainerInstanceArn) + } + } + + for _, arns := range p.chunkIDs(containerInstancesArns) { + resp, err := client.ecs.DescribeContainerInstancesWithContext(ctx, &ecs.DescribeContainerInstancesInput{ + ContainerInstances: arns, + Cluster: clusterName, + }) + if err != nil { + logger.Errorf("Unable to describe container instances: %v", err) + return nil, err + } + + for _, container := range resp.ContainerInstances { + instanceIds[aws.StringValue(container.Ec2InstanceId)] = aws.StringValue(container.ContainerInstanceArn) + instanceArns = append(instanceArns, container.Ec2InstanceId) + } + } + + if len(instanceArns) > 0 { + for _, ids := range p.chunkIDs(instanceArns) { + input := &ec2.DescribeInstancesInput{ + InstanceIds: ids, + } + + err := client.ec2.DescribeInstancesPagesWithContext(ctx, input, func(page *ec2.DescribeInstancesOutput, lastPage bool) bool { + if len(page.Reservations) > 0 { + for _, r := range page.Reservations { + for _, i := range r.Instances { + if i.InstanceId != nil { + ec2Instances[instanceIds[aws.StringValue(i.InstanceId)]] = i + } + } + } + } + return !lastPage + }) + if err != nil { + logger.Errorf("Unable to describe instances: %v", err) + return nil, err + } + } + } + + return ec2Instances, nil +} + +func (p *Provider) lookupTaskDefinitions(ctx context.Context, client *awsClient, taskDefArns map[string]*ecs.Task) (map[string]*ecs.TaskDefinition, error) { + logger := log.FromContext(ctx) + taskDef := make(map[string]*ecs.TaskDefinition) + + for arn, task := range taskDefArns { + if definition, ok := existingTaskDefCache.Get(arn); ok { + taskDef[arn] = definition.(*ecs.TaskDefinition) + logger.Debugf("Found cached task definition for %s. Skipping the call", arn) + } else { + resp, err := client.ecs.DescribeTaskDefinitionWithContext(ctx, &ecs.DescribeTaskDefinitionInput{ + TaskDefinition: task.TaskDefinitionArn, + }) + if err != nil { + logger.Errorf("Unable to describe task definition: %v", err) + return nil, err + } + + taskDef[arn] = resp.TaskDefinition + existingTaskDefCache.Set(arn, resp.TaskDefinition, cache.DefaultExpiration) + } + } + return taskDef, nil +} + +// chunkIDs ECS expects no more than 100 parameters be passed to a API call; +// thus, pack each string into an array capped at 100 elements. +func (p *Provider) chunkIDs(ids []*string) [][]*string { + var chuncked [][]*string + for i := 0; i < len(ids); i += 100 { + var sliceEnd int + if i+100 < len(ids) { + sliceEnd = i + 100 + } else { + sliceEnd = len(ids) + } + chuncked = append(chuncked, ids[i:sliceEnd]) + } + return chuncked +} diff --git a/pkg/provider/ecs/ecs_test.go b/pkg/provider/ecs/ecs_test.go new file mode 100644 index 000000000..f55510305 --- /dev/null +++ b/pkg/provider/ecs/ecs_test.go @@ -0,0 +1,88 @@ +package ecs + +import ( + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/stretchr/testify/assert" +) + +func TestChunkIDs(t *testing.T) { + provider := &Provider{} + + testCases := []struct { + desc string + count int + expected []int + }{ + { + desc: "0 element", + count: 0, + expected: []int(nil), + }, + { + desc: "1 element", + count: 1, + expected: []int{1}, + }, + { + desc: "99 elements, 1 chunk", + count: 99, + expected: []int{99}, + }, + { + desc: "100 elements, 1 chunk", + count: 100, + expected: []int{100}, + }, + { + desc: "101 elements, 2 chunks", + count: 101, + expected: []int{100, 1}, + }, + { + desc: "199 elements, 2 chunks", + count: 199, + expected: []int{100, 99}, + }, + { + desc: "200 elements, 2 chunks", + count: 200, + expected: []int{100, 100}, + }, + { + desc: "201 elements, 3 chunks", + count: 201, + expected: []int{100, 100, 1}, + }, + { + desc: "555 elements, 5 chunks", + count: 555, + expected: []int{100, 100, 100, 100, 100, 55}, + }, + { + desc: "1001 elements, 11 chunks", + count: 1001, + expected: []int{100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 1}, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + var IDs []*string + for v := 0; v < test.count; v++ { + IDs = append(IDs, aws.String("a")) + } + + var outCount []int + for _, el := range provider.chunkIDs(IDs) { + outCount = append(outCount, len(el)) + } + + assert.Equal(t, test.expected, outCount) + }) + } +} diff --git a/pkg/provider/ecs/label.go b/pkg/provider/ecs/label.go new file mode 100644 index 000000000..d73a3703f --- /dev/null +++ b/pkg/provider/ecs/label.go @@ -0,0 +1,23 @@ +package ecs + +import ( + "github.com/containous/traefik/v2/pkg/config/label" +) + +// configuration Contains information from the labels that are globals (not related to the dynamic configuration) or specific to the provider. +type configuration struct { + Enable bool +} + +func (p *Provider) getConfiguration(instance ecsInstance) (configuration, error) { + conf := configuration{ + Enable: p.ExposedByDefault, + } + + err := label.Decode(instance.Labels, &conf, "traefik.ecs.", "traefik.enable") + if err != nil { + return configuration{}, err + } + + return conf, nil +} diff --git a/webui/src/statics/providers/ecs.svg b/webui/src/statics/providers/ecs.svg new file mode 100644 index 000000000..aad9305b5 --- /dev/null +++ b/webui/src/statics/providers/ecs.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + +