From eab140ee71bffa0e51c07bd234d3e9f8e8ff3b09 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Fri, 2 Oct 2020 14:08:23 +0200 Subject: [PATCH] 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 --- ecs/aws.go | 22 ++++++++++++++++++++++ ecs/down.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++-- ecs/sdk.go | 41 +++++++++++++++++++++++++++++++++++++++-- ecs/wait.go | 7 +++++-- 4 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 ecs/aws.go diff --git a/ecs/aws.go b/ecs/aws.go new file mode 100644 index 000000000..a4b692552 --- /dev/null +++ b/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" +) diff --git a/ecs/down.go b/ecs/down.go index 914f680db..10dca8094 100644 --- a/ecs/down.go +++ b/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 } - return b.WaitStackCompletion(ctx, project, stackDelete) + + 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 + } + + 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) + } } diff --git a/ecs/sdk.go b/ecs/sdk.go index 0670a7181..e1cef4c98 100644 --- a/ecs/sdk.go +++ b/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 +} diff --git a/ecs/wait.go b/ecs/wait.go index 27ccef4b9..7413a4feb 100644 --- a/ecs/wait.go +++ b/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 {