Skip to content

Commit 82db418

Browse files
authored
add implementation of DeleteChangeSet for CFnV2 (#12876)
1 parent a5822ea commit 82db418

File tree

6 files changed

+82
-9
lines changed

6 files changed

+82
-9
lines changed

localstack-core/localstack/services/cloudformation/v2/provider.py

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
CreateChangeSetOutput,
1717
CreateStackInput,
1818
CreateStackOutput,
19+
DeleteChangeSetOutput,
1920
DeletionMode,
2021
DescribeChangeSetOutput,
2122
DescribeStackEventsOutput,
@@ -82,9 +83,14 @@ def is_changeset_arn(change_set_name_or_id: str) -> bool:
8283
return ARN_CHANGESET_REGEX.match(change_set_name_or_id) is not None
8384

8485

85-
class StackNotFoundError(ValidationError):
86+
class StackWithNameNotFoundError(ValidationError):
8687
def __init__(self, stack_name: str):
87-
super().__init__(f"Stack with id {stack_name} does not exist")
88+
super().__init__(f"Stack [{stack_name}] does not exist")
89+
90+
91+
class StackWithIdNotFoundError(ValidationError):
92+
def __init__(self, stack_id: str):
93+
super().__init__("Stack with id <stack-name> does not exist")
8894

8995

9096
def find_stack_v2(state: CloudFormationStore, stack_name: str | None) -> Stack | None:
@@ -115,7 +121,7 @@ def find_change_set_v2(
115121
if stack_name is not None:
116122
stack = find_stack_v2(state, stack_name)
117123
if not stack:
118-
raise StackNotFoundError(stack_name)
124+
raise StackWithNameNotFoundError(stack_name)
119125

120126
for change_set_id in stack.change_set_ids:
121127
change_set_candidate = state.change_sets[change_set_id]
@@ -448,6 +454,33 @@ def describe_change_set(
448454
)
449455
return result
450456

457+
@handler("DeleteChangeSet")
458+
def delete_change_set(
459+
self,
460+
context: RequestContext,
461+
change_set_name: ChangeSetNameOrId,
462+
stack_name: StackNameOrId = None,
463+
**kwargs,
464+
) -> DeleteChangeSetOutput:
465+
state = get_cloudformation_store(context.account_id, context.region)
466+
467+
if is_changeset_arn(change_set_name):
468+
change_set = state.change_sets.get(change_set_name)
469+
elif not is_changeset_arn(change_set_name) and stack_name:
470+
change_set = find_change_set_v2(state, change_set_name, stack_name)
471+
else:
472+
raise ValidationError(
473+
"StackName must be specified if ChangeSetName is not specified as an ARN."
474+
)
475+
476+
if not change_set:
477+
return DeleteChangeSetOutput()
478+
479+
change_set.stack.change_set_ids.remove(change_set.change_set_id)
480+
state.change_sets.pop(change_set.change_set_id)
481+
482+
return DeleteChangeSetOutput()
483+
451484
@handler("CreateStack", expand=False)
452485
def create_stack(self, context: RequestContext, request: CreateStackInput) -> CreateStackOutput:
453486
try:
@@ -548,7 +581,7 @@ def describe_stacks(
548581
state = get_cloudformation_store(context.account_id, context.region)
549582
stack = find_stack_v2(state, stack_name)
550583
if not stack:
551-
raise StackNotFoundError(stack_name)
584+
raise StackWithIdNotFoundError(stack_name)
552585
return DescribeStacksOutput(Stacks=[stack.describe_details()])
553586

554587
@handler("DescribeStackResources")
@@ -565,7 +598,7 @@ def describe_stack_resources(
565598
state = get_cloudformation_store(context.account_id, context.region)
566599
stack = find_stack_v2(state, stack_name)
567600
if not stack:
568-
raise StackNotFoundError(stack_name)
601+
raise StackWithIdNotFoundError(stack_name)
569602
# TODO: filter stack by PhysicalResourceId!
570603
statuses = []
571604
for resource_id, resource_status in stack.resource_states.items():
@@ -586,7 +619,7 @@ def describe_stack_events(
586619
state = get_cloudformation_store(context.account_id, context.region)
587620
stack = find_stack_v2(state, stack_name)
588621
if not stack:
589-
raise StackNotFoundError(stack_name)
622+
raise StackWithIdNotFoundError(stack_name)
590623
return DescribeStackEventsOutput(StackEvents=stack.events)
591624

592625
@handler("GetTemplateSummary", expand=False)
@@ -601,7 +634,7 @@ def get_template_summary(
601634
if stack_name:
602635
stack = find_stack_v2(state, stack_name)
603636
if not stack:
604-
raise StackNotFoundError(stack_name)
637+
raise StackWithIdNotFoundError(stack_name)
605638
template = stack.template
606639
else:
607640
template_body = request.get("TemplateBody")

tests/aws/services/cloudformation/api/test_changesets.snapshot.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@
172172
}
173173
},
174174
"tests/aws/services/cloudformation/api/test_changesets.py::test_delete_change_set_exception": {
175-
"recorded-date": "12-03-2025, 10:14:25",
175+
"recorded-date": "21-07-2025, 18:04:27",
176176
"recorded-content": {
177177
"e1": {
178178
"Error": {

tests/aws/services/cloudformation/api/test_changesets.validation.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,13 @@
6060
"last_validated_date": "2023-11-22T07:49:15+00:00"
6161
},
6262
"tests/aws/services/cloudformation/api/test_changesets.py::test_delete_change_set_exception": {
63-
"last_validated_date": "2025-03-12T10:14:25+00:00"
63+
"last_validated_date": "2025-07-21T18:04:27+00:00",
64+
"durations_in_seconds": {
65+
"setup": 0.31,
66+
"call": 0.4,
67+
"teardown": 0.0,
68+
"total": 0.71
69+
}
6470
},
6571
"tests/aws/services/cloudformation/api/test_changesets.py::test_deleted_changeset": {
6672
"last_validated_date": "2022-08-11T09:11:47+00:00"

tests/aws/services/cloudformation/api/test_stacks.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,3 +1069,12 @@ def test_stack_resource_not_found(deploy_cfn_template, aws_client, snapshot):
10691069

10701070
snapshot.add_transformer(snapshot.transform.regex(stack.stack_name, "<stack-name>"))
10711071
snapshot.match("Error", ex.value.response)
1072+
1073+
1074+
@markers.aws.validated
1075+
def test_non_existing_stack_message(aws_client, snapshot):
1076+
with pytest.raises(botocore.exceptions.ClientError) as ex:
1077+
aws_client.cloudformation.describe_stacks(StackName="non-existing")
1078+
1079+
snapshot.add_transformer(snapshot.transform.regex("non-existing", "<stack-name>"))
1080+
snapshot.match("Error", ex.value.response)

tests/aws/services/cloudformation/api/test_stacks.snapshot.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2286,5 +2286,21 @@
22862286
}
22872287
}
22882288
}
2289+
},
2290+
"tests/aws/services/cloudformation/api/test_stacks.py::test_non_existing_stack_message": {
2291+
"recorded-date": "21-07-2025, 18:00:27",
2292+
"recorded-content": {
2293+
"Error": {
2294+
"Error": {
2295+
"Code": "ValidationError",
2296+
"Message": "Stack with id <stack-name> does not exist",
2297+
"Type": "Sender"
2298+
},
2299+
"ResponseMetadata": {
2300+
"HTTPHeaders": {},
2301+
"HTTPStatusCode": 400
2302+
}
2303+
}
2304+
}
22892305
}
22902306
}

tests/aws/services/cloudformation/api/test_stacks.validation.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@
6262
"tests/aws/services/cloudformation/api/test_stacks.py::test_no_echo_parameter": {
6363
"last_validated_date": "2024-12-19T11:35:15+00:00"
6464
},
65+
"tests/aws/services/cloudformation/api/test_stacks.py::test_non_existing_stack_message": {
66+
"last_validated_date": "2025-07-21T18:00:27+00:00",
67+
"durations_in_seconds": {
68+
"setup": 0.3,
69+
"call": 0.32,
70+
"teardown": 0.0,
71+
"total": 0.62
72+
}
73+
},
6574
"tests/aws/services/cloudformation/api/test_stacks.py::test_stack_deploy_order2": {
6675
"last_validated_date": "2024-05-21T09:48:14+00:00"
6776
},

0 commit comments

Comments
 (0)