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 <michael.matur@gmail.com>
Co-authored-by: romain <romain@containo.us>
Co-authored-by: Fernandez Ludovic <ludovic@containo.us>
This commit is contained in:
Alessandro Chitolina 2020-07-15 16:28:04 +02:00 committed by GitHub
parent 6e4f5821dc
commit 285ded6e49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 4348 additions and 0 deletions

View file

@ -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

View file

@ -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"
```

View file

@ -0,0 +1 @@
- "traefik.enable=true"

View file

@ -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```)

View file

@ -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```)

View file

@ -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"

View file

@ -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

View file

@ -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 `<router_name>`."
??? info "`traefik.http.routers.<router_name>.rule`"
See [rule](../routers/index.md#rule) for more information.
```yaml
traefik.http.routers.myrouter.rule=Host(`example.com`)
```
??? info "`traefik.http.routers.<router_name>.entrypoints`"
See [entry points](../routers/index.md#entrypoints) for more information.
```yaml
traefik.http.routers.myrouter.entrypoints=web,websecure
```
??? info "`traefik.http.routers.<router_name>.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.<router_name>.service`"
See [rule](../routers/index.md#service) for more information.
```yaml
traefik.http.routers.myrouter.service=myservice
```
??? info "`traefik.http.routers.<router_name>.tls`"
See [tls](../routers/index.md#tls) for more information.
```yaml
traefik.http.routers.myrouter>.tls=true
```
??? info "`traefik.http.routers.<router_name>.tls.certresolver`"
See [certResolver](../routers/index.md#certresolver) for more information.
```yaml
traefik.http.routers.myrouter.tls.certresolver=myresolver
```
??? info "`traefik.http.routers.<router_name>.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.<router_name>.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.<router_name>.tls.options`"
See [options](../routers/index.md#options) for more information.
```yaml
traefik.http.routers.myrouter.tls.options=foobar
```
??? info "`traefik.http.routers.<router_name>.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 `<service_name>`."
??? info "`traefik.http.services.<service_name>.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.<service_name>.loadbalancer.server.scheme`"
Overrides the default scheme.
```yaml
traefik.http.services.myservice.loadbalancer.server.scheme=http
```
??? info "`traefik.http.services.<service_name>.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.<service_name>.loadbalancer.healthcheck.headers.<header_name>`"
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.<service_name>.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.<service_name>.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.<service_name>.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.<service_name>.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.<service_name>.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.<service_name>.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.<service_name>.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.<service_name>.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.<service_name>.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.<service_name>.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.<service_name>.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.<service_name>.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.<service_name>.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.<router_name>.entrypoints`"
See [entry points](../routers/index.md#entrypoints_1) for more information.
```yaml
traefik.tcp.routers.mytcprouter.entrypoints=ep1,ep2
```
??? info "`traefik.tcp.routers.<router_name>.rule`"
See [rule](../routers/index.md#rule_1) for more information.
```yaml
traefik.tcp.routers.mytcprouter.rule=HostSNI(`example.com`)
```
??? info "`traefik.tcp.routers.<router_name>.service`"
See [service](../routers/index.md#services) for more information.
```yaml
traefik.tcp.routers.mytcprouter.service=myservice
```
??? info "`traefik.tcp.routers.<router_name>.tls`"
See [TLS](../routers/index.md#tls_1) for more information.
```yaml
traefik.tcp.routers.mytcprouter.tls=true
```
??? info "`traefik.tcp.routers.<router_name>.tls.certresolver`"
See [certResolver](../routers/index.md#certresolver_1) for more information.
```yaml
traefik.tcp.routers.mytcprouter.tls.certresolver=myresolver
```
??? info "`traefik.tcp.routers.<router_name>.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.<router_name>.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.<router_name>.tls.options`"
See [options](../routers/index.md#options_1) for more information.
```yaml
traefik.tcp.routers.mytcprouter.tls.options=mysoptions
```
??? info "`traefik.tcp.routers.<router_name>.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.<service_name>.loadbalancer.server.port`"
Registers a port of the application.
```yaml
traefik.tcp.services.mytcpservice.loadbalancer.server.port=423
```
??? info "`traefik.tcp.services.<service_name>.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.<router_name>.entrypoints`"
See [entry points](../routers/index.md#entrypoints_2) for more information.
```yaml
traefik.udp.routers.myudprouter.entrypoints=ep1,ep2
```
??? info "`traefik.udp.routers.<router_name>.service`"
See [service](../routers/index.md#services_1) for more information.
```yaml
traefik.udp.routers.myudprouter.service=myservice
```
#### UDP Services
??? info "`traefik.udp.services.<service_name>.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`.

View file

@ -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'

1
go.mod
View file

@ -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

View file

@ -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"`

View file

@ -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)
}

View file

@ -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
}
}

339
pkg/provider/ecs/config.go Normal file
View file

@ -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)
}

File diff suppressed because it is too large Load diff

469
pkg/provider/ecs/ecs.go Normal file
View file

@ -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
}

View file

@ -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)
})
}
}

23
pkg/provider/ecs/label.go Normal file
View file

@ -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
}

View file

@ -0,0 +1,36 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" fill="#fff"
fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round">
<style><![CDATA[.B{stroke:none}.C{fill:#9d5025}.D{fill:#f58536}]]></style>
<circle cx="60" cy="60" r="60" fill="#FFFFFF" stroke="none"/>
<use xlink:href="#A" x="20" y="20"/>
<symbol id="A" overflow="visible">
<path
d="M4.282 5.345L0 7.627v64.746l4.282 2.282 16.87-33.306L4.282 5.345zM17.318 17.03l6.306-9.568 28.26 13.464-6.588 1.122L17.32 17.03zm-4.894 46.632l6.94 10.224 32.518-15.04L45.6 57.8l-33.176 5.862z"
class="B C"/>
<path d="M14.258 72.146l-9.976 2.51v-69.3l9.976 2.433v64.368z" class="B D"/>
<path
d="M9.012 2.8L14.26 0l9.882 44.488L14.26 80l-5.247-2.8V2.8zm36.235 54.94l6.635 1.097 6.07-18.115-6.07-19.805-6.635 1.122v35.7z"
class="B C"/>
<g class="D">
<path d="M24.47 76.912L14.26 80V0L24.47 3.1v73.8z" class="B"/>
<path d="M58.836 57.447L14.26 66.322V80l44.577-13.5v-9.064zm.082-35.097l-44.66-9.1V0l44.66 13.577v8.774z"
class="B"/>
<path d="M51.882 11.434l7.165 2.118v52.96l-7.165 2.105V11.434z" class="B"/>
</g>
<path d="M80 33.003l-19.177.97-6.776-.492 19.388-16.74L80 33.003z" fill="#6b3a19" class="B"/>
<path d="M54.047 33.482L73.435 32.3V16.74l-19.388 3.366v13.375z" class="B C"/>
<path d="M35.8 32.172l19.647-21.217 9.988 20.498L45.07 32.84l-9.27-.668z" fill="#6b3a19" class="B"/>
<g class="C">
<path
d="M35.8 32.172l19.647-1.8V10.955L35.8 15.884V32.17zm18.247 14.623l25.953.48-6.565 16.262-19.388-3.366V46.795z"
class="B"/>
<path d="M35.8 48.106l29.635.73-9.988 20.485L35.8 64.406v-16.3z" class="B"/>
</g>
<path
d="M35.8 48.106l19.647 1.803 9.988-1.072-20.365-1.4-9.27.668zm44.2-.832l-19.177-.958-6.776.48 19.388 1.185L80 47.274z"
class="B" fill="#fbbf93"/>
<path
d="M73.435 32.3l6.565.693V18.846l-6.565-2.105V32.3zm-8-.847l-9.988-1.072V10.955l9.988 3.215v17.283zm8 16.527L80 47.274V61.43l-6.565 2.105V47.98zm-8 .857l-9.988 1.072v19.414l9.988-3.202V48.837z"
class="B D"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB