24
24
DescribeStackResourcesOutput ,
25
25
DescribeStacksOutput ,
26
26
DisableRollback ,
27
+ EnableTerminationProtection ,
27
28
ExecuteChangeSetOutput ,
28
29
ExecutionStatus ,
29
30
GetTemplateOutput ,
48
49
TemplateStage ,
49
50
UpdateStackInput ,
50
51
UpdateStackOutput ,
52
+ UpdateTerminationProtectionOutput ,
51
53
)
52
54
from localstack .services .cloudformation import api_utils
53
55
from localstack .services .cloudformation .engine import template_preparer
@@ -90,14 +92,12 @@ def is_changeset_arn(change_set_name_or_id: str) -> bool:
90
92
return ARN_CHANGESET_REGEX .match (change_set_name_or_id ) is not None
91
93
92
94
93
- class StackWithNameNotFoundError (ValidationError ):
94
- def __init__ (self , stack_name : str ):
95
- super ().__init__ (f"Stack [{ stack_name } ] does not exist" )
96
-
97
-
98
- class StackWithIdNotFoundError (ValidationError ):
99
- def __init__ (self , stack_id : str ):
100
- super ().__init__ ("Stack with id <stack-name> does not exist" )
95
+ class StackNotFoundError (ValidationError ):
96
+ def __init__ (self , stack_name_or_id : str ):
97
+ if is_stack_arn (stack_name_or_id ):
98
+ super ().__init__ (f"Stack with id { stack_name_or_id } does not exist" )
99
+ else :
100
+ super ().__init__ (f"Stack [{ stack_name_or_id } ] does not exist" )
101
101
102
102
103
103
def find_stack_v2 (state : CloudFormationStore , stack_name : str | None ) -> Stack | None :
@@ -128,7 +128,7 @@ def find_change_set_v2(
128
128
if stack_name is not None :
129
129
stack = find_stack_v2 (state , stack_name )
130
130
if not stack :
131
- raise StackWithNameNotFoundError (stack_name )
131
+ raise StackNotFoundError (stack_name )
132
132
133
133
for change_set_id in stack .change_set_ids :
134
134
change_set_candidate = state .change_sets [change_set_id ]
@@ -250,18 +250,14 @@ def create_change_set(
250
250
request_payload = request ,
251
251
template = structured_template ,
252
252
template_body = template_body ,
253
+ initial_status = StackStatus .REVIEW_IN_PROGRESS ,
253
254
)
254
255
state .stacks_v2 [stack .stack_id ] = stack
255
256
else :
256
257
if not active_stack_candidates :
257
258
raise ValidationError (f"Stack '{ stack_name } ' does not exist." )
258
259
stack = active_stack_candidates [0 ]
259
260
260
- if stack .status in [StackStatus .CREATE_COMPLETE , StackStatus .UPDATE_COMPLETE ]:
261
- stack .set_stack_status (StackStatus .UPDATE_IN_PROGRESS )
262
- else :
263
- stack .set_stack_status (StackStatus .REVIEW_IN_PROGRESS )
264
-
265
261
# TODO: test if rollback status is allowed as well
266
262
if (
267
263
change_set_type == ChangeSetType .CREATE
@@ -330,7 +326,19 @@ def create_change_set(
330
326
previous_update_model = previous_update_model ,
331
327
)
332
328
333
- change_set .set_change_set_status (ChangeSetStatus .CREATE_COMPLETE )
329
+ # TODO: handle the empty change set case
330
+ if not change_set .has_changes ():
331
+ change_set .set_change_set_status (ChangeSetStatus .FAILED )
332
+ change_set .set_execution_status (ExecutionStatus .UNAVAILABLE )
333
+ change_set .status_reason = "The submitted information didn't contain changes. Submit different information to create a change set."
334
+ else :
335
+ if stack .status in [StackStatus .CREATE_COMPLETE , StackStatus .UPDATE_COMPLETE ]:
336
+ stack .set_stack_status (StackStatus .UPDATE_IN_PROGRESS )
337
+ else :
338
+ stack .set_stack_status (StackStatus .REVIEW_IN_PROGRESS )
339
+
340
+ change_set .set_change_set_status (ChangeSetStatus .CREATE_COMPLETE )
341
+
334
342
stack .change_set_id = change_set .change_set_id
335
343
stack .change_set_ids .append (change_set .change_set_id )
336
344
state .change_sets [change_set .change_set_id ] = change_set
@@ -438,6 +446,8 @@ def _describe_change_set(
438
446
for (key , value ) in change_set .stack .resolved_parameters .items ()
439
447
],
440
448
Changes = changes ,
449
+ Capabilities = change_set .stack .capabilities ,
450
+ StatusReason = change_set .status_reason ,
441
451
)
442
452
return result
443
453
@@ -455,6 +465,7 @@ def describe_change_set(
455
465
# only relevant if change_set_name isn't an ARN
456
466
state = get_cloudformation_store (context .account_id , context .region )
457
467
change_set = find_change_set_v2 (state , change_set_name , stack_name )
468
+
458
469
if not change_set :
459
470
raise ChangeSetNotFoundException (f"ChangeSet [{ change_set_name } ] does not exist" )
460
471
result = self ._describe_change_set (
@@ -596,7 +607,8 @@ def describe_stacks(
596
607
state = get_cloudformation_store (context .account_id , context .region )
597
608
stack = find_stack_v2 (state , stack_name )
598
609
if not stack :
599
- raise StackWithIdNotFoundError (stack_name )
610
+ raise StackNotFoundError (stack_name )
611
+ # TODO: move describe_details method to provider
600
612
return DescribeStacksOutput (Stacks = [stack .describe_details ()])
601
613
602
614
@handler ("ListStacks" )
@@ -645,7 +657,7 @@ def describe_stack_resources(
645
657
state = get_cloudformation_store (context .account_id , context .region )
646
658
stack = find_stack_v2 (state , stack_name )
647
659
if not stack :
648
- raise StackWithIdNotFoundError (stack_name )
660
+ raise StackNotFoundError (stack_name )
649
661
# TODO: filter stack by PhysicalResourceId!
650
662
statuses = []
651
663
for resource_id , resource_status in stack .resource_states .items ():
@@ -663,10 +675,14 @@ def describe_stack_events(
663
675
next_token : NextToken = None ,
664
676
** kwargs ,
665
677
) -> DescribeStackEventsOutput :
678
+ if not stack_name :
679
+ raise ValidationError (
680
+ "1 validation error detected: Value null at 'stackName' failed to satisfy constraint: Member must not be null"
681
+ )
666
682
state = get_cloudformation_store (context .account_id , context .region )
667
683
stack = find_stack_v2 (state , stack_name )
668
684
if not stack :
669
- raise StackWithIdNotFoundError (stack_name )
685
+ raise StackNotFoundError (stack_name )
670
686
return DescribeStackEventsOutput (StackEvents = stack .events )
671
687
672
688
@handler ("GetTemplate" )
@@ -685,7 +701,7 @@ def get_template(
685
701
elif stack_name :
686
702
stack = find_stack_v2 (state , stack_name )
687
703
else :
688
- raise StackWithIdNotFoundError (stack_name )
704
+ raise StackNotFoundError (stack_name )
689
705
690
706
if template_stage == TemplateStage .Processed and "Transform" in stack .template_body :
691
707
template_body = json .dumps (stack .processed_template )
@@ -709,7 +725,7 @@ def get_template_summary(
709
725
if stack_name :
710
726
stack = find_stack_v2 (state , stack_name )
711
727
if not stack :
712
- raise StackWithIdNotFoundError (stack_name )
728
+ raise StackNotFoundError (stack_name )
713
729
template = stack .template
714
730
else :
715
731
template_body = request .get ("TemplateBody" )
@@ -758,6 +774,22 @@ def get_template_summary(
758
774
759
775
return result
760
776
777
+ @handler ("UpdateTerminationProtection" )
778
+ def update_termination_protection (
779
+ self ,
780
+ context : RequestContext ,
781
+ enable_termination_protection : EnableTerminationProtection ,
782
+ stack_name : StackNameOrId ,
783
+ ** kwargs ,
784
+ ) -> UpdateTerminationProtectionOutput :
785
+ state = get_cloudformation_store (context .account_id , context .region )
786
+ stack = find_stack_v2 (state , stack_name )
787
+ if not stack :
788
+ raise StackNotFoundError (stack_name )
789
+
790
+ stack .enable_termination_protection = enable_termination_protection
791
+ return UpdateTerminationProtectionOutput (StackId = stack .stack_id )
792
+
761
793
@handler ("UpdateStack" , expand = False )
762
794
def update_stack (
763
795
self ,
0 commit comments