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

Skip to content

CloudFormation v2 Engine: Base Support for Fn::Transform #12662

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 9 commits into from
Jun 2, 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 @@ -386,6 +386,7 @@ def __init__(self, scope: Scope, value: Any):
FnEqualsKey: Final[str] = "Fn::Equals"
FnFindInMapKey: Final[str] = "Fn::FindInMap"
FnSubKey: Final[str] = "Fn::Sub"
FnTransform: Final[str] = "Fn::Transform"
INTRINSIC_FUNCTIONS: Final[set[str]] = {
RefKey,
FnIfKey,
Expand All @@ -395,6 +396,7 @@ def __init__(self, scope: Scope, value: Any):
FnGetAttKey,
FnFindInMapKey,
FnSubKey,
FnTransform,
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
import re
from typing import Any, Final, Generic, Optional, TypeVar

from localstack.services.cloudformation.engine.transformers import (
Transformer,
execute_macro,
transformers,
)
from localstack.services.cloudformation.engine.v2.change_set_model import (
ChangeSetEntity,
ChangeType,
Expand Down Expand Up @@ -30,6 +35,7 @@
from localstack.services.cloudformation.engine.v2.change_set_model_visitor import (
ChangeSetModelVisitor,
)
from localstack.services.cloudformation.stores import get_cloudformation_store
from localstack.services.cloudformation.v2.entities import ChangeSet
from localstack.utils.aws.arns import get_partition
from localstack.utils.urls import localstack_host
Expand Down Expand Up @@ -490,6 +496,78 @@ def visit_node_intrinsic_function_fn_not(
# Implicit change type computation.
return PreprocEntityDelta(before=before, after=after)

def _compute_fn_transform(self, args: dict[str, Any]) -> Any:
# TODO: add typing to arguments before this level.
# TODO: add schema validation
# TODO: add support for other transform types

account_id = self._change_set.account_id
region_name = self._change_set.region_name
transform_name: str = args.get("Name")
if not isinstance(transform_name, str):
raise RuntimeError("Invalid or missing Fn::Transform 'Name' argument")
transform_parameters: dict = args.get("Parameters")
if not isinstance(transform_parameters, dict):
raise RuntimeError("Invalid or missing Fn::Transform 'Parameters' argument")

if transform_name in transformers:
# TODO: port and refactor this 'transformers' logic to this package.
builtin_transformer_class = transformers[transform_name]
builtin_transformer: Transformer = builtin_transformer_class()
transform_output: Any = builtin_transformer.transform(
account_id=account_id, region_name=region_name, parameters=transform_parameters
)
return transform_output

macros_store = get_cloudformation_store(
account_id=account_id, region_name=region_name
).macros
if transform_name in macros_store:
# TODO: this formatting of stack parameters is odd but required to integrate with v1 execute_macro util.
# consider porting this utils and passing the plain list of parameters instead.
stack_parameters = {
parameter["ParameterKey"]: parameter
for parameter in self._change_set.stack.parameters
}
transform_output: Any = execute_macro(
account_id=account_id,
region_name=region_name,
parsed_template=dict(), # TODO: review the requirements for this argument.
macro=args, # TODO: review support for non dict bindings (v1).
stack_parameters=stack_parameters,
transformation_parameters=transform_parameters,
is_intrinsic=True,
)
return transform_output

raise RuntimeError(
f"Unsupported transform function '{transform_name}' in '{self._change_set.stack.stack_name}'"
)

def visit_node_intrinsic_function_fn_transform(
self, node_intrinsic_function: NodeIntrinsicFunction
) -> PreprocEntityDelta:
arguments_delta = self.visit(node_intrinsic_function.arguments)
arguments_before = arguments_delta.before
arguments_after = arguments_delta.after

# TODO: review the use of cache in self.precessed from the 'before' run to
# ensure changes to the lambda (such as after UpdateFunctionCode) do not
# generalise tot he before value at this depth (thus making it seems as
# though for this transformation before==after). Another options may be to
# have specialised caching for transformations.

# TODO: add tests to review the behaviour of CFN with changes to transformation
# function code and no changes to the template.

before = None
if arguments_before:
before = self._compute_fn_transform(args=arguments_before)
after = None
if arguments_after:
after = self._compute_fn_transform(args=arguments_after)
return PreprocEntityDelta(before=before, after=after)

def visit_node_intrinsic_function_fn_sub(
self, node_intrinsic_function: NodeIntrinsicFunction
) -> PreprocEntityDelta:
Expand Down Expand Up @@ -532,6 +610,8 @@ def _compute_sub(args: str | list[Any], select_before: bool = False) -> str:
template_variable_value = (
reference_delta.before if select_before else reference_delta.after
)
if isinstance(template_variable_value, PreprocResource):
template_variable_value = template_variable_value.logical_id
except RuntimeError:
raise RuntimeError(
f"Undefined variable name in Fn::Sub string template '{template_variable_name}'"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ def visit_node_intrinsic_function_fn_equals(
):
self.visit_children(node_intrinsic_function)

def visit_node_intrinsic_function_fn_transform(
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 @@ -283,7 +283,7 @@ def test_update_stack_with_same_template_withoutchange(

snapshot.match("no_change_exception", ctx.value.response)

@pytest.mark.skip(reason="CFNV2:Transform")
@pytest.mark.skip(reason="CFNV2:Other")
@markers.aws.validated
def test_update_stack_with_same_template_withoutchange_transformation(
self, deploy_cfn_template, aws_client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
reason="Only targeting the new engine",
)

pytestmark = pytest.mark.skip(reason="CFNV2:Transform")


@markers.aws.validated
@markers.snapshot.skip_snapshot_verify(paths=["$..tags"])
Expand Down Expand Up @@ -73,6 +71,12 @@ def test_duplicate_resources(deploy_cfn_template, s3_bucket, snapshot, aws_clien
snapshot.match("api-resources", resources)


@pytest.mark.skip(
reason=(
"CFNV2:AWS::Include the transformation is run however the "
"physical resource id for the resource is not available"
)
)
@markers.aws.validated
def test_transformer_property_level(deploy_cfn_template, s3_bucket, aws_client, snapshot):
api_spec = textwrap.dedent("""
Expand Down Expand Up @@ -125,6 +129,12 @@ def test_transformer_property_level(deploy_cfn_template, s3_bucket, aws_client,
snapshot.match("processed_template", processed_template)


@pytest.mark.skip(
reason=(
"CFNV2:AWS::Include the transformation is run however the "
"physical resource id for the resource is not available"
)
)
@markers.aws.validated
def test_transformer_individual_resource_level(deploy_cfn_template, s3_bucket, aws_client):
api_spec = textwrap.dedent("""
Expand Down
Loading
Loading