forked from lox/parfait
/
poller.go
135 lines (115 loc) · 3.17 KB
/
poller.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package poller
import (
"errors"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
cfn "github.com/aws/aws-sdk-go/service/cloudformation"
)
type cfnInterface interface {
DescribeStackEventsPages(*cfn.DescribeStackEventsInput, func(*cfn.DescribeStackEventsOutput, bool) bool) error
}
type Poller struct {
StackName string
awsApi cfnInterface
}
func NewPoller(api cfnInterface, stackName string) *Poller {
return &Poller{
StackName: stackName,
awsApi: api,
}
}
func (p *Poller) Poll(condition EndCondition, f func(e *cfn.StackEvent)) error {
lastSeen := time.Time{}
for {
events, err := p.getEvents(lastSeen)
if err != nil {
return err
}
for i := len(events) - 1; i >= 0; i-- {
if events[i].Timestamp.After(lastSeen) {
f(events[i])
lastSeen = *events[i].Timestamp
}
}
if len(events) > 0 {
t, err := condition(p.StackName, events[0])
if err != nil {
return err
}
if t {
break
}
}
time.Sleep(2 * time.Second)
}
return nil
}
// getEvents returns all events after a given time in reverse chronological order
func (p *Poller) getEvents(after time.Time) (events []*cfn.StackEvent, err error) {
params := &cfn.DescribeStackEventsInput{
StackName: aws.String(p.StackName),
}
err = p.awsApi.DescribeStackEventsPages(params, func(page *cfn.DescribeStackEventsOutput, last bool) bool {
for _, event := range page.StackEvents {
if !event.Timestamp.After(after) {
return true
}
events = append(events, event)
// stop once we hit the most recent User Initiated event
if event.ResourceStatusReason != nil &&
*event.ResourceStatusReason == `User Initiated` {
return true
}
}
return last
})
return
}
func UntilCreatedOrUpdated(api cfnInterface, stackName string, f func(e *cfn.StackEvent)) error {
return NewPoller(api, stackName).Poll(isCreatedOrUpdated, f)
}
func UntilDeleted(api cfnInterface, stackName string, f func(e *cfn.StackEvent)) error {
err := NewPoller(api, stackName).Poll(isDeleted, f)
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
// 400: ValidationError: Stack does not exist
if awsErr.Code() == "ValidationError" {
return nil
}
}
}
return err
}
type EndCondition func(stackName string, ev *cfn.StackEvent) (bool, error)
func isCreatedOrUpdated(stackName string, ev *cfn.StackEvent) (bool, error) {
if *ev.LogicalResourceId == stackName {
switch *ev.ResourceStatus {
case cfn.ResourceStatusUpdateComplete,
cfn.ResourceStatusCreateComplete,
cfn.ResourceStatusUpdateFailed,
cfn.ResourceStatusCreateFailed,
cfn.StackStatusRollbackComplete,
cfn.StackStatusRollbackFailed,
cfn.StackStatusUpdateRollbackComplete,
cfn.StackStatusUpdateRollbackFailed:
var err error
if ev.ResourceStatusReason != nil {
err = errors.New(*ev.ResourceStatusReason)
}
return true, err
}
}
return false, nil
}
func isDeleted(stackName string, ev *cfn.StackEvent) (bool, error) {
if *ev.LogicalResourceId == stackName {
switch *ev.ResourceStatus {
case cfn.ResourceStatusDeleteComplete:
return true, nil
case cfn.ResourceStatusDeleteFailed:
return true, errors.New(*ev.ResourceStatusReason)
}
}
return false, nil
}