From 14643ef01649a347ad3d36394ab0ac507ac0fc61 Mon Sep 17 00:00:00 2001 From: Aidan Steele Date: Fri, 10 May 2019 09:34:40 +1000 Subject: [PATCH] Improve stack event polling behaviour (closes #25 and #26) --- cmd/up.go | 4 +++- cmd/up_test.go | 50 +++++++++++++++++++++++++++++++++++++++++++ pkg/stackit/poller.go | 44 ++++++++++++++++--------------------- sample/sample.yml | 2 +- 4 files changed, 73 insertions(+), 27 deletions(-) diff --git a/cmd/up.go b/cmd/up.go index ad38fa5..bac2fdc 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -45,6 +45,7 @@ var upCmd = &cobra.Command{ stackPolicy := viper.GetString("stack-policy") template := viper.GetString("template") previousTemplate := viper.GetBool("previous-template") + alwaysSucceed := viper.GetBool("always-succeed") //noDestroy := viper.GetBool("no-destroy") //cancelOnExit := !viper.GetBool("no-cancel-on-exit") @@ -90,7 +91,7 @@ var upCmd = &cobra.Command{ } stackId := *prepared.Output.StackId - if success, _ := sit.IsSuccessfulState(stackId); !success { + if success, _ := sit.IsSuccessfulState(stackId); !success && !alwaysSucceed { os.Exit(1) } @@ -192,6 +193,7 @@ func init() { upCmd.PersistentFlags().Bool("no-cancel-on-exit", false, "") upCmd.PersistentFlags().Bool("no-timestamps", false, "") upCmd.PersistentFlags().Bool("no-color", false, "") + upCmd.PersistentFlags().Bool("always-succeed", false, "Typically stackit will return a nonzero exit code on failure. This disables that.") viper.BindPFlags(upCmd.PersistentFlags()) } diff --git a/cmd/up_test.go b/cmd/up_test.go index b4cda5b..58f64d6 100644 --- a/cmd/up_test.go +++ b/cmd/up_test.go @@ -2,13 +2,63 @@ package cmd import ( "bytes" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/magiconair/properties/assert" "io" "os" "strings" "testing" + "time" ) +func TestUp_DoesntHangWhenCreationCancelled(t *testing.T) { + if testing.Short() { + t.Skip("skip e2e tests in short mode") + } + + stackName := "test-cancelled-stack" + RootCmd.SetArgs([]string{ + "up", + "--always-succeed", + "--stack-name", stackName, + "--template", "../sample/sample.yml", + }) + + buf := &bytes.Buffer{} + out := io.MultiWriter(buf, os.Stderr) + RootCmd.SetOutput(out) + + time.AfterFunc(5*time.Second, func() { + for !strings.Contains(buf.String(), "LogGroup - CREATE_IN_PROGRESS") { + time.Sleep(time.Second) + } + + sess := session.Must(session.NewSession()) + cfn := cloudformation.New(sess) + input := &cloudformation.DeleteStackInput{StackName: &stackName} + _, err := cfn.DeleteStack(input) + if err != nil { + panic(err) + } + }) + + _ = RootCmd.Execute() + + assert.Matches(t, buf.String(), strings.TrimSpace(` +\[\d\d:\d\d:\d\d] test-cancelled-stack - REVIEW_IN_PROGRESS - User Initiated +\[\d\d:\d\d:\d\d] test-cancelled-stack - CREATE_IN_PROGRESS - User Initiated +\[\d\d:\d\d:\d\d] LogGroup - CREATE_IN_PROGRESS +\[\d\d:\d\d:\d\d] LogGroup - CREATE_IN_PROGRESS - Resource creation Initiated +\[\d\d:\d\d:\d\d] LogGroup - CREATE_COMPLETE +\[\d\d:\d\d:\d\d] test-cancelled-stack - DELETE_IN_PROGRESS - User Initiated +\[\d\d:\d\d:\d\d] LogGroup - DELETE_IN_PROGRESS +\[\d\d:\d\d:\d\d] LogGroup - DELETE_COMPLETE +\[\d\d:\d\d:\d\d] test-cancelled-stack - DELETE_COMPLETE +\{\} +`)) +} + func TestUp(t *testing.T) { if testing.Short() { t.Skip("skip e2e tests in short mode") diff --git a/pkg/stackit/poller.go b/pkg/stackit/poller.go index 101dfac..ec8ff11 100644 --- a/pkg/stackit/poller.go +++ b/pkg/stackit/poller.go @@ -11,7 +11,8 @@ type TailStackEvent struct { } func (s *Stackit) PollStackEvents(stackId, token string, callback func(event TailStackEvent)) (*TailStackEvent, error) { - lastSentEventId := "" + mostRecentEventTimestamp := time.Now().AddDate(0, 0, -1) + haveSeenExpectedToken := false for { time.Sleep(3 * time.Second) @@ -22,56 +23,49 @@ func (s *Stackit) PollStackEvents(stackId, token string, callback func(event Tai StackName: &stackId, }, func(page *cloudformation.DescribeStackEventsOutput, lastPage bool) bool { for _, event := range page.StackEvents { - crt := "nil" - if event.ClientRequestToken != nil { - crt = *event.ClientRequestToken + if event.ClientRequestToken != nil && *event.ClientRequestToken == token { + haveSeenExpectedToken = true } - if token == "" { - token = crt + if haveSeenExpectedToken && event.Timestamp.After(mostRecentEventTimestamp) { + events = append(events, event) } - - if *event.EventId == lastSentEventId || crt != token { - return false - } - - events = append(events, event) } - return true + + earliestEvent := page.StackEvents[len(page.StackEvents)-1] + shouldPaginate := earliestEvent.Timestamp.After(mostRecentEventTimestamp) + mostRecentEventTimestamp = *page.StackEvents[0].Timestamp + return shouldPaginate }) if err != nil { if awsErr, ok := err.(awserr.Error); ok { code := awsErr.Code() - if code == "ThrottlingException" { - continue + if code != "ThrottlingException" { + return nil, err } + } else { + return nil, err } - return nil, err } if len(events) == 0 { continue } - lastSentEventId = *events[0].EventId stack, err := s.Describe(*events[0].StackId) if err != nil { return nil, err } - terminal := IsTerminalStatus(*stack.StackStatus) - for ev_i := len(events) - 1; ev_i >= 0; ev_i-- { event := events[ev_i] tailEvent := TailStackEvent{*event} - - done := terminal && ev_i == 0 - if done { - return &tailEvent, nil - } - callback(tailEvent) } + + if IsTerminalStatus(*stack.StackStatus) { + return &TailStackEvent{*events[0]}, nil + } } } diff --git a/sample/sample.yml b/sample/sample.yml index 8f250cb..2786e6d 100644 --- a/sample/sample.yml +++ b/sample/sample.yml @@ -51,7 +51,7 @@ Resources: LogGroup: Type: AWS::Logs::LogGroup Properties: - LogGroupName: test-stack-LogGroup + LogGroupName: !Sub ${AWS::StackName}-LogGroup Outputs: LogGroup: Value: !Ref LogGroup