Skip to content
This repository was archived by the owner on Mar 23, 2026. It is now read-only.

Commit 237a9e1

Browse files
authored
Add possibility to ignore specific unsupported resource types in CloudFormation (#13496)
1 parent 7f1e6b5 commit 237a9e1

7 files changed

Lines changed: 146 additions & 36 deletions

File tree

localstack-core/localstack/config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,6 +1216,15 @@ def populate_edge_configuration(
12161216
# EXPERIMENTAL
12171217
CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES = is_env_not_false("CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES")
12181218

1219+
# Comma-separated list of resource type names that CloudFormation will ignore on stack creation
1220+
CFN_IGNORE_UNSUPPORTED_TYPE_CREATE = parse_comma_separated_list(
1221+
"CFN_IGNORE_UNSUPPORTED_TYPE_CREATE"
1222+
)
1223+
# Comma-separated list of resource type names that CloudFormation will ignore on stack update
1224+
CFN_IGNORE_UNSUPPORTED_TYPE_UPDATE = parse_comma_separated_list(
1225+
"CFN_IGNORE_UNSUPPORTED_TYPE_UPDATE"
1226+
)
1227+
12191228
# Decrease the waiting time for resource deployment
12201229
CFN_NO_WAIT_ITERATIONS: str | int | None = os.environ.get("CFN_NO_WAIT_ITERATIONS")
12211230

localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
PreprocProperties,
3434
PreprocResource,
3535
)
36+
from localstack.services.cloudformation.engine.v2.unsupported_resource import (
37+
should_ignore_unsupported_resource_type,
38+
)
3639
from localstack.services.cloudformation.resource_provider import (
3740
Credentials,
3841
OperationStatus,
@@ -512,7 +515,9 @@ def _execute_resource_action(
512515
resource_model={},
513516
message=f"Resource provider operation failed: {reason}",
514517
)
515-
elif config.CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES:
518+
elif should_ignore_unsupported_resource_type(
519+
resource_type=resource_type, change_set_type=self._change_set.change_set_type
520+
):
516521
log_not_available_message(
517522
resource_type,
518523
f'No resource provider found for "{resource_type}"',

localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@
5050
extract_dynamic_reference,
5151
perform_dynamic_reference_lookup,
5252
)
53+
from localstack.services.cloudformation.engine.v2.unsupported_resource import (
54+
should_ignore_unsupported_resource_type,
55+
)
5356
from localstack.services.cloudformation.engine.validations import ValidationError
5457
from localstack.services.cloudformation.stores import (
5558
exports_map,
@@ -274,6 +277,7 @@ def _deployed_property_value_of(
274277
f"No deployed instances of resource '{resource_logical_id}' were found"
275278
)
276279
properties = resolved_resource.get("Properties", {})
280+
resource_type = resolved_resource.get("Type")
277281
# TODO support structured properties, e.g. NestedStack.Outputs.OutputName
278282
property_value: Any | None = get_value_from_path(properties, property_name)
279283

@@ -286,7 +290,10 @@ def _deployed_property_value_of(
286290
f"Accessing property '{property_name}' from '{resource_logical_id}' resulted in a non-string value nor list"
287291
)
288292
return property_value
289-
elif config.CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES:
293+
elif resource_type and should_ignore_unsupported_resource_type(
294+
resource_type=resource_type,
295+
change_set_type=self._change_set.change_set_type,
296+
):
290297
return MOCKED_REFERENCE
291298

292299
return property_value

localstack-core/localstack/services/cloudformation/engine/v2/change_set_resource_support_checker.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
from localstack.services.cloudformation.engine.v2.change_set_model import (
2-
NodeResource,
3-
)
1+
from localstack.aws.api.cloudformation import ChangeSetType
2+
from localstack.services.cloudformation.engine.v2.change_set_model import NodeResource
43
from localstack.services.cloudformation.engine.v2.change_set_model_visitor import (
54
ChangeSetModelVisitor,
65
)
6+
from localstack.services.cloudformation.engine.v2.unsupported_resource import (
7+
should_ignore_unsupported_resource_type,
8+
)
79
from localstack.services.cloudformation.resources import AWS_AVAILABLE_CFN_RESOURCES
810
from localstack.utils.catalog.catalog import (
911
AwsServicesSupportStatus,
@@ -81,17 +83,23 @@ def _build_resource_failure_message(
8183

8284

8385
class ChangeSetResourceSupportChecker(ChangeSetModelVisitor):
86+
change_set_type: ChangeSetType
8487
catalog: CatalogPlugin
8588

8689
TITLE_MESSAGE = "Unsupported resources detected:"
8790

88-
def __init__(self):
91+
def __init__(self, change_set_type: ChangeSetType):
8992
self._resource_failure_messages: dict[str, str] = {}
93+
self.change_set_type = change_set_type
9094
self.catalog = get_aws_catalog()
9195

9296
def visit_node_resource(self, node_resource: NodeResource):
9397
resource_type = node_resource.type_.value
94-
if resource_type not in self._resource_failure_messages:
98+
ignore_unsupported = should_ignore_unsupported_resource_type(
99+
resource_type=resource_type, change_set_type=self.change_set_type
100+
)
101+
102+
if resource_type not in self._resource_failure_messages and not ignore_unsupported:
95103
if resource_type not in AWS_AVAILABLE_CFN_RESOURCES:
96104
# Ignore non-AWS resources
97105
pass
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from __future__ import annotations
2+
3+
from localstack import config
4+
from localstack.aws.api.cloudformation import ChangeSetType
5+
6+
7+
def should_ignore_unsupported_resource_type(
8+
resource_type: str, change_set_type: ChangeSetType
9+
) -> bool:
10+
if config.CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES:
11+
return True
12+
13+
match change_set_type:
14+
case ChangeSetType.CREATE:
15+
return resource_type in config.CFN_IGNORE_UNSUPPORTED_TYPE_CREATE
16+
case ChangeSetType.UPDATE | ChangeSetType.IMPORT:
17+
return resource_type in config.CFN_IGNORE_UNSUPPORTED_TYPE_UPDATE
18+
case _:
19+
return False

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,9 @@ def _setup_change_set_model(
427427
change_set.processed_template = transformed_after_template
428428

429429
if not config.CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES:
430-
support_visitor = ChangeSetResourceSupportChecker()
430+
support_visitor = ChangeSetResourceSupportChecker(
431+
change_set_type=change_set.change_set_type
432+
)
431433
support_visitor.visit(change_set.update_model.node_template)
432434
failure_messages = support_visitor.failure_messages
433435
if failure_messages:

tests/integration/test_catalog.py

Lines changed: 88 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import textwrap
1+
import json
22

33
import pytest
4+
from botocore.exceptions import WaiterError
45

56
from localstack import config
67
from localstack.services.cloudformation.engine.v2 import (
@@ -22,7 +23,6 @@
2223
CloudFormationResourcesSupportInLatest,
2324
)
2425
from localstack.utils.strings import short_uid
25-
from localstack.utils.sync import retry
2626

2727
UNSUPPORTED_RESOURCE_CASES = [
2828
(
@@ -86,6 +86,76 @@ def testing_catalog(monkeypatch):
8686
return plugin
8787

8888

89+
@markers.aws.only_localstack
90+
def test_ignore_unsupported_resources_toggle(testing_catalog, aws_client, monkeypatch, cleanups):
91+
unsupported_resource = "AWS::LatestService::NotSupported"
92+
93+
# template with one supported and one unsupported resource
94+
bucket_name = f"cfn-toggle-{short_uid()}"
95+
template_body = json.dumps(
96+
{
97+
"Resources": {
98+
"SupportedBucket": {
99+
"Type": "AWS::S3::Bucket",
100+
"Properties": {"BucketName": bucket_name},
101+
},
102+
"Unsupported": {"Type": unsupported_resource},
103+
},
104+
}
105+
)
106+
107+
# 1) ignore lists empty -> change set should fail
108+
monkeypatch.setattr(config, "CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES", False)
109+
monkeypatch.setattr(config, "CFN_IGNORE_UNSUPPORTED_TYPE_CREATE", [])
110+
stack_name_fail = f"stack-fail-{short_uid()}"
111+
change_set_name_fail = f"cs-{short_uid()}"
112+
response = aws_client.cloudformation.create_change_set(
113+
StackName=stack_name_fail,
114+
ChangeSetName=change_set_name_fail,
115+
TemplateBody=template_body,
116+
ChangeSetType="CREATE",
117+
)
118+
cs_id_fail, stack_id_fail = response["Id"], response["StackId"]
119+
120+
waiter = aws_client.cloudformation.get_waiter("change_set_create_complete")
121+
with pytest.raises(WaiterError) as exc_info:
122+
waiter.wait(
123+
ChangeSetName=cs_id_fail,
124+
)
125+
126+
assert exc_info.value.last_response["Status"] == "FAILED"
127+
status_reason = exc_info.value.last_response["StatusReason"]
128+
assert ChangeSetResourceSupportChecker.TITLE_MESSAGE in status_reason
129+
assert unsupported_resource in status_reason
130+
cleanups.append(lambda: aws_client.cloudformation.delete_change_set(ChangeSetName=cs_id_fail))
131+
cleanups.append(lambda: aws_client.cloudformation.delete_stack(StackName=stack_id_fail))
132+
133+
# 2) add unsupported resource to create ignore list -> deployment succeeds and bucket is present
134+
monkeypatch.setattr(config, "CFN_IGNORE_UNSUPPORTED_TYPE_CREATE", [unsupported_resource])
135+
stack_name_ok = f"stack-ok-{short_uid()}"
136+
change_set_name_ok = f"cs-{short_uid()}"
137+
response = aws_client.cloudformation.create_change_set(
138+
StackName=stack_name_ok,
139+
ChangeSetName=change_set_name_ok,
140+
TemplateBody=template_body,
141+
ChangeSetType="CREATE",
142+
)
143+
cs_id_ok, stack_id_ok = response["Id"], response["StackId"]
144+
145+
waiter.wait(
146+
ChangeSetName=cs_id_ok,
147+
)
148+
aws_client.cloudformation.execute_change_set(ChangeSetName=cs_id_ok)
149+
aws_client.cloudformation.get_waiter("stack_create_complete").wait(
150+
StackName=stack_name_ok,
151+
)
152+
153+
buckets = aws_client.s3.list_buckets()["Buckets"]
154+
assert any(b["Name"] == bucket_name for b in buckets)
155+
156+
cleanups.append(lambda: aws_client.cloudformation.delete_stack(StackName=stack_id_ok))
157+
158+
89159
@markers.aws.only_localstack
90160
@pytest.mark.parametrize(
91161
"unsupported_resource, expected_service",
@@ -95,13 +165,10 @@ def test_catalog_reports_unsupported_resources_in_stack_status(
95165
testing_catalog, aws_client, unsupported_resource, expected_service, monkeypatch, cleanups
96166
):
97167
monkeypatch.setattr(config, "CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES", False)
98-
template_body = textwrap.dedent(
99-
f"""
100-
AWSTemplateFormatVersion: '2010-09-09'
101-
Resources:
102-
Unsupported:
103-
Type: {unsupported_resource}
104-
"""
168+
template_body = json.dumps(
169+
{
170+
"Resources": {"Unsupported": {"Type": unsupported_resource}},
171+
}
105172
)
106173

107174
stack_name = f"stack-{short_uid()}"
@@ -118,29 +185,22 @@ def test_catalog_reports_unsupported_resources_in_stack_status(
118185
change_set_id = response["Id"]
119186
stack_id = response["StackId"]
120187

121-
def _describe_failed_change_set():
122-
result = aws_client.cloudformation.describe_change_set(ChangeSetName=change_set_id)
123-
status = result["Status"]
124-
if status == "FAILED":
125-
return result
126-
if status == "CREATE_COMPLETE":
127-
pytest.fail("expected change set creation to fail for unsupported resource")
128-
raise Exception("gave up on waiting for change set creation to fail")
129-
130-
change_set = retry(_describe_failed_change_set, retries=20, sleep=2)
131-
132-
status_reason = change_set.get("StatusReason", "")
188+
waiter = aws_client.cloudformation.get_waiter("change_set_create_complete")
189+
with pytest.raises(WaiterError) as exc_info:
190+
waiter.wait(
191+
ChangeSetName=change_set_id,
192+
)
193+
assert exc_info.value.last_response["Status"] == "FAILED"
194+
status_reason = exc_info.value.last_response["StatusReason"]
133195
assert ChangeSetResourceSupportChecker.TITLE_MESSAGE in status_reason
134196
assert unsupported_resource in status_reason
135197

136-
def _describe_failed_stack():
137-
stack = aws_client.cloudformation.describe_stacks(StackName=stack_id)["Stacks"][0]
138-
stack_status = stack["StackStatus"]
139-
if stack_status in {"CREATE_FAILED", "ROLLBACK_COMPLETE"}:
140-
return stack
141-
raise Exception("gave on waiting for stack creation to fail for unsupported resource")
198+
with pytest.raises(WaiterError) as exc_info:
199+
aws_client.cloudformation.get_waiter("stack_create_complete").wait(
200+
StackName=stack_id,
201+
)
142202

143-
stack_description = retry(_describe_failed_stack, retries=30, sleep=2)
203+
stack_description = exc_info.value.last_response["Stacks"][0]
144204
stack_status_reason = stack_description.get("StackStatusReason", "")
145205
assert ChangeSetResourceSupportChecker.TITLE_MESSAGE in stack_status_reason
146206
assert unsupported_resource in stack_status_reason

0 commit comments

Comments
 (0)