2017-04-17 13:50:02 +03:00
package ecs
2017-01-05 17:24:17 +03:00
import (
"context"
"fmt"
"strings"
"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/cenk/backoff"
"github.com/containous/traefik/job"
"github.com/containous/traefik/log"
2017-04-17 13:50:02 +03:00
"github.com/containous/traefik/provider"
2017-01-05 17:24:17 +03:00
"github.com/containous/traefik/safe"
"github.com/containous/traefik/types"
)
2017-04-17 13:50:02 +03:00
var _ provider . Provider = ( * Provider ) ( nil )
2017-01-05 17:24:17 +03:00
2017-04-17 13:50:02 +03:00
// Provider holds configurations of the provider.
type Provider struct {
2017-10-02 11:32:02 +03:00
provider . BaseProvider ` mapstructure:",squash" export:"true" `
2017-01-05 17:24:17 +03:00
Domain string ` description:"Default domain used" `
2017-10-02 11:32:02 +03:00
ExposedByDefault bool ` description:"Expose containers by default" export:"true" `
RefreshSeconds int ` description:"Polling interval (in seconds)" export:"true" `
2017-01-05 17:24:17 +03:00
2017-04-17 13:50:02 +03:00
// Provider lookup parameters
2017-08-22 12:46:03 +03:00
Clusters Clusters ` description:"ECS Clusters name" `
Cluster string ` description:"deprecated - ECS Cluster name" ` // deprecated
2017-10-02 11:32:02 +03:00
AutoDiscoverClusters bool ` description:"Auto discover cluster" export:"true" `
Region string ` description:"The AWS region to use for requests" export:"true" `
2017-08-22 12:46:03 +03:00
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" `
2017-01-05 17:24:17 +03:00
}
type ecsInstance struct {
Name string
ID string
containerDefinition * ecs . ContainerDefinition
2018-05-28 19:52:03 +03:00
machine * machine
2018-03-28 03:13:48 +03:00
TraefikLabels map [ string ] string
2017-01-05 17:24:17 +03:00
}
2018-07-04 16:08:03 +03:00
type portMapping struct {
containerPort int64
hostPort int64
}
2018-05-28 19:52:03 +03:00
type machine struct {
name string
state string
privateIP string
2018-07-04 16:08:03 +03:00
ports [ ] portMapping
2018-05-28 19:52:03 +03:00
}
2017-01-05 17:24:17 +03:00
type awsClient struct {
ecs * ecs . ECS
ec2 * ec2 . EC2
}
2017-04-17 13:50:02 +03:00
func ( p * Provider ) createClient ( ) ( * awsClient , error ) {
2018-02-22 16:58:04 +03:00
sess , err := session . NewSession ( )
if err != nil {
return nil , err
}
2017-01-05 17:24:17 +03:00
ec2meta := ec2metadata . New ( sess )
2017-04-17 13:50:02 +03:00
if p . Region == "" {
2017-01-05 17:24:17 +03:00
log . Infoln ( "No EC2 region provided, querying instance metadata endpoint..." )
identity , err := ec2meta . GetInstanceIdentityDocument ( )
if err != nil {
return nil , err
}
2017-04-17 13:50:02 +03:00
p . Region = identity . Region
2017-01-05 17:24:17 +03:00
}
cfg := & aws . Config {
2017-04-17 13:50:02 +03:00
Region : & p . Region ,
2017-01-05 17:24:17 +03:00
Credentials : credentials . NewChainCredentials (
[ ] credentials . Provider {
& credentials . StaticProvider {
Value : credentials . Value {
2017-04-17 13:50:02 +03:00
AccessKeyID : p . AccessKeyID ,
SecretAccessKey : p . SecretAccessKey ,
2017-01-05 17:24:17 +03:00
} ,
} ,
& credentials . EnvProvider { } ,
& credentials . SharedCredentialsProvider { } ,
defaults . RemoteCredProvider ( * ( defaults . Config ( ) ) , defaults . Handlers ( ) ) ,
} ) ,
}
2017-07-08 00:48:53 +03:00
if p . Trace {
cfg . WithLogger ( aws . LoggerFunc ( func ( args ... interface { } ) {
log . Debug ( args ... )
} ) )
}
2017-01-05 17:24:17 +03:00
return & awsClient {
ecs . New ( sess , cfg ) ,
ec2 . New ( sess , cfg ) ,
} , nil
}
2017-04-17 13:50:02 +03:00
// Provide allows the ecs provider to provide configurations to traefik
2017-01-05 17:24:17 +03:00
// using the given configuration channel.
2017-04-17 13:50:02 +03:00
func ( p * Provider ) Provide ( configurationChan chan <- types . ConfigMessage , pool * safe . Pool , constraints types . Constraints ) error {
p . Constraints = append ( p . Constraints , constraints ... )
2017-01-05 17:24:17 +03:00
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 ( ) )
2017-07-19 15:11:45 +03:00
safe . Go ( func ( ) {
2017-12-06 12:52:03 +03:00
<- stop
cancel ( )
2017-07-19 15:11:45 +03:00
} )
2017-01-05 17:24:17 +03:00
operation := func ( ) error {
2017-10-10 12:10:02 +03:00
awsClient , err := p . createClient ( )
2017-01-05 17:24:17 +03:00
if err != nil {
return err
}
2017-10-10 12:10:02 +03:00
configuration , err := p . loadECSConfig ( ctx , awsClient )
2017-01-05 17:24:17 +03:00
if err != nil {
return handleCanceled ( ctx , err )
}
configurationChan <- types . ConfigMessage {
ProviderName : "ecs" ,
Configuration : configuration ,
}
2017-04-17 13:50:02 +03:00
if p . Watch {
reload := time . NewTicker ( time . Second * time . Duration ( p . RefreshSeconds ) )
2017-01-05 17:24:17 +03:00
defer reload . Stop ( )
for {
select {
case <- reload . C :
2017-10-10 12:10:02 +03:00
configuration , err := p . loadECSConfig ( ctx , awsClient )
2017-01-05 17:24:17 +03:00
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 ) {
2017-04-17 13:50:02 +03:00
log . Errorf ( "Provider connection error %+v, retrying in %s" , err , time )
2017-01-05 17:24:17 +03:00
}
err := backoff . RetryNotify ( safe . OperationWithRecover ( operation ) , job . NewBackOff ( backoff . NewExponentialBackOff ( ) ) , notify )
if err != nil {
2017-04-17 13:50:02 +03:00
log . Errorf ( "Cannot connect to Provider api %+v" , err )
2017-01-05 17:24:17 +03:00
}
} )
return nil
}
2017-04-17 13:50:02 +03:00
// Find all running Provider tasks in a cluster, also collect the task definitions (for docker labels)
2017-01-05 17:24:17 +03:00
// and the EC2 instance data
2017-04-17 13:50:02 +03:00
func ( p * Provider ) listInstances ( ctx context . Context , client * awsClient ) ( [ ] ecsInstance , error ) {
2017-08-22 12:46:03 +03:00
var clustersArn [ ] * string
var clusters Clusters
2017-01-05 17:24:17 +03:00
2017-08-22 12:46:03 +03:00
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
}
2017-01-05 17:24:17 +03:00
}
2018-02-22 16:58:04 +03:00
for _ , cArn := range clustersArn {
clusters = append ( clusters , * cArn )
2017-08-22 12:46:03 +03:00
}
} else if p . Cluster != "" {
// TODO: Deprecated configuration - Need to be removed in the future
clusters = Clusters { p . Cluster }
2018-07-04 16:08:03 +03:00
log . Warn ( "Deprecated configuration found: ecs.cluster. Please use ecs.clusters instead." )
2017-08-22 12:46:03 +03:00
} else {
clusters = p . Clusters
2017-01-05 17:24:17 +03:00
}
2018-03-28 03:13:48 +03:00
var instances [ ] ecsInstance
2017-08-22 12:46:03 +03:00
log . Debugf ( "ECS Clusters: %s" , clusters )
2018-03-28 03:13:48 +03:00
2017-08-22 12:46:03 +03:00
for _ , c := range clusters {
2017-01-05 17:24:17 +03:00
2018-05-28 19:52:03 +03:00
input := & ecs . ListTasksInput {
2017-08-22 12:46:03 +03:00
Cluster : & c ,
DesiredStatus : aws . String ( ecs . DesiredStatusRunning ) ,
2018-05-28 19:52:03 +03:00
}
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 {
2018-07-03 11:02:03 +03:00
log . Errorf ( "Unable to describe tasks for %v" , page . TaskArns )
2018-05-28 19:52:03 +03:00
} else {
for _ , t := range resp . Tasks {
tasks [ aws . StringValue ( t . TaskArn ) ] = t
}
}
2017-08-22 12:46:03 +03:00
}
2018-05-28 19:52:03 +03:00
return ! lastPage
} )
2017-03-01 21:06:34 +03:00
2018-05-28 19:52:03 +03:00
if err != nil {
log . Error ( "Unable to list tasks" )
return nil , err
2017-08-22 12:46:03 +03:00
}
2017-03-01 21:06:34 +03:00
2017-09-29 17:56:03 +03:00
// Skip to the next cluster if there are no tasks found on
// this cluster.
2018-05-28 19:52:03 +03:00
if len ( tasks ) == 0 {
2017-09-29 17:56:03 +03:00
continue
2017-03-01 21:06:34 +03:00
}
2017-01-05 17:24:17 +03:00
2018-05-28 19:52:03 +03:00
ec2Instances , err := p . lookupEc2Instances ( ctx , client , & c , tasks )
2017-08-22 12:46:03 +03:00
if err != nil {
return nil , err
}
2017-01-05 17:24:17 +03:00
2018-05-28 19:52:03 +03:00
taskDefinitions , err := p . lookupTaskDefinitions ( ctx , client , tasks )
2017-08-22 12:46:03 +03:00
if err != nil {
return nil , err
}
2017-01-05 17:24:17 +03:00
2018-05-28 19:52:03 +03:00
for key , task := range tasks {
2017-01-05 17:24:17 +03:00
2018-05-28 19:52:03 +03:00
containerInstance := ec2Instances [ aws . StringValue ( task . ContainerInstanceArn ) ]
taskDef := taskDefinitions [ key ]
2017-01-05 17:24:17 +03:00
2017-08-22 12:46:03 +03:00
for _ , container := range task . Containers {
2017-01-05 17:24:17 +03:00
2017-08-22 12:46:03 +03:00
var containerDefinition * ecs . ContainerDefinition
2018-05-28 19:52:03 +03:00
for _ , def := range taskDef . ContainerDefinitions {
2018-03-09 14:02:29 +03:00
if aws . StringValue ( container . Name ) == aws . StringValue ( def . Name ) {
2017-08-22 12:46:03 +03:00
containerDefinition = def
break
}
2017-01-05 17:24:17 +03:00
}
2018-05-28 19:52:03 +03:00
if containerDefinition == nil {
log . Debugf ( "Unable to find container definition for %s" , aws . StringValue ( container . Name ) )
continue
}
var mach * machine
if aws . StringValue ( task . LaunchType ) == ecs . LaunchTypeFargate {
2018-07-04 16:08:03 +03:00
var ports [ ] portMapping
for _ , mapping := range containerDefinition . PortMappings {
if mapping != nil {
ports = append ( ports , portMapping {
hostPort : aws . Int64Value ( mapping . HostPort ) ,
containerPort : aws . Int64Value ( mapping . ContainerPort ) ,
} )
}
2018-05-28 19:52:03 +03:00
}
mach = & machine {
privateIP : aws . StringValue ( container . NetworkInterfaces [ 0 ] . PrivateIpv4Address ) ,
2018-07-04 16:08:03 +03:00
ports : ports ,
2018-05-28 19:52:03 +03:00
state : aws . StringValue ( task . LastStatus ) ,
}
} else {
2018-07-04 16:08:03 +03:00
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 ) ,
} )
}
2018-05-28 19:52:03 +03:00
}
mach = & machine {
privateIP : aws . StringValue ( containerInstance . PrivateIpAddress ) ,
2018-07-04 16:08:03 +03:00
ports : ports ,
2018-05-28 19:52:03 +03:00
state : aws . StringValue ( containerInstance . State . Name ) ,
}
}
2017-08-22 12:46:03 +03:00
instances = append ( instances , ecsInstance {
2018-03-28 03:13:48 +03:00
Name : fmt . Sprintf ( "%s-%s" , strings . Replace ( aws . StringValue ( task . Group ) , ":" , "-" , 1 ) , * container . Name ) ,
2018-05-28 19:52:03 +03:00
ID : key [ len ( key ) - 12 : ] ,
2018-03-28 03:13:48 +03:00
containerDefinition : containerDefinition ,
2018-05-28 19:52:03 +03:00
machine : mach ,
2018-03-28 03:13:48 +03:00
TraefikLabels : aws . StringValueMap ( containerDefinition . DockerLabels ) ,
2017-08-22 12:46:03 +03:00
} )
}
2017-01-05 17:24:17 +03:00
}
}
return instances , nil
}
2018-05-28 19:52:03 +03:00
func ( p * Provider ) lookupEc2Instances ( ctx context . Context , client * awsClient , clusterName * string , ecsDatas map [ string ] * ecs . Task ) ( map [ string ] * ec2 . Instance , error ) {
instanceIds := make ( map [ string ] string )
ec2Instances := make ( map [ string ] * ec2 . Instance )
2017-01-05 17:24:17 +03:00
2018-05-28 19:52:03 +03:00
var containerInstancesArns [ ] * string
var instanceArns [ ] * string
for _ , task := range ecsDatas {
if task . ContainerInstanceArn != nil {
containerInstancesArns = append ( containerInstancesArns , task . ContainerInstanceArn )
}
2017-01-05 17:24:17 +03:00
}
2018-05-28 19:52:03 +03:00
resp , err := client . ecs . DescribeContainerInstancesWithContext ( ctx , & ecs . DescribeContainerInstancesInput {
ContainerInstances : containerInstancesArns ,
2017-08-22 12:46:03 +03:00
Cluster : clusterName ,
2017-02-09 02:18:38 +03:00
} )
2018-05-28 19:52:03 +03:00
if err != nil {
log . Errorf ( "Unable to describe container instances: %s" , err )
return nil , err
2017-01-05 17:24:17 +03:00
}
2018-05-28 19:52:03 +03:00
for _ , container := range resp . ContainerInstances {
instanceIds [ aws . StringValue ( container . Ec2InstanceId ) ] = aws . StringValue ( container . ContainerInstanceArn )
instanceArns = append ( instanceArns , container . Ec2InstanceId )
}
2017-01-05 17:24:17 +03:00
2018-05-28 19:52:03 +03:00
if len ( instanceArns ) > 0 {
input := & ec2 . DescribeInstancesInput {
InstanceIds : instanceArns ,
2017-02-09 02:18:38 +03:00
}
2017-01-05 17:24:17 +03:00
2018-05-28 19:52:03 +03:00
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
}
}
2017-02-09 02:18:38 +03:00
}
}
2018-05-28 19:52:03 +03:00
return ! lastPage
} )
if err != nil {
log . Errorf ( "Unable to describe instances: %s" , err )
return nil , err
2017-02-09 02:18:38 +03:00
}
2017-01-05 17:24:17 +03:00
}
2018-05-28 19:52:03 +03:00
return ec2Instances , nil
}
2017-01-05 17:24:17 +03:00
2018-05-28 19:52:03 +03:00
func ( p * Provider ) lookupTaskDefinitions ( ctx context . Context , client * awsClient , taskDefArns map [ string ] * ecs . Task ) ( map [ string ] * ecs . TaskDefinition , error ) {
taskDef := make ( map [ string ] * ecs . TaskDefinition )
for arn , task := range taskDefArns {
resp , err := client . ecs . DescribeTaskDefinitionWithContext ( ctx , & ecs . DescribeTaskDefinitionInput {
TaskDefinition : task . TaskDefinitionArn ,
2017-01-05 17:24:17 +03:00
} )
2018-05-28 19:52:03 +03:00
if err != nil {
log . Errorf ( "Unable to describe task definition: %s" , err )
2017-01-05 17:24:17 +03:00
return nil , err
}
2018-05-28 19:52:03 +03:00
taskDef [ arn ] = resp . TaskDefinition
2017-01-05 17:24:17 +03:00
}
2018-05-28 19:52:03 +03:00
return taskDef , nil
2017-01-05 17:24:17 +03:00
}
2018-04-11 13:26:03 +03:00
func ( p * Provider ) loadECSConfig ( ctx context . Context , client * awsClient ) ( * types . Configuration , error ) {
instances , err := p . listInstances ( ctx , client )
if err != nil {
return nil , err
2017-01-05 17:24:17 +03:00
}
2018-04-11 13:26:03 +03:00
return p . buildConfiguration ( instances )
2017-01-05 17:24:17 +03:00
}