Skip to content

Commit 1c42a86

Browse files
committed
feat(cloudformation-stack): new setting CreateRoleToDeleteStack
1 parent e33c74f commit 1c42a86

File tree

4 files changed

+222
-140
lines changed

4 files changed

+222
-140
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ require (
3131
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 // indirect
3232
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
3333
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.26 // indirect
34+
github.com/aws/aws-sdk-go-v2/service/iam v1.38.3 // indirect
3435
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect
3536
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.7 // indirect
3637
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvK
1818
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
1919
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.26 h1:GeNJsIFHB+WW5ap2Tec4K6dzcVTsRbsT1Lra46Hv9ME=
2020
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.26/go.mod h1:zfgMpwHDXX2WGoG84xG2H+ZlPTkJUU4YUvx2svLQYWo=
21+
github.com/aws/aws-sdk-go-v2/service/iam v1.38.3 h1:2sFIoFzU1IEL9epJWubJm9Dhrn45aTNEJuwsesaCGnk=
22+
github.com/aws/aws-sdk-go-v2/service/iam v1.38.3/go.mod h1:KzlNINwfr/47tKkEhgk0r10/OZq3rjtyWy0txL3lM+I=
2123
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
2224
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
2325
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.7 h1:tB4tNw83KcajNAzaIMhkhVI2Nt8fAZd5A5ro113FEMY=

resources/cloudformation-stack.go

Lines changed: 136 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import (
1515
"github.com/aws/aws-sdk-go/service/cloudformation"
1616
"github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface"
1717

18+
"github.com/aws/aws-sdk-go-v2/service/iam"
19+
iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
20+
1821
liberrors "github.com/ekristen/libnuke/pkg/errors"
1922
"github.com/ekristen/libnuke/pkg/registry"
2023
"github.com/ekristen/libnuke/pkg/resource"
@@ -36,6 +39,7 @@ func init() {
3639
Lister: &CloudFormationStackLister{},
3740
Settings: []string{
3841
"DisableDeletionProtection",
42+
"CreateRoleToDeleteStack",
3943
},
4044
})
4145
}
@@ -46,6 +50,7 @@ func (l *CloudFormationStackLister) List(_ context.Context, o interface{}) ([]re
4650
opts := o.(*nuke.ListerOpts)
4751

4852
svc := cloudformation.New(opts.Session)
53+
iamSvc := iam.NewFromConfig(*opts.Config)
4954

5055
params := &cloudformation.DescribeStacksInput{}
5156
resources := make([]resource.Resource, 0)
@@ -56,11 +61,26 @@ func (l *CloudFormationStackLister) List(_ context.Context, o interface{}) ([]re
5661
return nil, err
5762
}
5863
for _, stack := range resp.Stacks {
59-
resources = append(resources, &CloudFormationStack{
64+
newResource := &CloudFormationStack{
6065
svc: svc,
61-
stack: stack,
66+
iamSvc: iamSvc,
67+
logger: opts.Logger,
6268
maxDeleteAttempts: CloudformationMaxDeleteAttempt,
63-
})
69+
Name: stack.StackName,
70+
Status: stack.StackStatus,
71+
description: stack.Description,
72+
parentID: stack.ParentId,
73+
roleARN: stack.RoleARN,
74+
CreationTime: stack.CreationTime,
75+
LastUpdatedTime: stack.LastUpdatedTime,
76+
Tags: stack.Tags,
77+
}
78+
79+
if newResource.LastUpdatedTime == nil {
80+
newResource.LastUpdatedTime = newResource.CreationTime
81+
}
82+
83+
resources = append(resources, newResource)
6484
}
6585

6686
if resp.NextToken == nil {
@@ -75,58 +95,106 @@ func (l *CloudFormationStackLister) List(_ context.Context, o interface{}) ([]re
7595

7696
type CloudFormationStack struct {
7797
svc cloudformationiface.CloudFormationAPI
78-
stack *cloudformation.Stack
79-
maxDeleteAttempts int
98+
iamSvc *iam.Client
8099
settings *settings.Setting
100+
logger *logrus.Entry
101+
Name *string
102+
Status *string
103+
CreationTime *time.Time
104+
LastUpdatedTime *time.Time
105+
Tags []*cloudformation.Tag
106+
description *string
107+
parentID *string
108+
roleARN *string
109+
maxDeleteAttempts int
81110
}
82111

83-
func (cfs *CloudFormationStack) Filter() error {
84-
if ptr.ToString(cfs.stack.Description) == "DO NOT MODIFY THIS STACK! This stack is managed by Config Conformance Packs." {
112+
func (r *CloudFormationStack) Filter() error {
113+
if ptr.ToString(r.description) == "DO NOT MODIFY THIS STACK! This stack is managed by Config Conformance Packs." {
85114
return fmt.Errorf("stack is managed by Config Conformance Packs")
86115
}
87116
return nil
88117
}
89118

90-
func (cfs *CloudFormationStack) Settings(setting *settings.Setting) {
91-
cfs.settings = setting
119+
func (r *CloudFormationStack) Settings(setting *settings.Setting) {
120+
r.settings = setting
92121
}
93122

94-
func (cfs *CloudFormationStack) Remove(_ context.Context) error {
95-
return cfs.removeWithAttempts(0)
123+
func (r *CloudFormationStack) Remove(ctx context.Context) error {
124+
return r.removeWithAttempts(ctx, 0)
96125
}
97126

98-
func (cfs *CloudFormationStack) removeWithAttempts(attempt int) error {
99-
if err := cfs.doRemove(); err != nil {
100-
// TODO: pass logrus in via ListerOpts so that it can be used here instead of global
127+
func (r *CloudFormationStack) createRole(ctx context.Context) error {
128+
roleParts := strings.Split(ptr.ToString(r.roleARN), "/")
129+
_, err := r.iamSvc.CreateRole(ctx, &iam.CreateRoleInput{
130+
RoleName: ptr.String(roleParts[len(roleParts)-1]),
131+
AssumeRolePolicyDocument: ptr.String(`{
132+
"Version": "2012-10-17",
133+
"Statement": [
134+
{
135+
"Effect": "Allow",
136+
"Principal": {
137+
"Service": "cloudformation.amazonaws.com"
138+
},
139+
"Action": "sts:AssumeRole"
140+
}
141+
]
142+
}`),
143+
Tags: []iamtypes.Tag{
144+
{
145+
Key: ptr.String("Managed"),
146+
Value: ptr.String("aws-nuke"),
147+
},
148+
},
149+
})
150+
151+
return err
152+
}
101153

102-
logrus.Errorf("CloudFormationStack stackName=%s attempt=%d maxAttempts=%d delete failed: %s",
103-
*cfs.stack.StackName, attempt, cfs.maxDeleteAttempts, err.Error())
154+
func (r *CloudFormationStack) removeWithAttempts(ctx context.Context, attempt int) error {
155+
if err := r.doRemove(); err != nil {
156+
r.logger.Errorf("CloudFormationStack stackName=%s attempt=%d maxAttempts=%d delete failed: %s",
157+
*r.Name, attempt, r.maxDeleteAttempts, err.Error())
104158

105159
var awsErr awserr.Error
106160
ok := errors.As(err, &awsErr)
107-
if ok && awsErr.Code() == "ValidationError" &&
108-
awsErr.Message() == "Stack ["+*cfs.stack.StackName+"] cannot be deleted while TerminationProtection is enabled" {
109-
// check if the setting for the resource is set to allow deletion protection to be disabled
110-
if cfs.settings.GetBool("DisableDeletionProtection") {
111-
logrus.Infof("CloudFormationStack stackName=%s attempt=%d maxAttempts=%d updating termination protection",
112-
*cfs.stack.StackName, attempt, cfs.maxDeleteAttempts)
113-
_, err = cfs.svc.UpdateTerminationProtection(&cloudformation.UpdateTerminationProtectionInput{
114-
EnableTerminationProtection: aws.Bool(false),
115-
StackName: cfs.stack.StackName,
116-
})
117-
if err != nil {
161+
if ok && awsErr.Code() == "ValidationError" {
162+
if awsErr.Message() == fmt.Sprintf("Role %s is invalid or cannot be assumed", *r.roleARN) {
163+
if r.settings.GetBool("CreateRoleToDeleteStack") {
164+
r.logger.Infof("CloudFormationStack stackName=%s attempt=%d maxAttempts=%d creating role to delete stack",
165+
*r.Name, attempt, r.maxDeleteAttempts)
166+
if err := r.createRole(ctx); err != nil {
167+
return err
168+
}
169+
} else {
170+
r.logger.Warnf("CloudFormationStack stackName=%s attempt=%d maxAttempts=%d set feature flag to create role to delete stack",
171+
*r.Name, attempt, r.maxDeleteAttempts)
172+
return err
173+
}
174+
} else if awsErr.Message() == fmt.Sprintf("Stack [%s] cannot be deleted while TerminationProtection is enabled", *r.Name) {
175+
// check if the setting for the resource is set to allow deletion protection to be disabled
176+
if r.settings.GetBool("DisableDeletionProtection") {
177+
r.logger.Infof("CloudFormationStack stackName=%s attempt=%d maxAttempts=%d updating termination protection",
178+
*r.Name, attempt, r.maxDeleteAttempts)
179+
_, err = r.svc.UpdateTerminationProtection(&cloudformation.UpdateTerminationProtectionInput{
180+
EnableTerminationProtection: aws.Bool(false),
181+
StackName: r.Name,
182+
})
183+
if err != nil {
184+
return err
185+
}
186+
} else {
187+
r.logger.Warnf("CloudFormationStack stackName=%s attempt=%d maxAttempts=%d set feature flag to disable deletion protection",
188+
*r.Name, attempt, r.maxDeleteAttempts)
118189
return err
119190
}
120-
} else {
121-
logrus.Warnf("CloudFormationStack stackName=%s attempt=%d maxAttempts=%d set feature flag to disable deletion protection",
122-
*cfs.stack.StackName, attempt, cfs.maxDeleteAttempts)
123-
return err
124191
}
125192
}
126-
if attempt >= cfs.maxDeleteAttempts {
193+
194+
if attempt >= r.maxDeleteAttempts {
127195
return errors.New("CFS might not be deleted after this run")
128196
} else {
129-
return cfs.removeWithAttempts(attempt + 1)
197+
return r.removeWithAttempts(ctx, attempt+1)
130198
}
131199
} else {
132200
return nil
@@ -148,9 +216,9 @@ func GetParentStack(svc cloudformationiface.CloudFormationAPI, stackID string) (
148216
return nil, nil //nolint:nilnil
149217
}
150218

151-
func (cfs *CloudFormationStack) doRemove() error { //nolint:gocyclo
152-
if cfs.stack.ParentId != nil {
153-
p, err := GetParentStack(cfs.svc, *cfs.stack.ParentId)
219+
func (r *CloudFormationStack) doRemove() error { //nolint:gocyclo
220+
if r.parentID != nil {
221+
p, err := GetParentStack(r.svc, *r.parentID)
154222
if err != nil {
155223
return err
156224
}
@@ -160,14 +228,14 @@ func (cfs *CloudFormationStack) doRemove() error { //nolint:gocyclo
160228
}
161229
}
162230

163-
o, err := cfs.svc.DescribeStacks(&cloudformation.DescribeStacksInput{
164-
StackName: cfs.stack.StackName,
231+
o, err := r.svc.DescribeStacks(&cloudformation.DescribeStacksInput{
232+
StackName: r.Name,
165233
})
166234
if err != nil {
167235
var awsErr awserr.Error
168236
if errors.As(err, &awsErr) {
169237
if awsErr.Code() == "ValidationFailed" && strings.HasSuffix(awsErr.Message(), " does not exist") {
170-
logrus.Infof("CloudFormationStack stackName=%s no longer exists", *cfs.stack.StackName)
238+
r.logger.Infof("CloudFormationStack stackName=%s no longer exists", *r.Name)
171239
return nil
172240
}
173241
}
@@ -179,16 +247,16 @@ func (cfs *CloudFormationStack) doRemove() error { //nolint:gocyclo
179247
// stack already deleted, no need to re-delete
180248
return nil
181249
} else if *stack.StackStatus == cloudformation.StackStatusDeleteInProgress {
182-
logrus.Infof("CloudFormationStack stackName=%s delete in progress. Waiting", *cfs.stack.StackName)
183-
return cfs.svc.WaitUntilStackDeleteComplete(&cloudformation.DescribeStacksInput{
184-
StackName: cfs.stack.StackName,
250+
r.logger.Infof("CloudFormationStack stackName=%s delete in progress. Waiting", *r.Name)
251+
return r.svc.WaitUntilStackDeleteComplete(&cloudformation.DescribeStacksInput{
252+
StackName: r.Name,
185253
})
186254
} else if *stack.StackStatus == cloudformation.StackStatusDeleteFailed {
187-
logrus.Infof("CloudFormationStack stackName=%s delete failed. Attempting to retain and delete stack", *cfs.stack.StackName)
255+
r.logger.Infof("CloudFormationStack stackName=%s delete failed. Attempting to retain and delete stack", *r.Name)
188256
// This means the CFS has undetectable resources.
189257
// In order to move on with nuking, we retain them in the deletion.
190-
retainableResources, err := cfs.svc.ListStackResources(&cloudformation.ListStackResourcesInput{
191-
StackName: cfs.stack.StackName,
258+
retainableResources, err := r.svc.ListStackResources(&cloudformation.ListStackResourcesInput{
259+
StackName: r.Name,
192260
})
193261
if err != nil {
194262
return err
@@ -202,71 +270,59 @@ func (cfs *CloudFormationStack) doRemove() error { //nolint:gocyclo
202270
}
203271
}
204272

205-
_, err = cfs.svc.DeleteStack(&cloudformation.DeleteStackInput{
206-
StackName: cfs.stack.StackName,
273+
if _, err = r.svc.DeleteStack(&cloudformation.DeleteStackInput{
274+
StackName: r.Name,
207275
RetainResources: retain,
208-
})
209-
if err != nil {
276+
}); err != nil {
210277
return err
211278
}
212-
return cfs.svc.WaitUntilStackDeleteComplete(&cloudformation.DescribeStacksInput{
213-
StackName: cfs.stack.StackName,
279+
280+
return r.svc.WaitUntilStackDeleteComplete(&cloudformation.DescribeStacksInput{
281+
StackName: r.Name,
214282
})
215283
} else {
216-
if err := cfs.waitForStackToStabilize(*stack.StackStatus); err != nil {
284+
if err := r.waitForStackToStabilize(*stack.StackStatus); err != nil {
217285
return err
218-
} else if _, err := cfs.svc.DeleteStack(&cloudformation.DeleteStackInput{
219-
StackName: cfs.stack.StackName,
286+
} else if _, err := r.svc.DeleteStack(&cloudformation.DeleteStackInput{
287+
StackName: r.Name,
220288
}); err != nil {
221289
return err
222-
} else if err := cfs.svc.WaitUntilStackDeleteComplete(&cloudformation.DescribeStacksInput{
223-
StackName: cfs.stack.StackName,
290+
} else if err := r.svc.WaitUntilStackDeleteComplete(&cloudformation.DescribeStacksInput{
291+
StackName: r.Name,
224292
}); err != nil {
225293
return err
226294
} else {
227295
return nil
228296
}
229297
}
230298
}
231-
func (cfs *CloudFormationStack) waitForStackToStabilize(currentStatus string) error {
299+
300+
func (r *CloudFormationStack) waitForStackToStabilize(currentStatus string) error {
232301
switch currentStatus {
233302
case cloudformation.StackStatusUpdateInProgress,
234303
cloudformation.StackStatusUpdateRollbackCompleteCleanupInProgress,
235304
cloudformation.StackStatusUpdateRollbackInProgress:
236-
logrus.Infof("CloudFormationStack stackName=%s update in progress. Waiting to stabalize", *cfs.stack.StackName)
305+
r.logger.Infof("CloudFormationStack stackName=%s update in progress. Waiting to stabalize", *r.Name)
237306

238-
return cfs.svc.WaitUntilStackUpdateComplete(&cloudformation.DescribeStacksInput{
239-
StackName: cfs.stack.StackName,
307+
return r.svc.WaitUntilStackUpdateComplete(&cloudformation.DescribeStacksInput{
308+
StackName: r.Name,
240309
})
241310
case cloudformation.StackStatusCreateInProgress,
242311
cloudformation.StackStatusRollbackInProgress:
243-
logrus.Infof("CloudFormationStack stackName=%s create in progress. Waiting to stabalize", *cfs.stack.StackName)
312+
r.logger.Infof("CloudFormationStack stackName=%s create in progress. Waiting to stabalize", *r.Name)
244313

245-
return cfs.svc.WaitUntilStackCreateComplete(&cloudformation.DescribeStacksInput{
246-
StackName: cfs.stack.StackName,
314+
return r.svc.WaitUntilStackCreateComplete(&cloudformation.DescribeStacksInput{
315+
StackName: r.Name,
247316
})
248317
default:
249318
return nil
250319
}
251320
}
252321

253-
func (cfs *CloudFormationStack) Properties() types.Properties {
254-
properties := types.NewProperties()
255-
properties.Set("Name", cfs.stack.StackName)
256-
properties.Set("CreationTime", cfs.stack.CreationTime.Format(time.RFC3339))
257-
if cfs.stack.LastUpdatedTime == nil {
258-
properties.Set("LastUpdatedTime", cfs.stack.CreationTime.Format(time.RFC3339))
259-
} else {
260-
properties.Set("LastUpdatedTime", cfs.stack.LastUpdatedTime.Format(time.RFC3339))
261-
}
262-
263-
for _, tagValue := range cfs.stack.Tags {
264-
properties.SetTag(tagValue.Key, tagValue.Value)
265-
}
266-
267-
return properties
322+
func (r *CloudFormationStack) Properties() types.Properties {
323+
return types.NewPropertiesFromStruct(r)
268324
}
269325

270-
func (cfs *CloudFormationStack) String() string {
271-
return *cfs.stack.StackName
326+
func (r *CloudFormationStack) String() string {
327+
return *r.Name
272328
}

0 commit comments

Comments
 (0)