From 8c0fee5abf2237273ad9ba0b727ad545fcc69cbd Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Fri, 24 Apr 2020 14:19:14 +0200 Subject: [PATCH] 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 --- ecs/Makefile | 3 + ecs/pkg/amazon/api.go | 22 ++++ ecs/pkg/amazon/client.go | 28 +---- ecs/pkg/amazon/cloudformation.go | 42 ++++++- ecs/pkg/amazon/down.go | 14 +-- ecs/pkg/amazon/ecs.go | 50 --------- ecs/pkg/amazon/loadBalancer.go | 136 ---------------------- ecs/pkg/amazon/logs.go | 28 ----- ecs/pkg/amazon/network.go | 112 ------------------ ecs/pkg/amazon/roles.go | 49 -------- ecs/pkg/amazon/sdk.go | 187 +++++++++++++++++++++++++++++++ ecs/pkg/amazon/up.go | 47 +------- 12 files changed, 261 insertions(+), 457 deletions(-) create mode 100644 ecs/pkg/amazon/api.go delete mode 100644 ecs/pkg/amazon/ecs.go delete mode 100644 ecs/pkg/amazon/loadBalancer.go delete mode 100644 ecs/pkg/amazon/logs.go delete mode 100644 ecs/pkg/amazon/network.go delete mode 100644 ecs/pkg/amazon/roles.go create mode 100644 ecs/pkg/amazon/sdk.go diff --git a/ecs/Makefile b/ecs/Makefile index 6ff9437d7..5d3c5883d 100644 --- a/ecs/Makefile +++ b/ecs/Makefile @@ -1,3 +1,6 @@ +clean: + rm -rf dist/ + build: go build -v -o dist/docker-ecs cmd/main/main.go diff --git a/ecs/pkg/amazon/api.go b/ecs/pkg/amazon/api.go new file mode 100644 index 000000000..954c1b452 --- /dev/null +++ b/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 +} diff --git a/ecs/pkg/amazon/client.go b/ecs/pkg/amazon/client.go index ff202fc23..18ffc9855 100644 --- a/ecs/pkg/amazon/client.go +++ b/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{} diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index dd304ab48..7458e7863 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/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 +} \ No newline at end of file diff --git a/ecs/pkg/amazon/down.go b/ecs/pkg/amazon/down.go index 4dfdff4a3..49583864c 100644 --- a/ecs/pkg/amazon/down.go +++ b/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") diff --git a/ecs/pkg/amazon/ecs.go b/ecs/pkg/amazon/ecs.go deleted file mode 100644 index a1ee2f928..000000000 --- a/ecs/pkg/amazon/ecs.go +++ /dev/null @@ -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 -} diff --git a/ecs/pkg/amazon/loadBalancer.go b/ecs/pkg/amazon/loadBalancer.go deleted file mode 100644 index c8f96cd9b..000000000 --- a/ecs/pkg/amazon/loadBalancer.go +++ /dev/null @@ -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 -} diff --git a/ecs/pkg/amazon/logs.go b/ecs/pkg/amazon/logs.go deleted file mode 100644 index 0c06671c2..000000000 --- a/ecs/pkg/amazon/logs.go +++ /dev/null @@ -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 -} diff --git a/ecs/pkg/amazon/network.go b/ecs/pkg/amazon/network.go deleted file mode 100644 index 26b412758..000000000 --- a/ecs/pkg/amazon/network.go +++ /dev/null @@ -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 -} diff --git a/ecs/pkg/amazon/roles.go b/ecs/pkg/amazon/roles.go deleted file mode 100644 index 7e4594af4..000000000 --- a/ecs/pkg/amazon/roles.go +++ /dev/null @@ -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 -} diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go new file mode 100644 index 000000000..228507c17 --- /dev/null +++ b/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 +} diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index 2dbfd3062..4488cf670 100644 --- a/ecs/pkg/amazon/up.go +++ b/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