@@ -10,10 +10,12 @@ import (
1010 "github.com/gotidy/ptr"
1111 "github.com/sirupsen/logrus"
1212
13- "github.com/aws/aws-sdk-go/aws" //nolint:staticcheck
14- "github.com/aws/aws-sdk-go/aws/awserr" //nolint:staticcheck
15- "github.com/aws/aws-sdk-go/service/cloudformation" //nolint:staticcheck
16- "github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface"
13+ "github.com/aws/aws-sdk-go/aws" //nolint:staticcheck
14+ "github.com/aws/aws-sdk-go/aws/awserr" //nolint:staticcheck
15+ "github.com/aws/aws-sdk-go/service/cloudformation" //nolint:staticcheck
16+ "github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface" //nolint:staticcheck
17+ "github.com/aws/aws-sdk-go/service/sts" //nolint:staticcheck
18+ "github.com/aws/aws-sdk-go/service/sts/stsiface" //nolint:staticcheck
1719
1820 "github.com/aws/aws-sdk-go-v2/service/iam"
1921 iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
@@ -40,17 +42,26 @@ func init() {
4042 Settings : []string {
4143 "DisableDeletionProtection" ,
4244 "CreateRoleToDeleteStack" ,
45+ "UseCurrentRoleToDeleteStack" ,
4346 },
4447 })
4548}
4649
50+ // iamRoleAPI is the subset of the IAM v2 client used by CloudFormationStack for role
51+ // create/delete operations. Defined as an interface to enable test mocking.
52+ type iamRoleAPI interface {
53+ CreateRole (ctx context.Context , params * iam.CreateRoleInput , optFns ... func (* iam.Options )) (* iam.CreateRoleOutput , error )
54+ DeleteRole (ctx context.Context , params * iam.DeleteRoleInput , optFns ... func (* iam.Options )) (* iam.DeleteRoleOutput , error )
55+ }
56+
4757type CloudFormationStackLister struct {}
4858
4959func (l * CloudFormationStackLister ) List (_ context.Context , o interface {}) ([]resource.Resource , error ) {
5060 opts := o .(* nuke.ListerOpts )
5161
5262 svc := cloudformation .New (opts .Session )
5363 iamSvc := iam .NewFromConfig (* opts .Config )
64+ stsSvc := sts .New (opts .Session )
5465
5566 params := & cloudformation.DescribeStacksInput {}
5667 resources := make ([]resource.Resource , 0 )
@@ -63,6 +74,7 @@ func (l *CloudFormationStackLister) List(_ context.Context, o interface{}) ([]re
6374 for _ , stack := range resp .Stacks {
6475 newResource := & CloudFormationStack {
6576 svc : svc ,
77+ stsSvc : stsSvc ,
6678 iamSvc : iamSvc ,
6779 logger : opts .Logger ,
6880 maxDeleteAttempts : CloudformationMaxDeleteAttempt ,
@@ -94,21 +106,24 @@ func (l *CloudFormationStackLister) List(_ context.Context, o interface{}) ([]re
94106}
95107
96108type CloudFormationStack struct {
97- svc cloudformationiface.CloudFormationAPI
98- iamSvc * iam.Client
99- 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
110- roleCreated bool
111- roleName string
109+ svc cloudformationiface.CloudFormationAPI
110+ stsSvc stsiface.STSAPI
111+ iamSvc iamRoleAPI
112+ settings * settings.Setting
113+ logger * logrus.Entry
114+ Name * string
115+ Status * string
116+ CreationTime * time.Time
117+ LastUpdatedTime * time.Time
118+ Tags []* cloudformation.Tag
119+ description * string
120+ parentID * string
121+ roleARN * string
122+ callerRoleARN * string
123+ callerRoleResolved bool
124+ maxDeleteAttempts int
125+ roleCreated bool
126+ roleName string
112127}
113128
114129func (r * CloudFormationStack ) Filter () error {
@@ -150,10 +165,14 @@ func (r *CloudFormationStack) createRole(ctx context.Context) error {
150165 },
151166 })
152167
168+ if err != nil {
169+ return err
170+ }
171+
153172 r .roleCreated = true
154173 r .roleName = roleParts [len (roleParts )- 1 ]
155174
156- return err
175+ return nil
157176}
158177
159178func (r * CloudFormationStack ) removeRole (ctx context.Context ) error {
@@ -167,6 +186,69 @@ func (r *CloudFormationStack) removeRole(ctx context.Context) error {
167186 return err
168187}
169188
189+ // resolveCallerRoleARN lazily resolves the caller's IAM role ARN from STS on first call.
190+ // This is only invoked when UseCurrentRoleToDeleteStack is enabled, avoiding unnecessary
191+ // STS API calls for users who don't use this setting.
192+ // Note: IAM roles with path prefixes (e.g. /my-path/MyRole) cannot be fully reconstructed
193+ // from the STS assumed-role ARN because STS omits the path component.
194+ func (r * CloudFormationStack ) resolveCallerRoleARN () * string {
195+ if r .callerRoleResolved {
196+ return r .callerRoleARN
197+ }
198+ r .callerRoleResolved = true
199+
200+ identity , err := r .stsSvc .GetCallerIdentity (& sts.GetCallerIdentityInput {})
201+ if err != nil {
202+ r .logger .Warnf ("CloudFormationStack stackName=%s failed to resolve caller role ARN: %s" , * r .Name , err .Error ())
203+ return nil
204+ }
205+ if identity .Arn == nil {
206+ r .logger .Warnf ("CloudFormationStack stackName=%s GetCallerIdentity returned nil ARN" , * r .Name )
207+ return nil
208+ }
209+
210+ // Convert assumed-role ARN (arn:<partition>:sts::<ACCT>:assumed-role/ROLE/SESSION)
211+ // to role ARN (arn:<partition>:iam::<ACCT>:role/ROLE)
212+ arnStr := * identity .Arn
213+ if ! strings .Contains (arnStr , ":assumed-role/" ) {
214+ r .logger .Warnf ("CloudFormationStack stackName=%s caller ARN is not an assumed-role ARN (%s), cannot resolve role ARN" , * r .Name , arnStr )
215+ return nil
216+ }
217+
218+ parts := strings .Split (arnStr , ":" )
219+ if len (parts ) < 6 {
220+ r .logger .Warnf ("CloudFormationStack stackName=%s caller ARN has unexpected format (%s)" , * r .Name , arnStr )
221+ return nil
222+ }
223+
224+ partition := parts [1 ]
225+ accountID := parts [4 ]
226+ rolePart := strings .Split (parts [5 ], "/" )
227+ if len (rolePart ) < 2 {
228+ r .logger .Warnf ("CloudFormationStack stackName=%s caller ARN resource segment has unexpected format (%s)" , * r .Name , parts [5 ])
229+ return nil
230+ }
231+
232+ roleARN := fmt .Sprintf ("arn:%s:iam::%s:role/%s" , partition , accountID , rolePart [1 ])
233+ r .callerRoleARN = & roleARN
234+ return r .callerRoleARN
235+ }
236+
237+ // applyRoleOverride sets the RoleARN on a DeleteStackInput if UseCurrentRoleToDeleteStack is enabled.
238+ func (r * CloudFormationStack ) applyRoleOverride (input * cloudformation.DeleteStackInput ) {
239+ if ! r .settings .GetBool ("UseCurrentRoleToDeleteStack" ) {
240+ return
241+ }
242+ callerRole := r .resolveCallerRoleARN ()
243+ if callerRole != nil {
244+ r .logger .Infof ("CloudFormationStack stackName=%s UseCurrentRoleToDeleteStack: overriding RoleARN with %s" , * r .Name , * callerRole )
245+ input .RoleARN = callerRole
246+ } else {
247+ r .logger .Warnf ("CloudFormationStack stackName=%s UseCurrentRoleToDeleteStack enabled " +
248+ "but callerRoleARN could not be resolved, falling back to default role behavior" , * r .Name )
249+ }
250+ }
251+
170252func (r * CloudFormationStack ) removeWithAttempts (ctx context.Context , attempt int ) error {
171253 if err := r .doRemove (); err != nil {
172254 r .logger .Errorf ("CloudFormationStack stackName=%s attempt=%d maxAttempts=%d delete failed: %s" ,
@@ -270,7 +352,8 @@ func (r *CloudFormationStack) doRemove() error { //nolint:gocyclo
270352 StackName : r .Name ,
271353 })
272354 } else if * stack .StackStatus == cloudformation .StackStatusDeleteFailed {
273- r .logger .Infof ("CloudFormationStack stackName=%s delete failed. Attempting to retain and delete stack" , * r .Name )
355+ r .logger .Infof ("CloudFormationStack stackName=%s delete failed (reason=%s). Attempting to retain and delete stack" ,
356+ * r .Name , ptr .ToString (stack .StackStatusReason ))
274357 // This means the CFS has undetectable resources.
275358 // In order to move on with nuking, we retain them in the deletion.
276359 retainableResources , err := r .svc .ListStackResources (& cloudformation.ListStackResourcesInput {
@@ -282,16 +365,22 @@ func (r *CloudFormationStack) doRemove() error { //nolint:gocyclo
282365
283366 retain := make ([]* string , 0 )
284367
285- for _ , r := range retainableResources .StackResourceSummaries {
286- if * r .ResourceStatus != cloudformation .ResourceStatusDeleteComplete {
287- retain = append (retain , r .LogicalResourceId )
368+ for _ , res := range retainableResources .StackResourceSummaries {
369+ if * res .ResourceStatus != cloudformation .ResourceStatusDeleteComplete {
370+ retain = append (retain , res .LogicalResourceId )
371+ r .logger .Infof ("CloudFormationStack stackName=%s retaining resource %s (type=%s, status=%s, reason=%s)" ,
372+ * r .Name , ptr .ToString (res .LogicalResourceId ), ptr .ToString (res .ResourceType ),
373+ ptr .ToString (res .ResourceStatus ), ptr .ToString (res .ResourceStatusReason ))
288374 }
289375 }
290376
291- if _ , err = r . svc . DeleteStack ( & cloudformation.DeleteStackInput {
377+ deleteInput := & cloudformation.DeleteStackInput {
292378 StackName : r .Name ,
293379 RetainResources : retain ,
294- }); err != nil {
380+ }
381+ r .applyRoleOverride (deleteInput )
382+
383+ if _ , err = r .svc .DeleteStack (deleteInput ); err != nil {
295384 return err
296385 }
297386
@@ -301,9 +390,14 @@ func (r *CloudFormationStack) doRemove() error { //nolint:gocyclo
301390 } else {
302391 if err := r .waitForStackToStabilize (* stack .StackStatus ); err != nil {
303392 return err
304- } else if _ , err := r .svc .DeleteStack (& cloudformation.DeleteStackInput {
393+ }
394+
395+ deleteInput := & cloudformation.DeleteStackInput {
305396 StackName : r .Name ,
306- }); err != nil {
397+ }
398+ r .applyRoleOverride (deleteInput )
399+
400+ if _ , err := r .svc .DeleteStack (deleteInput ); err != nil {
307401 return err
308402 } else if err := r .svc .WaitUntilStackDeleteComplete (& cloudformation.DescribeStacksInput {
309403 StackName : r .Name ,
0 commit comments