Skip to content
This repository was archived by the owner on Mar 23, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions localstack-core/localstack/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1216,6 +1216,15 @@ def populate_edge_configuration(
# EXPERIMENTAL
CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES = is_env_not_false("CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES")

# Comma-separated list of resource type names that CloudFormation will ignore on stack creation
CFN_IGNORE_UNSUPPORTED_TYPE_CREATE = parse_comma_separated_list(
"CFN_IGNORE_UNSUPPORTED_TYPE_CREATE"
)
# Comma-separated list of resource type names that CloudFormation will ignore on stack update
CFN_IGNORE_UNSUPPORTED_TYPE_UPDATE = parse_comma_separated_list(
"CFN_IGNORE_UNSUPPORTED_TYPE_UPDATE"
)

# Decrease the waiting time for resource deployment
CFN_NO_WAIT_ITERATIONS: str | int | None = os.environ.get("CFN_NO_WAIT_ITERATIONS")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
PreprocProperties,
PreprocResource,
)
from localstack.services.cloudformation.engine.v2.unsupported_resource import (
should_ignore_unsupported_resource_type,
)
from localstack.services.cloudformation.resource_provider import (
Credentials,
OperationStatus,
Expand Down Expand Up @@ -512,7 +515,9 @@ def _execute_resource_action(
resource_model={},
message=f"Resource provider operation failed: {reason}",
)
elif config.CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES:
elif should_ignore_unsupported_resource_type(
resource_type=resource_type, change_set_type=self._change_set.change_set_type
):
log_not_available_message(
resource_type,
f'No resource provider found for "{resource_type}"',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@
extract_dynamic_reference,
perform_dynamic_reference_lookup,
)
from localstack.services.cloudformation.engine.v2.unsupported_resource import (
should_ignore_unsupported_resource_type,
)
from localstack.services.cloudformation.engine.validations import ValidationError
from localstack.services.cloudformation.stores import (
exports_map,
Expand Down Expand Up @@ -274,6 +277,7 @@ def _deployed_property_value_of(
f"No deployed instances of resource '{resource_logical_id}' were found"
)
properties = resolved_resource.get("Properties", {})
resource_type = resolved_resource.get("Type")
Comment thread
silv-io marked this conversation as resolved.
# TODO support structured properties, e.g. NestedStack.Outputs.OutputName
property_value: Any | None = get_value_from_path(properties, property_name)

Expand All @@ -286,7 +290,10 @@ def _deployed_property_value_of(
f"Accessing property '{property_name}' from '{resource_logical_id}' resulted in a non-string value nor list"
)
return property_value
elif config.CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES:
elif resource_type and should_ignore_unsupported_resource_type(
resource_type=resource_type,
change_set_type=self._change_set.change_set_type,
):
return MOCKED_REFERENCE

return property_value
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from localstack.services.cloudformation.engine.v2.change_set_model import (
NodeResource,
)
from localstack.aws.api.cloudformation import ChangeSetType
from localstack.services.cloudformation.engine.v2.change_set_model import NodeResource
from localstack.services.cloudformation.engine.v2.change_set_model_visitor import (
ChangeSetModelVisitor,
)
from localstack.services.cloudformation.engine.v2.unsupported_resource import (
should_ignore_unsupported_resource_type,
)
from localstack.services.cloudformation.resources import AWS_AVAILABLE_CFN_RESOURCES
from localstack.utils.catalog.catalog import (
AwsServicesSupportStatus,
Expand Down Expand Up @@ -81,17 +83,23 @@ def _build_resource_failure_message(


class ChangeSetResourceSupportChecker(ChangeSetModelVisitor):
change_set_type: ChangeSetType
catalog: CatalogPlugin

TITLE_MESSAGE = "Unsupported resources detected:"

def __init__(self):
def __init__(self, change_set_type: ChangeSetType):
self._resource_failure_messages: dict[str, str] = {}
self.change_set_type = change_set_type
self.catalog = get_aws_catalog()

def visit_node_resource(self, node_resource: NodeResource):
resource_type = node_resource.type_.value
if resource_type not in self._resource_failure_messages:
ignore_unsupported = should_ignore_unsupported_resource_type(
resource_type=resource_type, change_set_type=self.change_set_type
)

if resource_type not in self._resource_failure_messages and not ignore_unsupported:
if resource_type not in AWS_AVAILABLE_CFN_RESOURCES:
# Ignore non-AWS resources
pass
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from __future__ import annotations

from localstack import config
from localstack.aws.api.cloudformation import ChangeSetType


def should_ignore_unsupported_resource_type(
resource_type: str, change_set_type: ChangeSetType
) -> bool:
if config.CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES:
return True

match change_set_type:
case ChangeSetType.CREATE:
return resource_type in config.CFN_IGNORE_UNSUPPORTED_TYPE_CREATE
case ChangeSetType.UPDATE | ChangeSetType.IMPORT:
return resource_type in config.CFN_IGNORE_UNSUPPORTED_TYPE_UPDATE
case _:
return False
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,9 @@ def _setup_change_set_model(
change_set.processed_template = transformed_after_template

if not config.CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES:
support_visitor = ChangeSetResourceSupportChecker()
support_visitor = ChangeSetResourceSupportChecker(
change_set_type=change_set.change_set_type
)
support_visitor.visit(change_set.update_model.node_template)
failure_messages = support_visitor.failure_messages
if failure_messages:
Expand Down
116 changes: 88 additions & 28 deletions tests/integration/test_catalog.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import textwrap
import json

import pytest
from botocore.exceptions import WaiterError

from localstack import config
from localstack.services.cloudformation.engine.v2 import (
Expand All @@ -22,7 +23,6 @@
CloudFormationResourcesSupportInLatest,
)
from localstack.utils.strings import short_uid
from localstack.utils.sync import retry

UNSUPPORTED_RESOURCE_CASES = [
(
Expand Down Expand Up @@ -86,6 +86,76 @@ def testing_catalog(monkeypatch):
return plugin


@markers.aws.only_localstack
def test_ignore_unsupported_resources_toggle(testing_catalog, aws_client, monkeypatch, cleanups):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: in general I would prefer not defining inner functions (especially if they only have one use), and to use the waiters where possible. I don't find them easy to use, and the waiters code is more straightforward to read in the test itself. For the successful cases we swap a function definition and call with a single line, and for the failure cases using the WaiterError is clearer IMO.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks I didn't like it too much either but also didn't know about the waiters. Will switch it over :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've fixed it and also went ahead and fixed the old test to use the waiters. It's much cleaner now :)

unsupported_resource = "AWS::LatestService::NotSupported"

# template with one supported and one unsupported resource
bucket_name = f"cfn-toggle-{short_uid()}"
template_body = json.dumps(
{
"Resources": {
"SupportedBucket": {
"Type": "AWS::S3::Bucket",
"Properties": {"BucketName": bucket_name},
},
"Unsupported": {"Type": unsupported_resource},
},
}
)

# 1) ignore lists empty -> change set should fail
monkeypatch.setattr(config, "CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES", False)
monkeypatch.setattr(config, "CFN_IGNORE_UNSUPPORTED_TYPE_CREATE", [])
stack_name_fail = f"stack-fail-{short_uid()}"
change_set_name_fail = f"cs-{short_uid()}"
response = aws_client.cloudformation.create_change_set(
StackName=stack_name_fail,
ChangeSetName=change_set_name_fail,
TemplateBody=template_body,
ChangeSetType="CREATE",
)
cs_id_fail, stack_id_fail = response["Id"], response["StackId"]

waiter = aws_client.cloudformation.get_waiter("change_set_create_complete")
with pytest.raises(WaiterError) as exc_info:
waiter.wait(
ChangeSetName=cs_id_fail,
)

assert exc_info.value.last_response["Status"] == "FAILED"
status_reason = exc_info.value.last_response["StatusReason"]
assert ChangeSetResourceSupportChecker.TITLE_MESSAGE in status_reason
assert unsupported_resource in status_reason
cleanups.append(lambda: aws_client.cloudformation.delete_change_set(ChangeSetName=cs_id_fail))
cleanups.append(lambda: aws_client.cloudformation.delete_stack(StackName=stack_id_fail))

# 2) add unsupported resource to create ignore list -> deployment succeeds and bucket is present
monkeypatch.setattr(config, "CFN_IGNORE_UNSUPPORTED_TYPE_CREATE", [unsupported_resource])
stack_name_ok = f"stack-ok-{short_uid()}"
change_set_name_ok = f"cs-{short_uid()}"
response = aws_client.cloudformation.create_change_set(
StackName=stack_name_ok,
ChangeSetName=change_set_name_ok,
TemplateBody=template_body,
ChangeSetType="CREATE",
)
cs_id_ok, stack_id_ok = response["Id"], response["StackId"]

waiter.wait(
ChangeSetName=cs_id_ok,
)
aws_client.cloudformation.execute_change_set(ChangeSetName=cs_id_ok)
aws_client.cloudformation.get_waiter("stack_create_complete").wait(
StackName=stack_name_ok,
)

buckets = aws_client.s3.list_buckets()["Buckets"]
assert any(b["Name"] == bucket_name for b in buckets)

cleanups.append(lambda: aws_client.cloudformation.delete_stack(StackName=stack_id_ok))


@markers.aws.only_localstack
@pytest.mark.parametrize(
"unsupported_resource, expected_service",
Expand All @@ -95,13 +165,10 @@ def test_catalog_reports_unsupported_resources_in_stack_status(
testing_catalog, aws_client, unsupported_resource, expected_service, monkeypatch, cleanups
):
monkeypatch.setattr(config, "CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES", False)
template_body = textwrap.dedent(
f"""
AWSTemplateFormatVersion: '2010-09-09'
Resources:
Unsupported:
Type: {unsupported_resource}
"""
template_body = json.dumps(
{
"Resources": {"Unsupported": {"Type": unsupported_resource}},
}
)

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

def _describe_failed_change_set():
result = aws_client.cloudformation.describe_change_set(ChangeSetName=change_set_id)
status = result["Status"]
if status == "FAILED":
return result
if status == "CREATE_COMPLETE":
pytest.fail("expected change set creation to fail for unsupported resource")
raise Exception("gave up on waiting for change set creation to fail")

change_set = retry(_describe_failed_change_set, retries=20, sleep=2)

status_reason = change_set.get("StatusReason", "")
waiter = aws_client.cloudformation.get_waiter("change_set_create_complete")
with pytest.raises(WaiterError) as exc_info:
waiter.wait(
ChangeSetName=change_set_id,
)
assert exc_info.value.last_response["Status"] == "FAILED"
status_reason = exc_info.value.last_response["StatusReason"]
assert ChangeSetResourceSupportChecker.TITLE_MESSAGE in status_reason
assert unsupported_resource in status_reason

def _describe_failed_stack():
stack = aws_client.cloudformation.describe_stacks(StackName=stack_id)["Stacks"][0]
stack_status = stack["StackStatus"]
if stack_status in {"CREATE_FAILED", "ROLLBACK_COMPLETE"}:
return stack
raise Exception("gave on waiting for stack creation to fail for unsupported resource")
with pytest.raises(WaiterError) as exc_info:
aws_client.cloudformation.get_waiter("stack_create_complete").wait(
StackName=stack_id,
)

stack_description = retry(_describe_failed_stack, retries=30, sleep=2)
stack_description = exc_info.value.last_response["Stacks"][0]
stack_status_reason = stack_description.get("StackStatusReason", "")
assert ChangeSetResourceSupportChecker.TITLE_MESSAGE in stack_status_reason
assert unsupported_resource in stack_status_reason
Expand Down
Loading