Thanks to visit codestin.com
Credit goes to github.com

Skip to content

CloudFormation v2 Engine: Base Support for Fn::Split #12698

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jun 3, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ def __init__(self, scope: Scope, value: Any):
FnSubKey: Final[str] = "Fn::Sub"
FnTransform: Final[str] = "Fn::Transform"
FnSelect: Final[str] = "Fn::Select"
FnSplit: Final[str] = "Fn::Split"
INTRINSIC_FUNCTIONS: Final[set[str]] = {
RefKey,
FnIfKey,
Expand All @@ -435,6 +436,7 @@ def __init__(self, scope: Scope, value: Any):
FnSubKey,
FnTransform,
FnSelect,
FnSplit,
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,34 @@ def _compute_fn_select(args: list[Any]) -> Any:

return PreprocEntityDelta(before=before, after=after)

def visit_node_intrinsic_function_fn_split(
self, node_intrinsic_function: NodeIntrinsicFunction
) -> PreprocEntityDelta:
# TODO: add further support for schema validation
arguments_delta = self.visit(node_intrinsic_function.arguments)
arguments_before = arguments_delta.before
arguments_after = arguments_delta.after

def _compute_fn_split(args: list[Any]) -> Any:
delimiter = args[0]
if not isinstance(delimiter, str) or not delimiter:
raise RuntimeError(f"Invalid delimiter value for Fn::Split: '{delimiter}'")
source_string = args[1]
if not isinstance(source_string, str):
raise RuntimeError(f"Invalid source string value for Fn::Split: '{source_string}'")
split_string = source_string.split(delimiter)
return split_string

before = Nothing
if not is_nothing(arguments_before):
before = _compute_fn_split(arguments_before)

after = Nothing
if not is_nothing(arguments_after):
after = _compute_fn_split(arguments_after)

return PreprocEntityDelta(before=before, after=after)

def visit_node_intrinsic_function_fn_find_in_map(
self, node_intrinsic_function: NodeIntrinsicFunction
) -> PreprocEntityDelta:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ def visit_node_intrinsic_function_fn_select(
):
self.visit_children(node_intrinsic_function)

def visit_node_intrinsic_function_fn_split(
self, node_intrinsic_function: NodeIntrinsicFunction
):
self.visit_children(node_intrinsic_function)

def visit_node_intrinsic_function_fn_sub(self, node_intrinsic_function: NodeIntrinsicFunction):
self.visit_children(node_intrinsic_function)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,13 @@ def _run(*args):
def _describe_change_set(
self, change_set: ChangeSet, include_property_values: bool
) -> DescribeChangeSetOutput:
# TODO: The ChangeSetModelDescriber currently matches AWS behavior by listing
# resource changes in the order they appear in the template. However, when
# a resource change is triggered indirectly (e.g., via Ref or GetAtt), the
# dependency's change appears first in the list.
# Snapshot tests using the `capture_update_process` fixture rely on a
# normalizer to account for this ordering. This should be removed in the
# future by enforcing a consistently correct change ordering at the source.
change_set_describer = ChangeSetModelDescriber(
change_set=change_set, include_property_values=include_property_values
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ def _is_executed():
assert "hello from statemachine" in execution_desc["output"]


@pytest.mark.skip(reason="CFNV2:Fn::Split")
@pytest.mark.skip(
reason="CFNV2:Other During change set describe the a Ref to a not yet deployed resource returns null which is an invalid input for Fn::Split"
)
@markers.aws.validated
def test_nested_statemachine_with_sync2(deploy_cfn_template, aws_client):
stack = deploy_cfn_template(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@ def test_base64_sub_and_getatt_functions(self, deploy_cfn_template):
converted_string = base64.b64encode(bytes(original_string, "utf-8")).decode("utf-8")
assert converted_string == deployed.outputs["Encoded"]

@pytest.mark.skip(reason="CFNV2:Fn::Split")
Copy link
Contributor

Choose a reason for hiding this comment

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

🎉

@markers.aws.validated
def test_split_length_and_join_functions(self, deploy_cfn_template):
template_path = os.path.join(
Expand Down
243 changes: 243 additions & 0 deletions tests/aws/services/cloudformation/v2/test_change_set_fn_split.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import pytest
from localstack_snapshot.snapshots.transformer import RegexTransformer

from localstack.services.cloudformation.v2.utils import is_v2_engine
from localstack.testing.aws.util import is_aws_cloud
from localstack.testing.pytest import markers
from localstack.utils.strings import long_uid


@pytest.mark.skipif(
condition=not is_v2_engine() and not is_aws_cloud(), reason="Requires the V2 engine"
)
@markers.snapshot.skip_snapshot_verify(
paths=[
"per-resource-events..*",
"delete-describe..*",
#
# Before/After Context
"$..Capabilities",
"$..NotificationARNs",
"$..IncludeNestedStacks",
"$..Scope",
"$..Details",
"$..Parameters",
"$..Replacement",
"$..PolicyAction",
"$..StatusReason",
]
)
class TestChangeSetFnSplit:
@markers.aws.validated
def test_fn_split_add_to_static_property(
self,
snapshot,
capture_update_process,
):
name1 = f"topic-name-1-{long_uid()}"
snapshot.add_transformer(RegexTransformer(name1, "topic-name-1"))
snapshot.add_transformer(RegexTransformer(name1.replace("-", "_"), "topic_name_1"))
template_1 = {
"Resources": {
"Topic1": {"Type": "AWS::SNS::Topic", "Properties": {"DisplayName": name1}}
}
}
template_2 = {
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {
"DisplayName": {
"Fn::Join": [
"_",
{"Fn::Split": ["-", "part1-part2-part3"]},
]
}
},
}
}
}
capture_update_process(snapshot, template_1, template_2)

@markers.aws.validated
def test_fn_split_remove_from_static_property(
self,
snapshot,
capture_update_process,
):
name1 = f"topic-name-1-{long_uid()}"
snapshot.add_transformer(RegexTransformer(name1, "topic-name-1"))
snapshot.add_transformer(RegexTransformer(name1, "topic-name-1"))
snapshot.add_transformer(RegexTransformer(name1.replace("-", "_"), "topic_name_1"))
template_1 = {
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {
"DisplayName": {
"Fn::Join": [
"_",
{"Fn::Split": ["-", "part1-part2-part3"]},
]
}
},
}
}
}
template_2 = {
"Resources": {
"Topic1": {"Type": "AWS::SNS::Topic", "Properties": {"DisplayName": name1}}
}
}
capture_update_process(snapshot, template_1, template_2)

@markers.aws.validated
def test_fn_split_change_delimiter(
self,
snapshot,
capture_update_process,
):
template_1 = {
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {
"DisplayName": {"Fn::Join": ["_", {"Fn::Split": ["-", "a-b--c::d"]}]}
},
}
}
}
template_2 = {
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {
"DisplayName": {"Fn::Join": ["_", {"Fn::Split": [":", "a-b--c::d"]}]}
},
}
}
}
capture_update_process(snapshot, template_1, template_2)

@markers.aws.validated
def test_fn_split_change_source_string_only(
self,
snapshot,
capture_update_process,
):
template_1 = {
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {"DisplayName": {"Fn::Join": ["_", {"Fn::Split": ["-", "a-b"]}]}},
}
}
}
template_2 = {
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {
"DisplayName": {"Fn::Join": ["_", {"Fn::Split": ["-", "x-y-z"]}]}
},
}
}
}
capture_update_process(snapshot, template_1, template_2)

@markers.aws.validated
def test_fn_split_with_ref_as_string_source(
self,
snapshot,
capture_update_process,
):
param_name = "DelimiterParam"
template_1 = {
"Parameters": {param_name: {"Type": "String", "Default": "hello-world"}},
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {
"DisplayName": {
"Fn::Join": ["_", {"Fn::Split": ["-", {"Ref": param_name}]}]
}
},
}
},
}
template_2 = {
"Parameters": {param_name: {"Type": "String", "Default": "foo-bar-baz"}},
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {
"DisplayName": {
"Fn::Join": ["_", {"Fn::Split": ["-", {"Ref": param_name}]}]
}
},
}
},
}
capture_update_process(snapshot, template_1, template_2)

@markers.snapshot.skip_snapshot_verify(
paths=[
# Reason: AWS incorrectly does not list the second and third topic as
# needing modifying, however it needs to
Copy link
Contributor

Choose a reason for hiding this comment

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

😂

"describe-change-set-2-prop-values..Changes",
]
)
@markers.aws.validated
def test_fn_split_with_get_att(
self,
snapshot,
capture_update_process,
):
name1 = f"topic-name-1-{long_uid()}"
name2 = f"topic-name-2-{long_uid()}"
snapshot.add_transformer(RegexTransformer(name1, "topic-name-1"))
snapshot.add_transformer(RegexTransformer(name1.replace("-", "_"), "topic_name_1"))
snapshot.add_transformer(RegexTransformer(name2, "topic-name-2"))
snapshot.add_transformer(RegexTransformer(name2.replace("-", "_"), "topic_name_2"))

template_1 = {
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {"DisplayName": name1},
},
"Topic2": {
"Type": "AWS::SNS::Topic",
"Properties": {
"DisplayName": {
"Fn::Join": [
"_",
{"Fn::Split": ["-", {"Fn::GetAtt": ["Topic1", "DisplayName"]}]},
]
}
},
},
}
}

template_2 = {
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {"DisplayName": name2},
},
"Topic2": {
"Type": "AWS::SNS::Topic",
"Properties": {
"DisplayName": {
"Fn::Join": [
"_",
{"Fn::Split": ["-", {"Fn::GetAtt": ["Topic1", "DisplayName"]}]},
]
}
},
},
}
}

capture_update_process(snapshot, template_1, template_2)
Loading
Loading