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

Skip to content

CloudFormation v2 Engine: Support for Fn::Select #12679

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 12 commits into from
Jun 2, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ def __init__(self, scope: Scope, value: Any):
FnFindInMapKey: Final[str] = "Fn::FindInMap"
FnSubKey: Final[str] = "Fn::Sub"
FnTransform: Final[str] = "Fn::Transform"
FnSelect: Final[str] = "Fn::Select"
INTRINSIC_FUNCTIONS: Final[set[str]] = {
RefKey,
FnIfKey,
Expand All @@ -433,6 +434,7 @@ def __init__(self, scope: Scope, value: Any):
FnFindInMapKey,
FnSubKey,
FnTransform,
FnSelect,
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,35 @@ def _compute_join(args: list[Any]) -> str:
after = _compute_join(arguments_after)
return PreprocEntityDelta(before=before, after=after)

def visit_node_intrinsic_function_fn_select(
self, node_intrinsic_function: NodeIntrinsicFunction
):
# 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_select(args: list[Any]) -> Any:
values: list[Any] = args[1]
if not isinstance(values, list) or not values:
raise RuntimeError(f"Invalid arguments list value for Fn::Select: '{values}'")
values_len = len(values)
index: int = int(args[0])
if not isinstance(index, int) or index < 0 or index > values_len:
raise RuntimeError(f"Invalid or out of range index value for Fn::Select: '{index}'")
selection = values[index]
return selection

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

after = Nothing
if not is_nothing(arguments_after):
after = _compute_fn_select(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 @@ -118,6 +118,11 @@ def visit_node_intrinsic_function_fn_transform(
):
self.visit_children(node_intrinsic_function)

def visit_node_intrinsic_function_fn_select(
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 @@ -427,7 +427,6 @@ def test_conditional_in_conditional(self, env, region, deploy_cfn_template, aws_
else:
assert stack.outputs["Result"] == "false"

@pytest.mark.skip(reason="CFNV2:Fn::Select")
@markers.aws.validated
def test_conditional_with_select(self, deploy_cfn_template, aws_client):
stack = deploy_cfn_template(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1202,7 +1202,6 @@ class TestCfnLambdaDestinations:

"""

@pytest.mark.skip(reason="CFNV2:Fn::Select")
@pytest.mark.parametrize(
["on_success", "on_failure"],
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def _is_executed():
assert "hello from statemachine" in execution_desc["output"]


@pytest.mark.skip(reason="CFNV2:Fn::Split")
@markers.aws.validated
def test_nested_statemachine_with_sync2(deploy_cfn_template, aws_client):
stack = deploy_cfn_template(
Expand Down
203 changes: 203 additions & 0 deletions tests/aws/services/cloudformation/v2/test_change_set_fn_select.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
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",
]
)
class TestChangeSetFnSelect:
@markers.aws.validated
def test_fn_select_add_to_static_property(
self,
snapshot,
capture_update_process,
):
name1 = f"topic-name-1-{long_uid()}"
snapshot.add_transformer(RegexTransformer(name1, "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::Select": [1, ["1st", "2nd", "3rd"]]}},
}
}
}
capture_update_process(snapshot, template_1, template_2)

@markers.aws.validated
def test_fn_select_remove_from_static_property(
self,
snapshot,
capture_update_process,
):
name1 = f"topic-name-1-{long_uid()}"
snapshot.add_transformer(RegexTransformer(name1, "topic-name-1"))
template_1 = {
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {"DisplayName": {"Fn::Select": [1, ["1st", "2nd", "3rd"]]}},
}
}
}
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_select_change_in_selection_list(
self,
snapshot,
capture_update_process,
):
name1 = f"topic-name-1-{long_uid()}"
snapshot.add_transformer(RegexTransformer(name1, "topic-name-1"))
template_1 = {
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {"DisplayName": {"Fn::Select": [1, ["1st", "2nd"]]}},
}
}
}
template_2 = {
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {"DisplayName": {"Fn::Select": [1, ["1st", "new-2nd", "3rd"]]}},
}
}
}
capture_update_process(snapshot, template_1, template_2)

@markers.aws.validated
def test_fn_select_change_in_selection_index_only(
self,
snapshot,
capture_update_process,
):
name1 = f"topic-name-1-{long_uid()}"
snapshot.add_transformer(RegexTransformer(name1, "topic-name-1"))
template_1 = {
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {"DisplayName": {"Fn::Select": [1, ["1st", "2nd"]]}},
}
}
}
template_2 = {
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {"DisplayName": {"Fn::Select": [0, ["1st", "2nd"]]}},
}
}
}
capture_update_process(snapshot, template_1, template_2)

@markers.aws.validated
def test_fn_select_change_in_selected_element_type_ref(
self,
snapshot,
capture_update_process,
):
name1 = f"topic-name-1-{long_uid()}"
snapshot.add_transformer(RegexTransformer(name1, "topic-name-1"))
template_1 = {
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {"DisplayName": {"Fn::Select": [0, ["1st"]]}},
}
}
}
template_2 = {
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {"DisplayName": {"Fn::Select": [0, [{"Ref": "AWS::StackName"}]]}},
}
}
}
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
"describe-change-set-2-prop-values..Changes",
]
)
@markers.aws.validated
def test_fn_select_change_get_att_reference(
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(name2, "topic-name-2"))
template_1 = {
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {"DisplayName": name1},
},
"Topic2": {
"Type": "AWS::SNS::Topic",
"Properties": {
"DisplayName": {
"Fn::Select": [0, [{"Fn::GetAtt": ["Topic1", "DisplayName"]}]]
}
},
},
}
}
template_2 = {
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {"DisplayName": name2},
},
"Topic2": {
"Type": "AWS::SNS::Topic",
"Properties": {
"DisplayName": {
"Fn::Select": [0, [{"Fn::GetAtt": ["Topic1", "DisplayName"]}]]
}
},
},
}
}
capture_update_process(snapshot, template_1, template_2)
Loading
Loading