/
cloudformation.go
229 lines (203 loc) · 6.47 KB
/
cloudformation.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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
// Package cloudformation implements common CloudFormation utilities.
package cloudformation
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
svccfn "github.com/aws/aws-sdk-go/service/cloudformation"
"github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface"
"github.com/dustin/go-humanize"
"go.uber.org/zap"
)
// StackStatus represents the CloudFormation status.
type StackStatus struct {
Stack *svccfn.Stack
Error error
}
// Poll periodically fetches the stack status
// until the stack becomes the desired state.
func Poll(
ctx context.Context,
stopc chan struct{},
lg *zap.Logger,
cfnAPI cloudformationiface.CloudFormationAPI,
stackID string,
desiredStackStatus string,
initialWait time.Duration,
wait time.Duration,
) <-chan StackStatus {
now := time.Now()
lg.Info("polling stack",
zap.String("stack-id", stackID),
zap.String("want", desiredStackStatus),
)
ch := make(chan StackStatus, 10)
go func() {
// very first poll should be no-wait
// in case stack has already reached desired status
// wait from second interation
waitDur := time.Duration(0)
prevStatusReason, first := "", true
for ctx.Err() == nil {
select {
case <-ctx.Done():
lg.Warn("wait aborted", zap.Error(ctx.Err()))
ch <- StackStatus{Stack: nil, Error: ctx.Err()}
close(ch)
return
case <-stopc:
lg.Warn("wait stopped", zap.Error(ctx.Err()))
ch <- StackStatus{Stack: nil, Error: errors.New("wait stopped")}
close(ch)
return
case <-time.After(waitDur):
// very first poll should be no-wait
// in case stack has already reached desired status
// wait from second interation
if waitDur == time.Duration(0) {
waitDur = wait
}
}
output, err := cfnAPI.DescribeStacks(&svccfn.DescribeStacksInput{
StackName: aws.String(stackID),
})
if err != nil {
if StackNotExist(err) {
if desiredStackStatus == svccfn.ResourceStatusDeleteComplete {
lg.Info("stack is already deleted as desired; exiting", zap.Error(err))
ch <- StackStatus{Stack: nil, Error: nil}
close(ch)
return
}
lg.Warn("stack does not exist; aborting", zap.Error(ctx.Err()))
ch <- StackStatus{Stack: nil, Error: err}
close(ch)
return
}
lg.Warn("describe stack failed; retrying", zap.Error(err))
ch <- StackStatus{Stack: nil, Error: err}
continue
}
if len(output.Stacks) != 1 {
lg.Warn("expected only 1 stack; retrying", zap.String("stacks", output.GoString()))
ch <- StackStatus{Stack: nil, Error: fmt.Errorf("unexpected stack response %+v", output.GoString())}
continue
}
stack := output.Stacks[0]
currentStatus := aws.StringValue(stack.StackStatus)
currentStatusReason := aws.StringValue(stack.StackStatusReason)
if prevStatusReason == "" {
prevStatusReason = currentStatusReason
} else if currentStatusReason != "" && prevStatusReason != currentStatusReason {
prevStatusReason = currentStatusReason
}
lg.Info("polling",
zap.String("name", aws.StringValue(stack.StackName)),
zap.String("desired", desiredStackStatus),
zap.String("current", currentStatus),
zap.String("current-reason", currentStatusReason),
zap.String("started", humanize.RelTime(now, time.Now(), "ago", "from now")),
)
if desiredStackStatus != svccfn.ResourceStatusDeleteComplete &&
currentStatus == svccfn.ResourceStatusDeleteComplete {
lg.Warn("create stack failed; aborting")
ch <- StackStatus{
Stack: stack,
Error: fmt.Errorf("stack failed thus deleted (previous status reason %q, current stack status %q, current status reason %q)",
prevStatusReason,
currentStatus,
currentStatusReason,
)}
close(ch)
return
}
if desiredStackStatus == svccfn.ResourceStatusDeleteComplete &&
currentStatus == svccfn.ResourceStatusDeleteFailed {
lg.Warn("delete stack failed; aborting")
ch <- StackStatus{
Stack: stack,
Error: fmt.Errorf("failed to delete stack (previous status reason %q, current stack status %q, current status reason %q)",
prevStatusReason,
currentStatus,
currentStatusReason,
)}
close(ch)
return
}
ch <- StackStatus{Stack: stack, Error: nil}
if currentStatus == desiredStackStatus {
lg.Info("desired stack status; done", zap.String("current-stack-status", currentStatus))
close(ch)
return
}
if first {
lg.Info("sleeping", zap.Duration("initial-wait", initialWait))
select {
case <-ctx.Done():
lg.Warn("wait aborted", zap.Error(ctx.Err()))
ch <- StackStatus{Stack: nil, Error: ctx.Err()}
close(ch)
return
case <-stopc:
lg.Warn("wait stopped", zap.Error(ctx.Err()))
ch <- StackStatus{Stack: nil, Error: errors.New("wait stopped")}
close(ch)
return
case <-time.After(initialWait):
}
first = false
}
// continue for-loop
}
lg.Warn("wait aborted", zap.Error(ctx.Err()))
ch <- StackStatus{Stack: nil, Error: ctx.Err()}
close(ch)
return
}()
return ch
}
// StackCreateFailed return true if cloudformation status indicates its creation failure.
//
// CREATE_IN_PROGRESS
// CREATE_FAILED
// CREATE_COMPLETE
// ROLLBACK_IN_PROGRESS
// ROLLBACK_FAILED
// ROLLBACK_COMPLETE
// DELETE_IN_PROGRESS
// DELETE_FAILED
// DELETE_COMPLETE
// UPDATE_IN_PROGRESS
// UPDATE_COMPLETE_CLEANUP_IN_PROGRESS
// UPDATE_COMPLETE
// UPDATE_ROLLBACK_IN_PROGRESS
// UPDATE_ROLLBACK_FAILED
// UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS
// UPDATE_ROLLBACK_COMPLETE
// REVIEW_IN_PROGRESS
//
// ref. https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_Stack.html
//
func StackCreateFailed(status string) bool {
return !strings.HasPrefix(status, "REVIEW_") && !strings.HasPrefix(status, "CREATE_")
}
// StackNotExist returns true if cloudformation errror indicates
// that the stack has already been deleted.
// This message is Go client specific.
// e.g. ValidationError: Stack with id AWSTESTER-155460CAAC98A17003-CF-STACK-VPC does not exist\n\tstatus code: 400, request id: bf45410b-b863-11e8-9550-914acc220b7c
func StackNotExist(err error) bool {
if err == nil {
return false
}
return strings.Contains(err.Error(), "ValidationError:") && strings.Contains(err.Error(), " does not exist")
}
// NewTags returns a list of default CloudFormation tags.
func NewTags(input map[string]string) (tags []*svccfn.Tag) {
for k, v := range input {
tags = append(tags, &svccfn.Tag{Key: aws.String(k), Value: aws.String(v)})
}
return tags
}