Browse Source

Define amazon.API as a simplified and currated interface over AWS SDK

This makes code simpler to read and easier to mock within tests

Signed-off-by: Nicolas De Loof <[email protected]>
Nicolas De Loof 5 years ago
parent
commit
8c0fee5abf

+ 3 - 0
ecs/Makefile

@@ -1,3 +1,6 @@
+clean:
+	rm -rf dist/
+
 build:
 	go build -v -o dist/docker-ecs cmd/main/main.go
 

+ 22 - 0
ecs/pkg/amazon/api.go

@@ -0,0 +1,22 @@
+package amazon
+
+import (
+	"github.com/awslabs/goformation/v4/cloudformation"
+)
+
+type API interface {
+	ClusterExists(name string) (bool, error)
+	CreateCluster(name string) (string, error)
+	DeleteCluster(name string) error
+
+	GetDefaultVPC() (string, error)
+	GetSubNets(vpcId string) ([]string, error)
+
+	ListRolesForPolicy(policy string) ([]string, error)
+	GetRoleArn(name string) (string, error)
+
+	StackExists(name string) (bool, error)
+	CreateStack(name string, template *cloudformation.Template) error
+	DescribeStackEvents(stack string) error
+	DeleteStack(name string) error
+}

+ 2 - 26
ecs/pkg/amazon/client.go

@@ -3,18 +3,6 @@ package amazon
 import (
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/session"
-	"github.com/aws/aws-sdk-go/service/cloudformation"
-	"github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface"
-	"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
-	"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
-	"github.com/aws/aws-sdk-go/service/ec2"
-	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
-	"github.com/aws/aws-sdk-go/service/ecs"
-	"github.com/aws/aws-sdk-go/service/ecs/ecsiface"
-	"github.com/aws/aws-sdk-go/service/elbv2"
-	"github.com/aws/aws-sdk-go/service/elbv2/elbv2iface"
-	"github.com/aws/aws-sdk-go/service/iam"
-	"github.com/aws/aws-sdk-go/service/iam/iamiface"
 	"github.com/docker/ecs-plugin/pkg/compose"
 )
 
@@ -35,26 +23,14 @@ func NewClient(profile string, cluster string, region string) (compose.API, erro
 	return &client{
 		Cluster: cluster,
 		Region:  region,
-		sess:    sess,
-		ECS:     ecs.New(sess),
-		EC2:     ec2.New(sess),
-		ELB:     elbv2.New(sess),
-		CW:      cloudwatchlogs.New(sess),
-		IAM:     iam.New(sess),
-		CF:      cloudformation.New(sess),
+		api:     NewAPI(sess),
 	}, nil
 }
 
 type client struct {
 	Cluster string
 	Region  string
-	sess    *session.Session
-	ECS     ecsiface.ECSAPI
-	EC2     ec2iface.EC2API
-	ELB     elbv2iface.ELBV2API
-	CW      cloudwatchlogsiface.CloudWatchLogsAPI
-	IAM     iamiface.IAMAPI
-	CF      cloudformationiface.CloudFormationAPI
+	api API
 }
 
 var _ compose.API = &client{}

+ 38 - 4
ecs/pkg/amazon/cloudformation.go

@@ -2,6 +2,8 @@ package amazon
 
 import (
 	"fmt"
+	"github.com/compose-spec/compose-go/types"
+	"github.com/sirupsen/logrus"
 	"strings"
 
 	ecsapi "github.com/aws/aws-sdk-go/service/ecs"
@@ -14,12 +16,12 @@ import (
 
 func (c client) Convert(project *compose.Project, loadBalancerArn *string) (*cloudformation.Template, error) {
 	template := cloudformation.NewTemplate()
-	vpc, err := c.GetDefaultVPC()
+	vpc, err := c.api.GetDefaultVPC()
 	if err != nil {
 		return nil, err
 	}
 
-	subnets, err := c.GetSubNets(vpc)
+	subnets, err := c.api.GetSubNets(vpc)
 	if err != nil {
 		return nil, err
 	}
@@ -42,7 +44,7 @@ func (c client) Convert(project *compose.Project, loadBalancerArn *string) (*clo
 		GroupDescription:     securityGroup,
 		GroupName:            securityGroup,
 		SecurityGroupIngress: ingresses,
-		VpcId:                *vpc,
+		VpcId:                vpc,
 	}
 
 	for _, service := range project.Services {
@@ -55,7 +57,7 @@ func (c client) Convert(project *compose.Project, loadBalancerArn *string) (*clo
 		if err != nil {
 			return nil, err
 		}
-		definition.TaskRoleArn = *role
+		definition.TaskRoleArn = role
 
 		taskDefinition := fmt.Sprintf("%sTaskDefinition", service.Name)
 		template.Resources[taskDefinition] = definition
@@ -78,3 +80,35 @@ func (c client) Convert(project *compose.Project, loadBalancerArn *string) (*clo
 	}
 	return template, nil
 }
+
+const ECSTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
+var defaultTaskExecutionRole string
+
+// GetEcsTaskExecutionRole retrieve the role ARN to apply for task execution
+func (c client) GetEcsTaskExecutionRole(spec types.ServiceConfig) (string, error) {
+	if arn, ok := spec.Extras["x-ecs-TaskExecutionRole"]; ok {
+		return arn.(string), nil
+	}
+	if defaultTaskExecutionRole != "" {
+		return defaultTaskExecutionRole, nil
+	}
+
+	logrus.Debug("Retrieve Task Execution Role")
+	entities, err := c.api.ListRolesForPolicy(ECSTaskExecutionPolicy)
+	if err != nil {
+		return "", err
+	}
+	if len(entities) == 0 {
+		return "", fmt.Errorf("no Role is attached to AmazonECSTaskExecutionRole Policy, please provide an explicit task execution role")
+	}
+	if len(entities) > 1 {
+		return "", fmt.Errorf("multiple Roles are attached to AmazonECSTaskExecutionRole Policy, please provide an explicit task execution role")
+	}
+
+	arn, err := c.api.GetRoleArn(entities[0])
+	if err != nil {
+		return "", err
+	}
+	defaultTaskExecutionRole = arn
+	return arn, nil
+}

+ 3 - 11
ecs/pkg/amazon/down.go

@@ -2,30 +2,22 @@ package amazon
 
 import (
 	"fmt"
-
-	"github.com/aws/aws-sdk-go/service/cloudformation"
-	cf "github.com/aws/aws-sdk-go/service/cloudformation"
 )
 
 func (c *client) ComposeDown(projectName *string, keepLoadBalancer, deleteCluster bool) error {
-	_, err := c.CF.DeleteStack(&cloudformation.DeleteStackInput{
-		StackName: projectName,
-	})
+	err := c.api.DeleteStack(projectName)
 	if err != nil {
 		return err
 	}
 	fmt.Printf("Delete stack ")
-	if err = c.CF.WaitUntilStackDeleteComplete(&cf.DescribeStacksInput{StackName: projectName}); err != nil {
-		return err
-	}
-	fmt.Printf("... done.\n")
+
 
 	if !deleteCluster {
 		return nil
 	}
 
 	fmt.Printf("Delete cluster %s", c.Cluster)
-	if err = c.DeleteCluster(); err != nil {
+	if err = c.api.DeleteCluster(c.Cluster); err != nil {
 		return err
 	}
 	fmt.Printf("... done. \n")

+ 0 - 50
ecs/pkg/amazon/ecs.go

@@ -1,50 +0,0 @@
-package amazon
-
-import (
-	"errors"
-
-	"github.com/aws/aws-sdk-go/aws"
-	"github.com/aws/aws-sdk-go/service/ecs"
-	"github.com/sirupsen/logrus"
-)
-
-func (c client) RegisterTaskDefinition(task *ecs.RegisterTaskDefinitionInput) (*string, error) {
-	logrus.Debug("Register Task Definition")
-	def, err := c.ECS.RegisterTaskDefinition(task)
-	if err != nil {
-		return nil, err
-	}
-	return def.TaskDefinition.TaskDefinitionArn, err
-}
-
-func (c client) CreateCluster() (*string, error) {
-	logrus.Debug("Create cluster ", c.Cluster)
-	response, err := c.ECS.CreateCluster(&ecs.CreateClusterInput{ClusterName: &c.Cluster})
-	if err != nil {
-		return nil, err
-	}
-	return response.Cluster.Status, nil
-}
-
-func (c client) DeleteCluster() error {
-	logrus.Debug("Delete cluster ", c.Cluster)
-	response, err := c.ECS.DeleteCluster(&ecs.DeleteClusterInput{Cluster: &c.Cluster})
-	if err != nil {
-		return err
-	}
-	if *response.Cluster.Status == "INACTIVE" {
-		return nil
-	}
-	return errors.New("Failed to delete cluster, status: " + *response.Cluster.Status)
-}
-
-func (c client) ClusterExists() (bool, error) {
-	logrus.Debug("Check if cluster was already created: ", c.Cluster)
-	clusters, err := c.ECS.DescribeClusters(&ecs.DescribeClustersInput{
-		Clusters: []*string{aws.String(c.Cluster)},
-	})
-	if err != nil {
-		return false, err
-	}
-	return len(clusters.Clusters) > 0, nil
-}

+ 0 - 136
ecs/pkg/amazon/loadBalancer.go

@@ -1,136 +0,0 @@
-package amazon
-
-import (
-	"fmt"
-	"strings"
-
-	"github.com/docker/ecs-plugin/pkg/compose"
-	"github.com/sirupsen/logrus"
-
-	"github.com/aws/aws-sdk-go/aws"
-	"github.com/aws/aws-sdk-go/service/elbv2"
-	"github.com/compose-spec/compose-go/types"
-)
-
-func (c client) CreateLoadBalancer(project *compose.Project, subnets []*string) (*string, error) {
-	logrus.Debug("Create Load Balancer")
-	alb, err := c.ELB.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{
-		IpAddressType: nil,
-		Name:          aws.String(fmt.Sprintf("%s-LoadBalancer", project.Name)),
-		Subnets:       subnets,
-		Type:          aws.String(elbv2.LoadBalancerTypeEnumNetwork),
-		Tags: []*elbv2.Tag{
-			{
-				Key:   aws.String("com.docker.compose.project"),
-				Value: aws.String(project.Name),
-			},
-		},
-	})
-	if err != nil {
-		return nil, err
-	}
-	return alb.LoadBalancers[0].LoadBalancerArn, nil
-}
-
-func (c client) DeleteLoadBalancer(project *compose.Project, keepLoadBalancer bool) error {
-	logrus.Debug("Delete Load Balancer")
-	// FIXME We can tag LoadBalancer but not search by tag ?
-	loadBalancer, err := c.ELB.DescribeLoadBalancers(&elbv2.DescribeLoadBalancersInput{
-		Names: aws.StringSlice([]string{fmt.Sprintf("%s-LoadBalancer", project.Name)}),
-	})
-	if err != nil {
-		return err
-	}
-	arn := loadBalancer.LoadBalancers[0].LoadBalancerArn
-
-	err = c.DeleteListeners(arn)
-	if err != nil {
-		return err
-	}
-
-	err = c.DeleteTargetGroups(arn)
-	if err != nil {
-		return err
-	}
-
-	if !keepLoadBalancer {
-		_, err = c.ELB.DeleteLoadBalancer(&elbv2.DeleteLoadBalancerInput{LoadBalancerArn: arn})
-	}
-	return err
-}
-
-func (c client) CreateTargetGroup(name string, vpc *string, port types.ServicePortConfig) (*string, error) {
-	logrus.Debugf("Create Target Group %d/%s\n", port.Target, port.Protocol)
-	group, err := c.ELB.CreateTargetGroup(&elbv2.CreateTargetGroupInput{
-		Name:       aws.String(name),
-		Port:       aws.Int64(int64(port.Target)),
-		Protocol:   aws.String(strings.ToUpper(port.Protocol)),
-		TargetType: aws.String("ip"),
-		VpcId:      vpc,
-	})
-	if err != nil {
-		return nil, err
-	}
-	arn := group.TargetGroups[0].TargetGroupArn
-	return arn, nil
-}
-
-func (c client) DeleteTargetGroups(loadBalancer *string) error {
-	groups, err := c.ELB.DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{
-		LoadBalancerArn: loadBalancer,
-	})
-	if err != nil {
-		return err
-	}
-	for _, group := range groups.TargetGroups {
-		logrus.Debugf("Delete Target Group %s\n", *group.TargetGroupArn)
-		_, err := c.ELB.DeleteTargetGroup(&elbv2.DeleteTargetGroupInput{
-			TargetGroupArn: group.TargetGroupArn,
-		})
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func (c client) CreateListener(port types.ServicePortConfig, arn *string, target *string) error {
-	logrus.Debugf("Create Listener %d\n", port.Published)
-	_, err := c.ELB.CreateListener(&elbv2.CreateListenerInput{
-		DefaultActions: []*elbv2.Action{
-			{
-				ForwardConfig: &elbv2.ForwardActionConfig{
-					TargetGroups: []*elbv2.TargetGroupTuple{
-						{
-							TargetGroupArn: target,
-						},
-					},
-				},
-				Type: aws.String(elbv2.ActionTypeEnumForward),
-			},
-		},
-		LoadBalancerArn: arn,
-		Port:            aws.Int64(int64(port.Published)),
-		Protocol:        aws.String(strings.ToUpper(port.Protocol)),
-	})
-	return err
-}
-
-func (c client) DeleteListeners(loadBalancer *string) error {
-	listeners, err := c.ELB.DescribeListeners(&elbv2.DescribeListenersInput{
-		LoadBalancerArn: loadBalancer,
-	})
-	if err != nil {
-		return err
-	}
-	for _, listener := range listeners.Listeners {
-		logrus.Debugf("Delete Listener %s\n", *listener.ListenerArn)
-		_, err := c.ELB.DeleteListener(&elbv2.DeleteListenerInput{
-			ListenerArn: listener.ListenerArn,
-		})
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}

+ 0 - 28
ecs/pkg/amazon/logs.go

@@ -1,28 +0,0 @@
-package amazon
-
-import (
-	"fmt"
-
-	"github.com/aws/aws-sdk-go/aws"
-	"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
-	"github.com/docker/ecs-plugin/pkg/compose"
-	"github.com/sirupsen/logrus"
-)
-
-// GetOrCreateLogGroup retrieve a pre-existing log group for project or create one
-func (c client) GetOrCreateLogGroup(project *compose.Project) (*string, error) {
-	logrus.Debug("Create Log Group")
-	logGroup := fmt.Sprintf("/ecs/%s", project.Name)
-	_, err := c.CW.CreateLogGroup(&cloudwatchlogs.CreateLogGroupInput{
-		LogGroupName: aws.String(logGroup),
-		Tags: map[string]*string{
-			ProjectTag: aws.String(project.Name),
-		},
-	})
-	if err != nil {
-		if _, ok := err.(*cloudwatchlogs.ResourceAlreadyExistsException); !ok {
-			return nil, err
-		}
-	}
-	return &logGroup, nil
-}

+ 0 - 112
ecs/pkg/amazon/network.go

@@ -1,112 +0,0 @@
-package amazon
-
-import (
-	"fmt"
-	"strings"
-
-	"github.com/aws/aws-sdk-go/aws"
-	"github.com/aws/aws-sdk-go/service/ec2"
-	"github.com/compose-spec/compose-go/types"
-	"github.com/docker/ecs-plugin/pkg/compose"
-	"github.com/sirupsen/logrus"
-)
-
-// GetDefaultVPC retrieve the default VPC for AWS account
-func (c client) GetDefaultVPC() (*string, error) {
-	logrus.Debug("Retrieve default VPC")
-	vpcs, err := c.EC2.DescribeVpcs(&ec2.DescribeVpcsInput{
-		Filters: []*ec2.Filter{
-			{
-				Name:   aws.String("isDefault"),
-				Values: []*string{aws.String("true")},
-			},
-		},
-	})
-	if err != nil {
-		return nil, err
-	}
-	if len(vpcs.Vpcs) == 0 {
-		return nil, fmt.Errorf("account has not default VPC")
-	}
-	return vpcs.Vpcs[0].VpcId, nil
-}
-
-// GetSubNets retrieve default subnets for a VPC
-func (c client) GetSubNets(vpc *string) ([]string, error) {
-	logrus.Debug("Retrieve SubNets")
-	subnets, err := c.EC2.DescribeSubnets(&ec2.DescribeSubnetsInput{
-		DryRun: nil,
-		Filters: []*ec2.Filter{
-			{
-				Name:   aws.String("vpc-id"),
-				Values: []*string{vpc},
-			},
-			{
-				Name:   aws.String("default-for-az"),
-				Values: []*string{aws.String("true")},
-			},
-		},
-	})
-	if err != nil {
-		return nil, err
-	}
-
-	ids := []string{}
-	for _, subnet := range subnets.Subnets {
-		ids = append(ids, *subnet.SubnetId)
-	}
-	return ids, nil
-}
-
-// CreateSecurityGroup create a security group for the project
-func (c client) CreateSecurityGroup(project *compose.Project, vpc *string) (*string, error) {
-	logrus.Debug("Create Security Group")
-	name := fmt.Sprintf("%s Security Group", project.Name)
-	securityGroup, err := c.EC2.CreateSecurityGroup(&ec2.CreateSecurityGroupInput{
-		Description: aws.String(name),
-		GroupName:   aws.String(name),
-		VpcId:       vpc,
-	})
-	if err != nil {
-		return nil, err
-	}
-
-	_, err = c.EC2.CreateTags(&ec2.CreateTagsInput{
-		Resources: []*string{securityGroup.GroupId},
-		Tags: []*ec2.Tag{
-			{
-				Key:   aws.String("Name"),
-				Value: aws.String(name),
-			},
-			{
-				Key:   aws.String(ProjectTag),
-				Value: aws.String(project.Name),
-			},
-		},
-	})
-	if err != nil {
-		return nil, err
-	}
-
-	return securityGroup.GroupId, nil
-}
-
-func (c *client) ExposePort(securityGroup *string, port types.ServicePortConfig) error {
-	logrus.Debugf("Authorize ingress port %d/%s\n", port.Published, port.Protocol)
-	_, err := c.EC2.AuthorizeSecurityGroupIngress(&ec2.AuthorizeSecurityGroupIngressInput{
-		GroupId: securityGroup,
-		IpPermissions: []*ec2.IpPermission{
-			{
-				IpProtocol: aws.String(strings.ToUpper(port.Protocol)),
-				IpRanges: []*ec2.IpRange{
-					{
-						CidrIp: aws.String("0.0.0.0/0"),
-					},
-				},
-				FromPort: aws.Int64(int64(port.Target)),
-				ToPort:   aws.Int64(int64(port.Target)),
-			},
-		},
-	})
-	return err
-}

+ 0 - 49
ecs/pkg/amazon/roles.go

@@ -1,49 +0,0 @@
-package amazon
-
-import (
-	"fmt"
-
-	"github.com/aws/aws-sdk-go/aws"
-	"github.com/aws/aws-sdk-go/service/iam"
-	"github.com/compose-spec/compose-go/types"
-	"github.com/sirupsen/logrus"
-)
-
-const ECSTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
-
-var defaultTaskExecutionRole *string
-
-// GetEcsTaskExecutionRole retrieve the role ARN to apply for task execution
-func (c client) GetEcsTaskExecutionRole(spec types.ServiceConfig) (*string, error) {
-	if arn, ok := spec.Extras["x-ecs-TaskExecutionRole"]; ok {
-		s := arn.(string)
-		return &s, nil
-	}
-	if defaultTaskExecutionRole != nil {
-		return defaultTaskExecutionRole, nil
-	}
-
-	logrus.Debug("Retrieve Task Execution Role")
-	entities, err := c.IAM.ListEntitiesForPolicy(&iam.ListEntitiesForPolicyInput{
-		EntityFilter: aws.String("Role"),
-		PolicyArn:    aws.String(ECSTaskExecutionPolicy),
-	})
-	if err != nil {
-		return nil, err
-	}
-	if len(entities.PolicyRoles) == 0 {
-		return nil, fmt.Errorf("no Role is attached to AmazonECSTaskExecutionRole Policy, please provide an explicit task execution role")
-	}
-	if len(entities.PolicyRoles) > 1 {
-		return nil, fmt.Errorf("multiple Roles are attached to AmazonECSTaskExecutionRole Policy, please provide an explicit task execution role")
-	}
-
-	role, err := c.IAM.GetRole(&iam.GetRoleInput{
-		RoleName: entities.PolicyRoles[0].RoleName,
-	})
-	if err != nil {
-		return nil, err
-	}
-	defaultTaskExecutionRole = role.Role.Arn
-	return role.Role.Arn, nil
-}

+ 187 - 0
ecs/pkg/amazon/sdk.go

@@ -0,0 +1,187 @@
+package amazon
+
+import (
+	"fmt"
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/session"
+	"github.com/aws/aws-sdk-go/service/cloudformation"
+	"github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface"
+	"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
+	"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
+	"github.com/aws/aws-sdk-go/service/ec2"
+	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
+	"github.com/aws/aws-sdk-go/service/ecs"
+	"github.com/aws/aws-sdk-go/service/ecs/ecsiface"
+	"github.com/aws/aws-sdk-go/service/elbv2"
+	"github.com/aws/aws-sdk-go/service/elbv2/elbv2iface"
+	"github.com/aws/aws-sdk-go/service/iam"
+	"github.com/aws/aws-sdk-go/service/iam/iamiface"
+	cf "github.com/awslabs/goformation/v4/cloudformation"
+	"github.com/sirupsen/logrus"
+)
+
+type sdk struct {
+	sess    *session.Session
+	ECS     ecsiface.ECSAPI
+	EC2     ec2iface.EC2API
+	ELB     elbv2iface.ELBV2API
+	CW      cloudwatchlogsiface.CloudWatchLogsAPI
+	IAM     iamiface.IAMAPI
+	CF      cloudformationiface.CloudFormationAPI
+}
+
+func NewAPI(sess *session.Session) API {
+	return sdk{
+		ECS:     ecs.New(sess),
+		EC2:     ec2.New(sess),
+		ELB:     elbv2.New(sess),
+		CW:      cloudwatchlogs.New(sess),
+		IAM:     iam.New(sess),
+		CF:      cloudformation.New(sess),
+	}
+}
+
+func (s sdk) ClusterExists(name string) (bool, error) {
+	logrus.Debug("Check if cluster was already created: ", name)
+	clusters, err := s.ECS.DescribeClusters(&ecs.DescribeClustersInput{
+		Clusters: []*string{aws.String(name)},
+	})
+	if err != nil {
+		return false, err
+	}
+	return len(clusters.Clusters) > 0, nil
+}
+
+func (s sdk) CreateCluster(name string) (string, error) {
+	logrus.Debug("Create cluster ", name)
+	response, err := s.ECS.CreateCluster(&ecs.CreateClusterInput{ClusterName: aws.String(name)})
+	if err != nil {
+		return "", err
+	}
+	return *response.Cluster.Status, nil
+}
+
+func (s sdk) DeleteCluster(name string) error {
+	logrus.Debug("Delete cluster ", name)
+	response, err := s.ECS.DeleteCluster(&ecs.DeleteClusterInput{Cluster: aws.String(name)})
+	if err != nil {
+		return err
+	}
+	if *response.Cluster.Status == "INACTIVE" {
+		return nil
+	}
+	return fmt.Errorf("Failed to delete cluster, status: %s" + *response.Cluster.Status)
+}
+
+func (s sdk) GetDefaultVPC() (string, error) {
+	logrus.Debug("Retrieve default VPC")
+	vpcs, err := s.EC2.DescribeVpcs(&ec2.DescribeVpcsInput{
+		Filters: []*ec2.Filter{
+			{
+				Name:   aws.String("isDefault"),
+				Values: []*string{aws.String("true")},
+			},
+		},
+	})
+	if err != nil {
+		return "", err
+	}
+	if len(vpcs.Vpcs) == 0 {
+		return "", fmt.Errorf("account has not default VPC")
+	}
+	return *vpcs.Vpcs[0].VpcId, nil
+}
+
+func (s sdk) GetSubNets(vpc string) ([]string, error) {
+	logrus.Debug("Retrieve SubNets")
+	subnets, err := s.EC2.DescribeSubnets(&ec2.DescribeSubnetsInput{
+		DryRun: nil,
+		Filters: []*ec2.Filter{
+			{
+				Name:   aws.String("vpc-id"),
+				Values: []*string{ aws.String(vpc)},
+			},
+			{
+				Name:   aws.String("default-for-az"),
+				Values: []*string{aws.String("true")},
+			},
+		},
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	ids := []string{}
+	for _, subnet := range subnets.Subnets {
+		ids = append(ids, *subnet.SubnetId)
+	}
+	return ids, nil
+}
+
+func (s sdk) ListRolesForPolicy(policy string) ([]string, error) {
+	entities, err := s.IAM.ListEntitiesForPolicy(&iam.ListEntitiesForPolicyInput{
+		EntityFilter: aws.String("Role"),
+		PolicyArn:    aws.String(policy),
+	})
+	if err != nil {
+		return nil, err
+	}
+	roles := []string{}
+	for _, e := range entities.PolicyRoles {
+		roles = append(roles, *e.RoleName)
+	}
+	return roles, nil
+}
+
+func (s sdk) GetRoleArn(name string) (string, error) {
+	role, err := s.IAM.GetRole(&iam.GetRoleInput{
+		RoleName: aws.String(name),
+	})
+	if err != nil {
+		return "", err
+	}
+	return *role.Role.Arn, nil
+}
+
+func (s sdk) StackExists(name string) (bool, error) {
+	stacks, err := s.CF.DescribeStacks(&cloudformation.DescribeStacksInput{
+		StackName: aws.String(name),
+	})
+	if err != nil {
+		// FIXME doesn't work as expected
+		return false, nil
+	}
+	return len(stacks.Stacks) > 0, nil
+}
+
+func (s sdk) CreateStack(name string, template *cf.Template) error {
+	logrus.Debug("Create CloudFormation stack")
+	json, err := template.JSON()
+	if err != nil {
+		return err
+	}
+
+	_, err = s.CF.CreateStack(&cloudformation.CreateStackInput{
+		OnFailure:        aws.String("DELETE"),
+		StackName:        aws.String(name),
+		TemplateBody:     aws.String(string(json)),
+		TimeoutInMinutes: aws.Int64(10),
+	})
+	return err
+}
+
+func (s sdk) DescribeStackEvents(name string) error {
+	// Fixme implement Paginator on Events and return as a chan(events)
+	_, err := s.CF.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{
+		StackName: aws.String(name),
+	})
+	return err
+}
+
+func (s sdk) DeleteStack(name string) error {
+	logrus.Debug("Delete CloudFormation stack")
+	_, err := s.CF.DeleteStack(&cloudformation.DeleteStackInput{
+		StackName: aws.String(name),
+	})
+	return err
+}

+ 6 - 41
ecs/pkg/amazon/up.go

@@ -2,26 +2,19 @@ package amazon
 
 import (
 	"fmt"
-	"os"
-
-	"github.com/aws/aws-sdk-go/aws"
-	"github.com/aws/aws-sdk-go/service/cloudformation"
 	"github.com/docker/ecs-plugin/pkg/compose"
 )
 
 func (c *client) ComposeUp(project *compose.Project, loadBalancerArn *string) error {
-	ok, err := c.ClusterExists()
+	ok, err := c.api.ClusterExists(c.Cluster)
 	if err != nil {
 		return err
 	}
 	if !ok {
-		c.CreateCluster()
+		c.api.CreateCluster(c.Cluster)
 	}
-	_, err = c.CF.DescribeStacks(&cloudformation.DescribeStacksInput{
-		StackName: aws.String(project.Name),
-	})
-	if err == nil {
-		// FIXME no ErrNotFound err type here
+	update, err := c.api.StackExists(project.Name)
+	if update {
 		return fmt.Errorf("we do not (yet) support updating an existing CloudFormation stack")
 	}
 
@@ -30,40 +23,12 @@ func (c *client) ComposeUp(project *compose.Project, loadBalancerArn *string) er
 		return err
 	}
 
-	json, err := template.JSON()
+	err = c.api.CreateStack(project.Name, template)
 	if err != nil {
 		return err
 	}
 
-	_, err = c.CF.ValidateTemplate(&cloudformation.ValidateTemplateInput{
-		TemplateBody: aws.String(string(json)),
-	})
-	if err != nil {
-		return err
-	}
-
-	_, err = c.CF.CreateStack(&cloudformation.CreateStackInput{
-		OnFailure:        aws.String("DELETE"),
-		StackName:        aws.String(project.Name),
-		TemplateBody:     aws.String(string(json)),
-		TimeoutInMinutes: aws.Int64(10),
-	})
-	if err != nil {
-		return err
-	}
-
-	events, err := c.CF.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{
-		StackName: aws.String(project.Name),
-	})
-	if err != nil {
-		return err
-	}
-	for _, event := range events.StackEvents {
-		fmt.Printf("%s %s\n", *event.LogicalResourceId, *event.ResourceStatus)
-		if *event.ResourceStatus == "CREATE_FAILED" {
-			fmt.Fprintln(os.Stderr, event.ResourceStatusReason)
-		}
-	}
+	err = c.api.DescribeStackEvents(project.Name)
 
 	// TODO monitor progress
 	return nil