Skip to content

Commit 96fe37b

Browse files
authored
Merge pull request #36982 from hashicorp/f-cloudformation-stack-set
cloudformation/stack_set: Add retry on create
2 parents c3d8b4d + a41a2d5 commit 96fe37b

File tree

4 files changed

+92
-1
lines changed

4 files changed

+92
-1
lines changed

.changelog/36982.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
resource/aws_cloudformation_stack_set: Add retry when creating to potentially help with eventual consistency problems
3+
```

internal/service/cloudformation/stack_set.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,57 @@ func resourceStackSetCreate(ctx context.Context, d *schema.ResourceData, meta in
266266
input.TemplateURL = aws.String(v.(string))
267267
}
268268

269-
_, err := conn.CreateStackSetWithContext(ctx, input)
269+
_, err := tfresource.RetryWhen(ctx, propagationTimeout,
270+
func() (interface{}, error) {
271+
output, err := conn.CreateStackSetWithContext(ctx, input)
272+
if err != nil {
273+
return nil, err
274+
}
275+
276+
operation, err := WaitStackSetCreated(ctx, conn, name, d.Get("call_as").(string), d.Timeout(schema.TimeoutCreate))
277+
if err != nil {
278+
return nil, fmt.Errorf("waiting for completion (%s): %w", aws.StringValue(output.StackSetId), err)
279+
}
280+
return operation, nil
281+
},
282+
func(err error) (bool, error) {
283+
if err == nil {
284+
return false, nil
285+
}
286+
287+
message := err.Error()
288+
289+
// IAM eventual consistency
290+
if strings.Contains(message, "AccountGate check failed") {
291+
return true, err
292+
}
293+
294+
// IAM eventual consistency
295+
// User: XXX is not authorized to perform: cloudformation:CreateStack on resource: YYY
296+
if strings.Contains(message, "is not authorized") {
297+
return true, err
298+
}
299+
300+
// IAM eventual consistency
301+
// XXX role has insufficient YYY permissions
302+
if strings.Contains(message, "role has insufficient") {
303+
return true, err
304+
}
305+
306+
// IAM eventual consistency
307+
// Account XXX should have YYY role with trust relationship to Role ZZZ
308+
if strings.Contains(message, "role with trust relationship") {
309+
return true, err
310+
}
311+
312+
// IAM eventual consistency
313+
if strings.Contains(message, "The security token included in the request is invalid") {
314+
return true, err
315+
}
316+
317+
return false, err
318+
},
319+
)
270320

271321
if err != nil {
272322
return sdkdiag.AppendErrorf(diags, "creating CloudFormation StackSet (%s): %s", name, err)

internal/service/cloudformation/status.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,22 @@ func StatusChangeSet(ctx context.Context, conn *cloudformation.CloudFormation, s
2828
}
2929
}
3030

31+
func StatusStackSet(ctx context.Context, conn *cloudformation.CloudFormation, name, callAs string) retry.StateRefreshFunc {
32+
return func() (interface{}, string, error) {
33+
output, err := FindStackSetByName(ctx, conn, name, callAs)
34+
35+
if tfresource.NotFound(err) {
36+
return nil, "", nil
37+
}
38+
39+
if err != nil {
40+
return nil, "", err
41+
}
42+
43+
return output, aws.StringValue(output.Status), nil
44+
}
45+
}
46+
3147
func StatusStackSetOperation(ctx context.Context, conn *cloudformation.CloudFormation, stackSetName, operationID, callAs string) retry.StateRefreshFunc {
3248
return func() (interface{}, string, error) {
3349
output, err := FindStackSetOperationByStackSetNameAndOperationID(ctx, conn, stackSetName, operationID, callAs)

internal/service/cloudformation/wait.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,28 @@ func WaitChangeSetCreated(ctx context.Context, conn *cloudformation.CloudFormati
4040
return nil, err
4141
}
4242

43+
func WaitStackSetCreated(ctx context.Context, conn *cloudformation.CloudFormation, name, callAs string, timeout time.Duration) (*cloudformation.StackSet, error) {
44+
stateConf := retry.StateChangeConf{
45+
Pending: []string{},
46+
Target: []string{cloudformation.StackSetStatusActive},
47+
Timeout: ChangeSetCreatedTimeout,
48+
Refresh: StatusStackSet(ctx, conn, name, callAs),
49+
Delay: 15 * time.Second,
50+
}
51+
52+
outputRaw, err := stateConf.WaitForStateContext(ctx)
53+
54+
if output, ok := outputRaw.(*cloudformation.StackSet); ok {
55+
if status := aws.StringValue(output.Status); status == cloudformation.ChangeSetStatusFailed {
56+
tfresource.SetLastError(err, fmt.Errorf("describing CloudFormation Stack Set (%s) results: %w", name, err))
57+
}
58+
59+
return output, err
60+
}
61+
62+
return nil, err
63+
}
64+
4365
const (
4466
stackSetOperationDelay = 5 * time.Second
4567
)

0 commit comments

Comments
 (0)