Skip to content

Commit

Permalink
Stack event tracking and outputs.
Browse files Browse the repository at this point in the history
  • Loading branch information
mana-sys committed Jan 24, 2020
1 parent 4dd2d5e commit da47c68
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 14 deletions.
9 changes: 9 additions & 0 deletions internal/cli/clierrors/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package clierrors

type CliError struct {
message string
}

var (
//DeployCanceledError
)
15 changes: 12 additions & 3 deletions internal/cli/command/deploy/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,20 +255,29 @@ Do you want to apply this change set?
return err
}

fmt.Printf("Sent change set execution request.\n\n")
fmt.Printf("Sent the change set execution request. You may track the stack status below:\n\n")

// Wait until stack completion is done.
if err := monitorStack(cfn, *changeSetOutput.StackId, *changeSetOutput.StackName); err != nil {
stackOut, err := monitorStack(cfn, *changeSetOutput.StackId, *changeSetOutput.StackName, OpUpdate)
if err != nil {
return err
}

// Print change set execution summary.
fmt.Println()
color.Green("Change set execution complete!")
fmt.Printf("Summary of changes: %s, %s, %s\n",
fmt.Printf("Summary of changes: %s, %s, %s\n\n",
color.GreenString("%d added", numAdd),
color.YellowString("%d updated", numUpdate),
color.RedString("%d removed", numRemove),
)

// Print stack outputs.
fmt.Println("Outputs: ")
for _, output := range stackOut.Stacks[0].Outputs {
fmt.Printf("%s = %s\n", *output.OutputKey, *output.OutputValue)
}

return nil
}

Expand Down
31 changes: 20 additions & 11 deletions internal/cli/command/deploy/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"strings"
"time"

"github.com/mana-sys/adhesive/pkg/watchstack"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudformation"
)
Expand Down Expand Up @@ -43,7 +45,12 @@ func isFailed(status string) bool {
strings.HasSuffix(status, "ROLLBACK_FAILED")
}

func monitorStack(cfn *cloudformation.CloudFormation, stackId, stackName string) error {
func monitorStack(cfn *cloudformation.CloudFormation, stackId, stackName string, op stackOp) (*cloudformation.DescribeStacksOutput, error) {
var (
err error
out *cloudformation.DescribeStacksOutput
)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

Expand All @@ -54,28 +61,30 @@ func monitorStack(cfn *cloudformation.CloudFormation, stackId, stackName string)
uiInterval: 5 * time.Second,
}

go consoleMonitorStack(ctx, state)
for range time.Tick(5 * time.Second) {
out, err := cfn.DescribeStacks(&cloudformation.DescribeStacksInput{
w := watchstack.New(cfn)

go w.StreamEvents(ctx, stackId)
for range time.Tick(1 * time.Second) {
out, err = cfn.DescribeStacks(&cloudformation.DescribeStacksInput{
StackName: aws.String(stackId),
})
if err != nil {
return err
return nil, err
}

stack := out.Stacks[0]
if isSuccess(*stack.StackStatus) {
break
} else if isFailed(*stack.StackStatus) {
return StackOpError(fmt.Sprintf("operation failed with status %s: %s",
return nil, StackOpError(fmt.Sprintf("operation failed with status %s: %s",
*stack.StackStatus, *stack.StackStatusReason))
}
}

fmt.Printf("%s: operation complete after %v\n", stackName,
fmt.Printf("\n%s: operation complete after %v\n", stackName,
time.Now().Round(1*time.Second).Sub(state.start))

return nil
return out, nil
}

func consoleMonitorStack(ctx context.Context, state stackMonitorState) {
Expand All @@ -84,11 +93,11 @@ func consoleMonitorStack(ctx context.Context, state stackMonitorState) {
var message string
switch state.op {
case OpCreate:
message = "still creating..."
message = "Still creating..."
case OpDelete:
message = "still deleting..."
message = "Still deleting..."
case OpUpdate:
message = "still updating..."
message = "Still updating..."
default:
panic("unreachable")
}
Expand Down
80 changes: 80 additions & 0 deletions pkg/watchstack/watchstack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package watchstack

import (
"context"
"fmt"
"io"
"os"
"time"

"github.com/fatih/color"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudformation"
"github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface"
)

// WatchLog represents a CloudFormation events watcher. It will stream events
// to the configured destination until canceled. Adapted from Tyler Brock's
// github.com/TylerBrock/saw repository.
type WatchStack struct {
cfn cloudformationiface.CloudFormationAPI
PollInterval time.Duration
W io.Writer
}

func New(cfn cloudformationiface.CloudFormationAPI) *WatchStack {
return &WatchStack{
cfn: cfn,
PollInterval: time.Second,
W: os.Stdout,
}
}

func (w *WatchStack) StreamEvents(ctx context.Context, stackId string) error {
var (
lastSeenTime time.Time
seenEvents map[string]struct{}
)

input := &cloudformation.DescribeStackEventsInput{
StackName: aws.String(stackId),
}
pageHandler := func(output *cloudformation.DescribeStackEventsOutput, last bool) bool {
for _, event := range output.StackEvents {
// Filter out any events that occurred before that last seen timestamp.
// If we see a new timestamp, then we set the last seen timestamp to that
// new timestamp. We can also clear the set of seen events.
if event.Timestamp.Before(lastSeenTime) {
continue
} else if event.Timestamp.After(lastSeenTime) {
lastSeenTime = *event.Timestamp
seenEvents = make(map[string]struct{})
}

// If we have already seen this event, don't output it. Otherwise, we
// can output the event.
if _, ok := seenEvents[*event.EventId]; ok {
continue
}

fmt.Fprintf(w.W, "[%s] %s (%s): %s\n",
color.WhiteString(event.Timestamp.Format(time.RFC3339)),
*event.LogicalResourceId,
*event.ResourceType,
color.GreenString(*event.ResourceStatus),
)
seenEvents[*event.EventId] = struct{}{}
}
return !last
}

for {
err := w.cfn.DescribeStackEventsPages(input, pageHandler)
if err != nil {
return err
}

time.Sleep(w.PollInterval)
}
}

0 comments on commit da47c68

Please sign in to comment.