diff --git a/ecs/pkg/amazon/mock/api.go b/ecs/pkg/amazon/mock/api.go index fd9b01c05..571f0bf24 100644 --- a/ecs/pkg/amazon/mock/api.go +++ b/ecs/pkg/amazon/mock/api.go @@ -6,7 +6,8 @@ package mock import ( context "context" - cloudformation "github.com/awslabs/goformation/v4/cloudformation" + cloudformation "github.com/aws/aws-sdk-go/service/cloudformation" + cloudformation0 "github.com/awslabs/goformation/v4/cloudformation" gomock "github.com/golang/mock/gomock" reflect "reflect" ) @@ -65,7 +66,7 @@ func (mr *MockAPIMockRecorder) CreateCluster(arg0, arg1 interface{}) *gomock.Cal } // CreateStack mocks base method -func (m *MockAPI) CreateStack(arg0 context.Context, arg1 string, arg2 *cloudformation.Template) error { +func (m *MockAPI) CreateStack(arg0 context.Context, arg1 string, arg2 *cloudformation0.Template) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateStack", arg0, arg1, arg2) ret0, _ := ret[0].(error) @@ -107,11 +108,12 @@ func (mr *MockAPIMockRecorder) DeleteStack(arg0, arg1 interface{}) *gomock.Call } // DescribeStackEvents mocks base method -func (m *MockAPI) DescribeStackEvents(arg0 context.Context, arg1 string) error { +func (m *MockAPI) DescribeStackEvents(arg0 context.Context, arg1 string) ([]*cloudformation.StackEvent, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DescribeStackEvents", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].([]*cloudformation.StackEvent) + ret1, _ := ret[1].(error) + return ret0, ret1 } // DescribeStackEvents indicates an expected call of DescribeStackEvents @@ -209,3 +211,17 @@ func (mr *MockAPIMockRecorder) VpcExists(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VpcExists", reflect.TypeOf((*MockAPI)(nil).VpcExists), arg0, arg1) } + +// WaitStackComplete mocks base method +func (m *MockAPI) WaitStackComplete(arg0 context.Context, arg1 string, arg2 func() error) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WaitStackComplete", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// WaitStackComplete indicates an expected call of WaitStackComplete +func (mr *MockAPIMockRecorder) WaitStackComplete(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitStackComplete", reflect.TypeOf((*MockAPI)(nil).WaitStackComplete), arg0, arg1, arg2) +} diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index 8f8c34033..95897511b 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -3,6 +3,8 @@ package amazon import ( "context" "fmt" + "strings" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" @@ -183,13 +185,47 @@ func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template }) return err } +func (s sdk) WaitStackComplete(ctx context.Context, name string, fn func() error) error { + for i := 0; i < 120; i++ { + stacks, err := s.CF.DescribeStacks(&cloudformation.DescribeStacksInput{ + StackName: aws.String(name), + }) + if err != nil { + return err + } -func (s sdk) DescribeStackEvents(ctx context.Context, name string) error { + err = fn() + if err != nil { + return err + } + + status := *stacks.Stacks[0].StackStatus + if strings.HasSuffix(status, "_COMPLETE") || strings.HasSuffix(status, "_FAILED") { + return nil + } + time.Sleep(1 * time.Second) + } + return fmt.Errorf("120s timeout waiting for CloudFormation stack %s to complete", name) +} + +func (s sdk) DescribeStackEvents(ctx context.Context, name string) ([]*cloudformation.StackEvent, error) { // Fixme implement Paginator on Events and return as a chan(events) - _, err := s.CF.DescribeStackEventsWithContext(aws.Context(ctx), &cloudformation.DescribeStackEventsInput{ - StackName: aws.String(name), - }) - return err + events := []*cloudformation.StackEvent{} + var nextToken *string + for { + resp, err := s.CF.DescribeStackEventsWithContext(aws.Context(ctx), &cloudformation.DescribeStackEventsInput{ + StackName: aws.String(name), + NextToken: nextToken, + }) + if err != nil { + return nil, err + } + events = append(events, resp.StackEvents...) + if resp.NextToken == nil { + return events, nil + } + nextToken = resp.NextToken + } } func (s sdk) DeleteStack(ctx context.Context, name string) error { diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index 41b4f5cba..5b964b6eb 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + cf "github.com/aws/aws-sdk-go/service/cloudformation" + "github.com/awslabs/goformation/v4/cloudformation" "github.com/docker/ecs-plugin/pkg/compose" ) @@ -34,7 +36,26 @@ func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error return err } - err = c.api.DescribeStackEvents(ctx, project.Name) + known := map[string]struct{}{} + err = c.api.WaitStackComplete(ctx, project.Name, func() error { + events, err := c.api.DescribeStackEvents(ctx, project.Name) + if err != nil { + return err + } + for _, event := range events { + if _, ok := known[*event.EventId]; ok { + continue + } + known[*event.EventId] = struct{}{} + + description := "-" + if event.ResourceStatusReason != nil { + description = *event.ResourceStatusReason + } + fmt.Printf("%s %q %s %s\n", *event.ResourceType, *event.LogicalResourceId, *event.ResourceStatus, description) + } + return nil + }) if err != nil { return err } @@ -48,5 +69,6 @@ type upAPI interface { CreateCluster(ctx context.Context, name string) (string, error) StackExists(ctx context.Context, name string) (bool, error) CreateStack(ctx context.Context, name string, template *cloudformation.Template) error - DescribeStackEvents(ctx context.Context, stack string) error + WaitStackComplete(ctx context.Context, name string, fn func() error) error + DescribeStackEvents(ctx context.Context, stack string) ([]*cf.StackEvent, error) }