2424 DescribeStackResourcesOutput ,
2525 DescribeStacksOutput ,
2626 DisableRollback ,
27+ EnableTerminationProtection ,
2728 ExecuteChangeSetOutput ,
2829 ExecutionStatus ,
2930 GetTemplateOutput ,
4849 TemplateStage ,
4950 UpdateStackInput ,
5051 UpdateStackOutput ,
52+ UpdateTerminationProtectionOutput ,
5153)
5254from localstack .services .cloudformation import api_utils
5355from localstack .services .cloudformation .engine import template_preparer
@@ -90,14 +92,12 @@ def is_changeset_arn(change_set_name_or_id: str) -> bool:
9092 return ARN_CHANGESET_REGEX .match (change_set_name_or_id ) is not None
9193
9294
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" )
101101
102102
103103def find_stack_v2 (state : CloudFormationStore , stack_name : str | None ) -> Stack | None :
@@ -128,7 +128,7 @@ def find_change_set_v2(
128128 if stack_name is not None :
129129 stack = find_stack_v2 (state , stack_name )
130130 if not stack :
131- raise StackWithNameNotFoundError (stack_name )
131+ raise StackNotFoundError (stack_name )
132132
133133 for change_set_id in stack .change_set_ids :
134134 change_set_candidate = state .change_sets [change_set_id ]
@@ -250,18 +250,14 @@ def create_change_set(
250250 request_payload = request ,
251251 template = structured_template ,
252252 template_body = template_body ,
253+ initial_status = StackStatus .REVIEW_IN_PROGRESS ,
253254 )
254255 state .stacks_v2 [stack .stack_id ] = stack
255256 else :
256257 if not active_stack_candidates :
257258 raise ValidationError (f"Stack '{ stack_name } ' does not exist." )
258259 stack = active_stack_candidates [0 ]
259260
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-
265261 # TODO: test if rollback status is allowed as well
266262 if (
267263 change_set_type == ChangeSetType .CREATE
@@ -330,7 +326,19 @@ def create_change_set(
330326 previous_update_model = previous_update_model ,
331327 )
332328
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+
334342 stack .change_set_id = change_set .change_set_id
335343 stack .change_set_ids .append (change_set .change_set_id )
336344 state .change_sets [change_set .change_set_id ] = change_set
@@ -438,6 +446,8 @@ def _describe_change_set(
438446 for (key , value ) in change_set .stack .resolved_parameters .items ()
439447 ],
440448 Changes = changes ,
449+ Capabilities = change_set .stack .capabilities ,
450+ StatusReason = change_set .status_reason ,
441451 )
442452 return result
443453
@@ -455,6 +465,7 @@ def describe_change_set(
455465 # only relevant if change_set_name isn't an ARN
456466 state = get_cloudformation_store (context .account_id , context .region )
457467 change_set = find_change_set_v2 (state , change_set_name , stack_name )
468+
458469 if not change_set :
459470 raise ChangeSetNotFoundException (f"ChangeSet [{ change_set_name } ] does not exist" )
460471 result = self ._describe_change_set (
@@ -596,7 +607,8 @@ def describe_stacks(
596607 state = get_cloudformation_store (context .account_id , context .region )
597608 stack = find_stack_v2 (state , stack_name )
598609 if not stack :
599- raise StackWithIdNotFoundError (stack_name )
610+ raise StackNotFoundError (stack_name )
611+ # TODO: move describe_details method to provider
600612 return DescribeStacksOutput (Stacks = [stack .describe_details ()])
601613
602614 @handler ("ListStacks" )
@@ -645,7 +657,7 @@ def describe_stack_resources(
645657 state = get_cloudformation_store (context .account_id , context .region )
646658 stack = find_stack_v2 (state , stack_name )
647659 if not stack :
648- raise StackWithIdNotFoundError (stack_name )
660+ raise StackNotFoundError (stack_name )
649661 # TODO: filter stack by PhysicalResourceId!
650662 statuses = []
651663 for resource_id , resource_status in stack .resource_states .items ():
@@ -663,10 +675,14 @@ def describe_stack_events(
663675 next_token : NextToken = None ,
664676 ** kwargs ,
665677 ) -> 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+ )
666682 state = get_cloudformation_store (context .account_id , context .region )
667683 stack = find_stack_v2 (state , stack_name )
668684 if not stack :
669- raise StackWithIdNotFoundError (stack_name )
685+ raise StackNotFoundError (stack_name )
670686 return DescribeStackEventsOutput (StackEvents = stack .events )
671687
672688 @handler ("GetTemplate" )
@@ -685,7 +701,7 @@ def get_template(
685701 elif stack_name :
686702 stack = find_stack_v2 (state , stack_name )
687703 else :
688- raise StackWithIdNotFoundError (stack_name )
704+ raise StackNotFoundError (stack_name )
689705
690706 if template_stage == TemplateStage .Processed and "Transform" in stack .template_body :
691707 template_body = json .dumps (stack .processed_template )
@@ -709,7 +725,7 @@ def get_template_summary(
709725 if stack_name :
710726 stack = find_stack_v2 (state , stack_name )
711727 if not stack :
712- raise StackWithIdNotFoundError (stack_name )
728+ raise StackNotFoundError (stack_name )
713729 template = stack .template
714730 else :
715731 template_body = request .get ("TemplateBody" )
@@ -758,6 +774,22 @@ def get_template_summary(
758774
759775 return result
760776
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+
761793 @handler ("UpdateStack" , expand = False )
762794 def update_stack (
763795 self ,
0 commit comments