Merge pull request #1088 from lpetre/amazon_ecs_provider
Add an ECS provider
This commit is contained in:
commit
fce32ea5c7
10 changed files with 820 additions and 4 deletions
|
@ -50,6 +50,7 @@ type GlobalConfiguration struct {
|
|||
Kubernetes *provider.Kubernetes `description:"Enable Kubernetes backend"`
|
||||
Mesos *provider.Mesos `description:"Enable Mesos backend"`
|
||||
Eureka *provider.Eureka `description:"Enable Eureka backend"`
|
||||
ECS *provider.ECS `description:"Enable ECS backend"`
|
||||
}
|
||||
|
||||
// DefaultEntryPoints holds default entry points
|
||||
|
@ -391,6 +392,14 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
|
|||
defaultMesos.ExposedByDefault = true
|
||||
defaultMesos.Constraints = types.Constraints{}
|
||||
|
||||
//default ECS
|
||||
var defaultECS provider.ECS
|
||||
defaultECS.Watch = true
|
||||
defaultECS.ExposedByDefault = true
|
||||
defaultECS.RefreshSeconds = 15
|
||||
defaultECS.Cluster = "default"
|
||||
defaultECS.Constraints = types.Constraints{}
|
||||
|
||||
defaultConfiguration := GlobalConfiguration{
|
||||
Docker: &defaultDocker,
|
||||
File: &defaultFile,
|
||||
|
@ -403,6 +412,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
|
|||
Boltdb: &defaultBoltDb,
|
||||
Kubernetes: &defaultKubernetes,
|
||||
Mesos: &defaultMesos,
|
||||
ECS: &defaultECS,
|
||||
Retry: &Retry{},
|
||||
}
|
||||
return &TraefikConfiguration{
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
|
||||
Træfɪk is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease.
|
||||
It supports several backends ([Docker](https://www.docker.com/), [Swarm](https://docs.docker.com/swarm), [Mesos/Marathon](https://mesosphere.github.io/marathon/), [Consul](https://www.consul.io/), [Etcd](https://coreos.com/etcd/), [Zookeeper](https://zookeeper.apache.org), [BoltDB](https://github.com/boltdb/bolt), Rest API, file...) to manage its configuration automatically and dynamically.
|
||||
It supports several backends ([Docker](https://www.docker.com/), [Swarm](https://docs.docker.com/swarm), [Mesos/Marathon](https://mesosphere.github.io/marathon/), [Consul](https://www.consul.io/), [Etcd](https://coreos.com/etcd/), [Zookeeper](https://zookeeper.apache.org), [BoltDB](https://github.com/boltdb/bolt), [Amazon ECS](https://aws.amazon.com/ecs/), Rest API, file...) to manage its configuration automatically and dynamically.
|
||||
|
||||
## Overview
|
||||
|
||||
|
|
75
docs/toml.md
75
docs/toml.md
|
@ -1347,3 +1347,78 @@ delay = "1m"
|
|||
```
|
||||
|
||||
Please refer to the [Key Value storage structure](/user-guide/kv-config/#key-value-storage-structure) section to get documentation on traefik KV structure.
|
||||
|
||||
|
||||
## ECS backend
|
||||
|
||||
Træfɪk can be configured to use Amazon ECS as a backend configuration:
|
||||
|
||||
|
||||
```toml
|
||||
################################################################
|
||||
# ECS configuration backend
|
||||
################################################################
|
||||
|
||||
# Enable ECS configuration backend
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
[ecs]
|
||||
|
||||
# ECS Cluster Name
|
||||
#
|
||||
# Optional
|
||||
# Default: "default"
|
||||
#
|
||||
Cluster = "default"
|
||||
|
||||
# Enable watch ECS changes
|
||||
#
|
||||
# Optional
|
||||
# Default: true
|
||||
#
|
||||
Watch = true
|
||||
|
||||
# Polling interval (in seconds)
|
||||
#
|
||||
# Optional
|
||||
# Default: 15
|
||||
#
|
||||
RefreshSeconds = 15
|
||||
|
||||
# Expose ECS services by default in traefik
|
||||
#
|
||||
# Optional
|
||||
# Default: true
|
||||
#
|
||||
ExposedByDefault = false
|
||||
|
||||
# Region to use when connecting to AWS
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# Region = "us-east-1"
|
||||
|
||||
# AccessKeyID to use when connecting to AWS
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# AccessKeyID = "abc"
|
||||
|
||||
# SecretAccessKey to use when connecting to AWS
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# SecretAccessKey = "123"
|
||||
|
||||
```
|
||||
|
||||
Labels can be used on task containers to override default behaviour:
|
||||
|
||||
- `traefik.protocol=https`: override the default `http` protocol
|
||||
- `traefik.weight=10`: assign this weight to the container
|
||||
- `traefik.enable=false`: disable this container in Træfɪk
|
||||
- `traefik.frontend.rule=Host:test.traefik.io`: override the default frontend rule (Default: `Host:{containerName}.{domain}`).
|
||||
- `traefik.frontend.passHostHeader=true`: forward client `Host` header to the backend.
|
||||
- `traefik.frontend.priority=10`: override default frontend priority
|
||||
- `traefik.frontend.entryPoints=http,https`: assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`.
|
||||
|
|
11
glide.lock
generated
11
glide.lock
generated
|
@ -1,5 +1,5 @@
|
|||
hash: 2a2f73984ec9cccd62220c0eba5ddc82dec96b0044ffed48b25d964e2f3eee05
|
||||
updated: 2017-02-05T17:22:02.550162979+01:00
|
||||
hash: a0b0abed2162e490cbe75a6a36ebaaf39e748ee80e419e879e7253679a0bc134
|
||||
updated: 2017-02-05T18:09:09.856588042Z
|
||||
imports:
|
||||
- name: bitbucket.org/ww/goautoneg
|
||||
version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675
|
||||
|
@ -35,12 +35,17 @@ imports:
|
|||
- aws/session
|
||||
- aws/signer/v4
|
||||
- private/protocol
|
||||
- private/protocol/ec2query
|
||||
- private/protocol/json/jsonutil
|
||||
- private/protocol/jsonrpc
|
||||
- private/protocol/query
|
||||
- private/protocol/query/queryutil
|
||||
- private/protocol/rest
|
||||
- private/protocol/restxml
|
||||
- private/protocol/xml/xmlutil
|
||||
- private/waiter
|
||||
- service/ec2
|
||||
- service/ecs
|
||||
- service/route53
|
||||
- service/sts
|
||||
- name: github.com/Azure/azure-sdk-for-go
|
||||
|
@ -735,7 +740,7 @@ testImports:
|
|||
subpackages:
|
||||
- specs-go
|
||||
- name: github.com/pkg/errors
|
||||
version: 248dadf4e9068a0b3e79f02ed0a610d935de5302
|
||||
version: 01fa4104b9c248c8945d14d9f128454d5b28d595
|
||||
- name: github.com/vbatts/tar-split
|
||||
version: 6810cedb21b2c3d0b9bb8f9af12ff2dc7a2f14df
|
||||
subpackages:
|
||||
|
|
|
@ -119,7 +119,15 @@ import:
|
|||
- package: github.com/aws/aws-sdk-go
|
||||
version: v1.6.18
|
||||
subpackages:
|
||||
- aws
|
||||
- aws/credentials
|
||||
- aws/defaults
|
||||
- aws/ec2metadata
|
||||
- aws/endpoints
|
||||
- aws/request
|
||||
- aws/session
|
||||
- service/ec2
|
||||
- service/ecs
|
||||
- package: cloud.google.com/go
|
||||
version: v0.6.0
|
||||
subpackages:
|
||||
|
|
414
provider/ecs.go
Normal file
414
provider/ecs.go
Normal file
|
@ -0,0 +1,414 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/ty/fun"
|
||||
"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/request"
|
||||
"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/cenk/backoff"
|
||||
"github.com/containous/traefik/job"
|
||||
"github.com/containous/traefik/log"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
)
|
||||
|
||||
var _ Provider = (*ECS)(nil)
|
||||
|
||||
// ECS holds configurations of the ECS provider.
|
||||
type ECS struct {
|
||||
BaseProvider `mapstructure:",squash"`
|
||||
|
||||
Domain string `description:"Default domain used"`
|
||||
ExposedByDefault bool `description:"Expose containers by default"`
|
||||
RefreshSeconds int `description:"Polling interval (in seconds)"`
|
||||
|
||||
// ECS lookup parameters
|
||||
Cluster string `description:"ECS Cluster Name"`
|
||||
Region string `description:"The AWS region to use for requests"`
|
||||
AccessKeyID string `description:"The AWS credentials access key to use for making requests"`
|
||||
SecretAccessKey string `description:"The AWS credentials access key to use for making requests"`
|
||||
}
|
||||
|
||||
type ecsInstance struct {
|
||||
Name string
|
||||
ID string
|
||||
task *ecs.Task
|
||||
taskDefinition *ecs.TaskDefinition
|
||||
container *ecs.Container
|
||||
containerDefinition *ecs.ContainerDefinition
|
||||
machine *ec2.Instance
|
||||
}
|
||||
|
||||
type awsClient struct {
|
||||
ecs *ecs.ECS
|
||||
ec2 *ec2.EC2
|
||||
}
|
||||
|
||||
func (provider *ECS) createClient() (*awsClient, error) {
|
||||
sess := session.New()
|
||||
ec2meta := ec2metadata.New(sess)
|
||||
if provider.Region == "" {
|
||||
log.Infoln("No EC2 region provided, querying instance metadata endpoint...")
|
||||
identity, err := ec2meta.GetInstanceIdentityDocument()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
provider.Region = identity.Region
|
||||
}
|
||||
|
||||
cfg := &aws.Config{
|
||||
Region: &provider.Region,
|
||||
Credentials: credentials.NewChainCredentials(
|
||||
[]credentials.Provider{
|
||||
&credentials.StaticProvider{
|
||||
Value: credentials.Value{
|
||||
AccessKeyID: provider.AccessKeyID,
|
||||
SecretAccessKey: provider.SecretAccessKey,
|
||||
},
|
||||
},
|
||||
&credentials.EnvProvider{},
|
||||
&credentials.SharedCredentialsProvider{},
|
||||
defaults.RemoteCredProvider(*(defaults.Config()), defaults.Handlers()),
|
||||
}),
|
||||
}
|
||||
|
||||
return &awsClient{
|
||||
ecs.New(sess, cfg),
|
||||
ec2.New(sess, cfg),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Provide allows the provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (provider *ECS) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error {
|
||||
|
||||
provider.Constraints = append(provider.Constraints, constraints...)
|
||||
|
||||
handleCanceled := func(ctx context.Context, err error) error {
|
||||
if ctx.Err() == context.Canceled || err == context.Canceled {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
pool.Go(func(stop chan bool) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
select {
|
||||
case <-stop:
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
operation := func() error {
|
||||
aws, err := provider.createClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configuration, err := provider.loadECSConfig(ctx, aws)
|
||||
if err != nil {
|
||||
return handleCanceled(ctx, err)
|
||||
}
|
||||
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "ecs",
|
||||
Configuration: configuration,
|
||||
}
|
||||
|
||||
if provider.Watch {
|
||||
reload := time.NewTicker(time.Second * time.Duration(provider.RefreshSeconds))
|
||||
defer reload.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-reload.C:
|
||||
configuration, err := provider.loadECSConfig(ctx, aws)
|
||||
if err != nil {
|
||||
return handleCanceled(ctx, err)
|
||||
}
|
||||
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "ecs",
|
||||
Configuration: configuration,
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return handleCanceled(ctx, ctx.Err())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
notify := func(err error, time time.Duration) {
|
||||
log.Errorf("ECS connection error %+v, retrying in %s", err, time)
|
||||
}
|
||||
err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||
if err != nil {
|
||||
log.Errorf("Cannot connect to ECS api %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func wrapAws(ctx context.Context, req *request.Request) error {
|
||||
req.HTTPRequest = req.HTTPRequest.WithContext(ctx)
|
||||
return req.Send()
|
||||
}
|
||||
|
||||
func (provider *ECS) loadECSConfig(ctx context.Context, client *awsClient) (*types.Configuration, error) {
|
||||
var ecsFuncMap = template.FuncMap{
|
||||
"filterFrontends": provider.filterFrontends,
|
||||
"getFrontendRule": provider.getFrontendRule,
|
||||
}
|
||||
|
||||
instances, err := provider.listInstances(ctx, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
instances = fun.Filter(provider.filterInstance, instances).([]ecsInstance)
|
||||
|
||||
return provider.getConfiguration("templates/ecs.tmpl", ecsFuncMap, struct {
|
||||
Instances []ecsInstance
|
||||
}{
|
||||
instances,
|
||||
})
|
||||
}
|
||||
|
||||
// Find all running ECS tasks in a cluster, also collect the task definitions (for docker labels)
|
||||
// and the EC2 instance data
|
||||
func (provider *ECS) listInstances(ctx context.Context, client *awsClient) ([]ecsInstance, error) {
|
||||
var taskArns []*string
|
||||
req, _ := client.ecs.ListTasksRequest(&ecs.ListTasksInput{
|
||||
Cluster: &provider.Cluster,
|
||||
DesiredStatus: aws.String(ecs.DesiredStatusRunning),
|
||||
})
|
||||
|
||||
for ; req != nil; req = req.NextPage() {
|
||||
if err := wrapAws(ctx, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
taskArns = append(taskArns, req.Data.(*ecs.ListTasksOutput).TaskArns...)
|
||||
}
|
||||
|
||||
req, taskResp := client.ecs.DescribeTasksRequest(&ecs.DescribeTasksInput{
|
||||
Tasks: taskArns,
|
||||
Cluster: &provider.Cluster,
|
||||
})
|
||||
|
||||
if err := wrapAws(ctx, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
containerInstanceArns := make([]*string, 0)
|
||||
byContainerInstance := make(map[string]int)
|
||||
|
||||
taskDefinitionArns := make([]*string, 0)
|
||||
byTaskDefinition := make(map[string]int)
|
||||
|
||||
for _, task := range taskResp.Tasks {
|
||||
if _, found := byContainerInstance[*task.ContainerInstanceArn]; !found {
|
||||
byContainerInstance[*task.ContainerInstanceArn] = len(containerInstanceArns)
|
||||
containerInstanceArns = append(containerInstanceArns, task.ContainerInstanceArn)
|
||||
}
|
||||
if _, found := byTaskDefinition[*task.TaskDefinitionArn]; !found {
|
||||
byTaskDefinition[*task.TaskDefinitionArn] = len(taskDefinitionArns)
|
||||
taskDefinitionArns = append(taskDefinitionArns, task.TaskDefinitionArn)
|
||||
}
|
||||
}
|
||||
|
||||
machines, err := provider.lookupEc2Instances(ctx, client, containerInstanceArns)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
taskDefinitions, err := provider.lookupTaskDefinitions(ctx, client, taskDefinitionArns)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var instances []ecsInstance
|
||||
for _, task := range taskResp.Tasks {
|
||||
|
||||
machineIdx := byContainerInstance[*task.ContainerInstanceArn]
|
||||
taskDefIdx := byTaskDefinition[*task.TaskDefinitionArn]
|
||||
|
||||
for _, container := range task.Containers {
|
||||
|
||||
taskDefinition := taskDefinitions[taskDefIdx]
|
||||
var containerDefinition *ecs.ContainerDefinition
|
||||
for _, def := range taskDefinition.ContainerDefinitions {
|
||||
if *container.Name == *def.Name {
|
||||
containerDefinition = def
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
instances = append(instances, ecsInstance{
|
||||
fmt.Sprintf("%s-%s", strings.Replace(*task.Group, ":", "-", 1), *container.Name),
|
||||
(*task.TaskArn)[len(*task.TaskArn)-12:],
|
||||
task,
|
||||
taskDefinition,
|
||||
container,
|
||||
containerDefinition,
|
||||
machines[machineIdx],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return instances, nil
|
||||
}
|
||||
|
||||
func (provider *ECS) lookupEc2Instances(ctx context.Context, client *awsClient, containerArns []*string) ([]*ec2.Instance, error) {
|
||||
req, containerResp := client.ecs.DescribeContainerInstancesRequest(&ecs.DescribeContainerInstancesInput{
|
||||
ContainerInstances: containerArns,
|
||||
Cluster: &provider.Cluster,
|
||||
})
|
||||
|
||||
if err := wrapAws(ctx, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
order := make(map[string]int)
|
||||
for i, arn := range containerArns {
|
||||
order[*arn] = i
|
||||
}
|
||||
|
||||
instanceIds := make([]*string, len(containerArns))
|
||||
for i, container := range containerResp.ContainerInstances {
|
||||
order[*container.Ec2InstanceId] = order[*container.ContainerInstanceArn]
|
||||
instanceIds[i] = container.Ec2InstanceId
|
||||
}
|
||||
|
||||
req, instancesResp := client.ec2.DescribeInstancesRequest(&ec2.DescribeInstancesInput{
|
||||
InstanceIds: instanceIds,
|
||||
})
|
||||
|
||||
if err := wrapAws(ctx, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
instances := make([]*ec2.Instance, len(containerArns))
|
||||
for _, r := range instancesResp.Reservations {
|
||||
instances[order[*r.Instances[0].InstanceId]] = r.Instances[0]
|
||||
}
|
||||
return instances, nil
|
||||
}
|
||||
|
||||
func (provider *ECS) lookupTaskDefinitions(ctx context.Context, client *awsClient, taskDefArns []*string) ([]*ecs.TaskDefinition, error) {
|
||||
taskDefinitions := make([]*ecs.TaskDefinition, len(taskDefArns))
|
||||
for i, arn := range taskDefArns {
|
||||
|
||||
req, resp := client.ecs.DescribeTaskDefinitionRequest(&ecs.DescribeTaskDefinitionInput{
|
||||
TaskDefinition: arn,
|
||||
})
|
||||
|
||||
if err := wrapAws(ctx, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
taskDefinitions[i] = resp.TaskDefinition
|
||||
}
|
||||
return taskDefinitions, nil
|
||||
}
|
||||
|
||||
func (i ecsInstance) label(k string) string {
|
||||
if v, found := i.containerDefinition.DockerLabels[k]; found {
|
||||
return *v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (provider *ECS) filterInstance(i ecsInstance) bool {
|
||||
if len(i.container.NetworkBindings) == 0 {
|
||||
log.Debugf("Filtering ecs instance without port %s (%s)", i.Name, i.ID)
|
||||
return false
|
||||
}
|
||||
|
||||
label := i.label("traefik.enable")
|
||||
enabled := provider.ExposedByDefault && label != "false" || label == "true"
|
||||
if !enabled {
|
||||
log.Debugf("Filtering disabled ecs instance %s (%s) (traefik.enabled = '%s')", i.Name, i.ID, label)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (provider *ECS) filterFrontends(instances []ecsInstance) []ecsInstance {
|
||||
byName := make(map[string]bool)
|
||||
|
||||
return fun.Filter(func(i ecsInstance) bool {
|
||||
if _, found := byName[i.Name]; !found {
|
||||
byName[i.Name] = true
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}, instances).([]ecsInstance)
|
||||
}
|
||||
|
||||
func (provider *ECS) getFrontendRule(i ecsInstance) string {
|
||||
if label := i.label("traefik.frontend.rule"); label != "" {
|
||||
return label
|
||||
}
|
||||
return "Host:" + strings.ToLower(strings.Replace(i.Name, "_", "-", -1)) + "." + provider.Domain
|
||||
}
|
||||
|
||||
func (i ecsInstance) Protocol() string {
|
||||
if label := i.label("traefik.protocol"); label != "" {
|
||||
return label
|
||||
}
|
||||
return "http"
|
||||
}
|
||||
|
||||
func (i ecsInstance) Host() string {
|
||||
return *i.machine.PrivateIpAddress
|
||||
}
|
||||
|
||||
func (i ecsInstance) Port() string {
|
||||
return strconv.FormatInt(*i.container.NetworkBindings[0].HostPort, 10)
|
||||
}
|
||||
|
||||
func (i ecsInstance) Weight() string {
|
||||
if label := i.label("traefik.weight"); label != "" {
|
||||
return label
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
|
||||
func (i ecsInstance) PassHostHeader() string {
|
||||
if label := i.label("traefik.frontend.passHostHeader"); label != "" {
|
||||
return label
|
||||
}
|
||||
return "true"
|
||||
}
|
||||
|
||||
func (i ecsInstance) Priority() string {
|
||||
if label := i.label("traefik.frontend.priority"); label != "" {
|
||||
return label
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
|
||||
func (i ecsInstance) EntryPoints() []string {
|
||||
if label := i.label("traefik.frontend.entryPoints"); label != "" {
|
||||
return strings.Split(label, ",")
|
||||
}
|
||||
return []string{}
|
||||
}
|
223
provider/ecs_test.go
Normal file
223
provider/ecs_test.go
Normal file
|
@ -0,0 +1,223 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go/service/ecs"
|
||||
)
|
||||
|
||||
func makeEcsInstance(containerDef *ecs.ContainerDefinition) ecsInstance {
|
||||
container := &ecs.Container{
|
||||
Name: containerDef.Name,
|
||||
NetworkBindings: make([]*ecs.NetworkBinding, len(containerDef.PortMappings)),
|
||||
}
|
||||
|
||||
for i, pm := range containerDef.PortMappings {
|
||||
container.NetworkBindings[i] = &ecs.NetworkBinding{
|
||||
HostPort: pm.HostPort,
|
||||
ContainerPort: pm.ContainerPort,
|
||||
Protocol: pm.Protocol,
|
||||
BindIP: aws.String("0.0.0.0"),
|
||||
}
|
||||
}
|
||||
|
||||
return ecsInstance{
|
||||
Name: "foo-http",
|
||||
ID: "123456789abc",
|
||||
task: &ecs.Task{
|
||||
Containers: []*ecs.Container{container},
|
||||
},
|
||||
taskDefinition: &ecs.TaskDefinition{
|
||||
ContainerDefinitions: []*ecs.ContainerDefinition{containerDef},
|
||||
},
|
||||
container: container,
|
||||
containerDefinition: containerDef,
|
||||
machine: &ec2.Instance{
|
||||
PrivateIpAddress: aws.String("10.0.0.0"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func simpleEcsInstance(labels map[string]*string) ecsInstance {
|
||||
return makeEcsInstance(&ecs.ContainerDefinition{
|
||||
Name: aws.String("http"),
|
||||
PortMappings: []*ecs.PortMapping{{
|
||||
HostPort: aws.Int64(80),
|
||||
ContainerPort: aws.Int64(80),
|
||||
Protocol: aws.String("tcp"),
|
||||
}},
|
||||
DockerLabels: labels,
|
||||
})
|
||||
}
|
||||
|
||||
func TestEcsProtocol(t *testing.T) {
|
||||
cases := []struct {
|
||||
expected string
|
||||
instanceInfo ecsInstance
|
||||
}{
|
||||
{
|
||||
expected: "http",
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||
},
|
||||
{
|
||||
expected: "https",
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{
|
||||
"traefik.protocol": aws.String("https"),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
value := c.instanceInfo.Protocol()
|
||||
if value != c.expected {
|
||||
t.Fatalf("Should have been %s, got %s", c.expected, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEcsHost(t *testing.T) {
|
||||
cases := []struct {
|
||||
expected string
|
||||
instanceInfo ecsInstance
|
||||
}{
|
||||
{
|
||||
expected: "10.0.0.0",
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
value := c.instanceInfo.Host()
|
||||
if value != c.expected {
|
||||
t.Fatalf("Should have been %s, got %s", c.expected, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEcsPort(t *testing.T) {
|
||||
cases := []struct {
|
||||
expected string
|
||||
instanceInfo ecsInstance
|
||||
}{
|
||||
{
|
||||
expected: "80",
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
value := c.instanceInfo.Port()
|
||||
if value != c.expected {
|
||||
t.Fatalf("Should have been %s, got %s", c.expected, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEcsWeight(t *testing.T) {
|
||||
cases := []struct {
|
||||
expected string
|
||||
instanceInfo ecsInstance
|
||||
}{
|
||||
{
|
||||
expected: "0",
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||
},
|
||||
{
|
||||
expected: "10",
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{
|
||||
"traefik.weight": aws.String("10"),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
value := c.instanceInfo.Weight()
|
||||
if value != c.expected {
|
||||
t.Fatalf("Should have been %s, got %s", c.expected, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEcsPassHostHeader(t *testing.T) {
|
||||
cases := []struct {
|
||||
expected string
|
||||
instanceInfo ecsInstance
|
||||
}{
|
||||
{
|
||||
expected: "true",
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||
},
|
||||
{
|
||||
expected: "false",
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{
|
||||
"traefik.frontend.passHostHeader": aws.String("false"),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
value := c.instanceInfo.PassHostHeader()
|
||||
if value != c.expected {
|
||||
t.Fatalf("Should have been %s, got %s", c.expected, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEcsPriority(t *testing.T) {
|
||||
cases := []struct {
|
||||
expected string
|
||||
instanceInfo ecsInstance
|
||||
}{
|
||||
{
|
||||
expected: "0",
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||
},
|
||||
{
|
||||
expected: "10",
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{
|
||||
"traefik.frontend.priority": aws.String("10"),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
value := c.instanceInfo.Priority()
|
||||
if value != c.expected {
|
||||
t.Fatalf("Should have been %s, got %s", c.expected, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEcsEntryPoints(t *testing.T) {
|
||||
cases := []struct {
|
||||
expected []string
|
||||
instanceInfo ecsInstance
|
||||
}{
|
||||
{
|
||||
expected: []string{},
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||
},
|
||||
{
|
||||
expected: []string{"http"},
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{
|
||||
"traefik.frontend.entryPoints": aws.String("http"),
|
||||
}),
|
||||
},
|
||||
{
|
||||
expected: []string{"http", "https"},
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{
|
||||
"traefik.frontend.entryPoints": aws.String("http,https"),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
value := c.instanceInfo.EntryPoints()
|
||||
if !reflect.DeepEqual(value, c.expected) {
|
||||
t.Fatalf("Should have been %s, got %s", c.expected, value)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -373,6 +373,9 @@ func (server *Server) configureProviders() {
|
|||
if server.globalConfiguration.Eureka != nil {
|
||||
server.providers = append(server.providers, server.globalConfiguration.Eureka)
|
||||
}
|
||||
if server.globalConfiguration.ECS != nil {
|
||||
server.providers = append(server.providers, server.globalConfiguration.ECS)
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) startProviders() {
|
||||
|
|
17
templates/ecs.tmpl
Normal file
17
templates/ecs.tmpl
Normal file
|
@ -0,0 +1,17 @@
|
|||
[backends]{{range .Instances}}
|
||||
[backends.backend-{{ .Name }}.servers.server-{{ .Name }}{{ .ID }}]
|
||||
url = "{{ .Protocol }}://{{ .Host }}:{{ .Port }}"
|
||||
weight = {{ .Weight }}
|
||||
{{end}}
|
||||
|
||||
[frontends]{{range filterFrontends .Instances}}
|
||||
[frontends.frontend-{{ .Name }}]
|
||||
backend = "backend-{{ .Name }}"
|
||||
passHostHeader = {{ .PassHostHeader }}
|
||||
priority = {{ .Priority }}
|
||||
entryPoints = [{{range .EntryPoints }}
|
||||
"{{.}}",
|
||||
{{end}}]
|
||||
[frontends.frontend-{{ .Name }}.routes.route-frontend-{{ .Name }}]
|
||||
rule = "{{getFrontendRule .}}"
|
||||
{{end}}
|
|
@ -868,6 +868,67 @@
|
|||
# filename = "boltdb.tmpl"
|
||||
|
||||
|
||||
################################################################
|
||||
# ECS configuration backend
|
||||
################################################################
|
||||
|
||||
# Enable ECS configuration backend
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# [ecs]
|
||||
|
||||
# ECS Cluster Name
|
||||
#
|
||||
# Optional
|
||||
# Default: "default"
|
||||
#
|
||||
# Cluster = "default"
|
||||
|
||||
# Enable watch ECS changes
|
||||
#
|
||||
# Optional
|
||||
# Default: true
|
||||
#
|
||||
# Watch = true
|
||||
|
||||
# Polling interval (in seconds)
|
||||
#
|
||||
# Optional
|
||||
# Default: 15
|
||||
#
|
||||
# RefreshSeconds = 15
|
||||
|
||||
# Expose ECS services by default in traefik
|
||||
#
|
||||
# Optional
|
||||
# Default: true
|
||||
#
|
||||
# ExposedByDefault = false
|
||||
|
||||
# Region to use when connecting to AWS
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# Region = "us-east-1"
|
||||
|
||||
# AccessKeyID to use when connecting to AWS
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# AccessKeyID = "abc"
|
||||
|
||||
# SecretAccessKey to use when connecting to AWS
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# SecretAccessKey = "123"
|
||||
|
||||
# Override default configuration template. For advanced users :)
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# filename = "ecs.tmpl"
|
||||
|
||||
|
||||
################################################################
|
||||
|
|
Loading…
Reference in a new issue