Browse Source

delete CapacityProvider before we invoke DeleteStack

this is a workaround for CloudFormation issue to manage CapacityProvider <-> Cluster reverse dependency

Signed-off-by: Nicolas De Loof <[email protected]>
Nicolas De Loof 5 years ago
parent
commit
eab140ee71
4 changed files with 114 additions and 6 deletions
  1. 22 0
      ecs/aws.go
  2. 48 2
      ecs/down.go
  3. 39 2
      ecs/sdk.go
  4. 5 2
      ecs/wait.go

+ 22 - 0
ecs/aws.go

@@ -0,0 +1,22 @@
+/*
+   Copyright 2020 Docker Compose CLI authors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package ecs
+
+const (
+	awsTypeCapacityProvider = "AWS::ECS::CapacityProvider"
+	awsTypeAutoscalingGroup = "AWS::AutoScaling::AutoScalingGroup"
+)

+ 48 - 2
ecs/down.go

@@ -18,12 +18,58 @@ package ecs
 
 import (
 	"context"
+
+	"github.com/docker/compose-cli/progress"
 )
 
 func (b *ecsAPIService) Down(ctx context.Context, project string) error {
-	err := b.SDK.DeleteStack(ctx, project)
+	resources, err := b.SDK.ListStackResources(ctx, project)
+	if err != nil {
+		return err
+	}
+
+	err = resources.apply(awsTypeCapacityProvider, delete(ctx, b.SDK.DeleteCapacityProvider))
+	if err != nil {
+		return err
+	}
+
+	err = resources.apply(awsTypeAutoscalingGroup, delete(ctx, b.SDK.DeleteAutoscalingGroup))
+	if err != nil {
+		return err
+	}
+
+	previousEvents, err := b.previousStackEvents(ctx, project)
 	if err != nil {
 		return err
 	}
-	return b.WaitStackCompletion(ctx, project, stackDelete)
+
+	err = b.SDK.DeleteStack(ctx, project)
+	if err != nil {
+		return err
+	}
+	return b.WaitStackCompletion(ctx, project, stackDelete, previousEvents...)
+}
+
+func (b *ecsAPIService) previousStackEvents(ctx context.Context, project string) ([]string, error) {
+	events, err := b.SDK.DescribeStackEvents(ctx, project)
+	if err != nil {
+		return nil, err
+	}
+	var previousEvents []string
+	for _, e := range events {
+		previousEvents = append(previousEvents, *e.EventId)
+	}
+	return previousEvents, nil
+}
+
+func delete(ctx context.Context, delete func(ctx context.Context, arn string) error) func(r stackResource) error {
+	return func(r stackResource) error {
+		w := progress.ContextWriter(ctx)
+		w.Event(progress.Event{
+			ID:         r.LogicalID,
+			Status:     progress.Working,
+			StatusText: "DELETE_IN_PROGRESS",
+		})
+		return delete(ctx, r.ARN)
+	}
 }

+ 39 - 2
ecs/sdk.go

@@ -32,6 +32,8 @@ import (
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/request"
 	"github.com/aws/aws-sdk-go/aws/session"
+	"github.com/aws/aws-sdk-go/service/autoscaling"
+	"github.com/aws/aws-sdk-go/service/autoscaling/autoscalingiface"
 	"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"
@@ -48,6 +50,7 @@ import (
 	"github.com/aws/aws-sdk-go/service/iam/iamiface"
 	"github.com/aws/aws-sdk-go/service/secretsmanager"
 	"github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface"
+	"github.com/hashicorp/go-multierror"
 	"github.com/sirupsen/logrus"
 )
 
@@ -61,6 +64,7 @@ type sdk struct {
 	CF  cloudformationiface.CloudFormationAPI
 	SM  secretsmanageriface.SecretsManagerAPI
 	SSM ssmiface.SSMAPI
+	AG  autoscalingiface.AutoScalingAPI
 }
 
 func newSDK(sess *session.Session) sdk {
@@ -77,6 +81,7 @@ func newSDK(sess *session.Session) sdk {
 		CF:  cloudformation.New(sess),
 		SM:  secretsmanager.New(sess),
 		SSM: ssm.New(sess),
+		AG:  autoscaling.New(sess),
 	}
 }
 
@@ -364,7 +369,24 @@ type stackResource struct {
 	Status    string
 }
 
-func (s sdk) ListStackResources(ctx context.Context, name string) ([]stackResource, error) {
+type stackResourceFn func(r stackResource) error
+
+type stackResources []stackResource
+
+func (resources stackResources) apply(awsType string, fn stackResourceFn) error {
+	var errs *multierror.Error
+	for _, r := range resources {
+		if r.Type == awsType {
+			err := fn(r)
+			if err != nil {
+				errs = multierror.Append(err)
+			}
+		}
+	}
+	return errs.ErrorOrNil()
+}
+
+func (s sdk) ListStackResources(ctx context.Context, name string) (stackResources, error) {
 	// FIXME handle pagination
 	res, err := s.CF.ListStackResourcesWithContext(ctx, &cloudformation.ListStackResourcesInput{
 		StackName: aws.String(name),
@@ -373,7 +395,7 @@ func (s sdk) ListStackResources(ctx context.Context, name string) ([]stackResour
 		return nil, err
 	}
 
-	resources := []stackResource{}
+	resources := stackResources{}
 	for _, r := range res.StackResourceSummaries {
 		resources = append(resources, stackResource{
 			LogicalID: aws.StringValue(r.LogicalResourceId),
@@ -714,3 +736,18 @@ func (s sdk) SecurityGroupExists(ctx context.Context, sg string) (bool, error) {
 	}
 	return len(desc.SecurityGroups) > 0, nil
 }
+
+func (s sdk) DeleteCapacityProvider(ctx context.Context, arn string) error {
+	_, err := s.ECS.DeleteCapacityProvider(&ecs.DeleteCapacityProviderInput{
+		CapacityProvider: aws.String(arn),
+	})
+	return err
+}
+
+func (s sdk) DeleteAutoscalingGroup(ctx context.Context, arn string) error {
+	_, err := s.AG.DeleteAutoScalingGroup(&autoscaling.DeleteAutoScalingGroupInput{
+		AutoScalingGroupName: aws.String(arn),
+		ForceDelete:          aws.Bool(true),
+	})
+	return err
+}

+ 5 - 2
ecs/wait.go

@@ -28,8 +28,12 @@ import (
 	"github.com/aws/aws-sdk-go/aws"
 )
 
-func (b *ecsAPIService) WaitStackCompletion(ctx context.Context, name string, operation int) error { //nolint:gocyclo
+func (b *ecsAPIService) WaitStackCompletion(ctx context.Context, name string, operation int, ignored ...string) error { //nolint:gocyclo
 	knownEvents := map[string]struct{}{}
+	for _, id := range ignored {
+		knownEvents[id] = struct{}{}
+	}
+
 	// progress writer
 	w := progress.ContextWriter(ctx)
 	// Get the unique Stack ID so we can collect events without getting some from previous deployments with same name
@@ -78,7 +82,6 @@ func (b *ecsAPIService) WaitStackCompletion(ctx context.Context, name string, op
 			case "CREATE_COMPLETE":
 				if operation == stackCreate {
 					progressStatus = progress.Done
-
 				}
 			case "UPDATE_COMPLETE":
 				if operation == stackUpdate {