Browse Source

Collect events while waiting for stack to complete

Signed-off-by: Nicolas De Loof <[email protected]>
Nicolas De Loof 5 years ago
parent
commit
678f4018f0
3 changed files with 86 additions and 12 deletions
  1. 21 5
      ecs/pkg/amazon/mock/api.go
  2. 41 5
      ecs/pkg/amazon/sdk.go
  3. 24 2
      ecs/pkg/amazon/up.go

+ 21 - 5
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)
+}

+ 41 - 5
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
+		}
+
+		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) error {
+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 {

+ 24 - 2
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)
 }