Skip to content

Commit 75d12d6

Browse files
committed
base support for fn::transform
1 parent 66937e0 commit 75d12d6

File tree

9 files changed

+2177
-3
lines changed

9 files changed

+2177
-3
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ def __init__(self, scope: Scope, value: Any):
386386
FnEqualsKey: Final[str] = "Fn::Equals"
387387
FnFindInMapKey: Final[str] = "Fn::FindInMap"
388388
FnSubKey: Final[str] = "Fn::Sub"
389+
FnTransform: Final[str] = "Fn::Transform"
389390
INTRINSIC_FUNCTIONS: Final[set[str]] = {
390391
RefKey,
391392
FnIfKey,
@@ -395,6 +396,7 @@ def __init__(self, scope: Scope, value: Any):
395396
FnGetAttKey,
396397
FnFindInMapKey,
397398
FnSubKey,
399+
FnTransform,
398400
}
399401

400402

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

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
import re
44
from typing import Any, Final, Generic, Optional, TypeVar
55

6+
from localstack.services.cloudformation.engine.transformers import (
7+
Transformer,
8+
execute_macro,
9+
transformers,
10+
)
611
from localstack.services.cloudformation.engine.v2.change_set_model import (
712
ChangeSetEntity,
813
ChangeType,
@@ -30,6 +35,7 @@
3035
from localstack.services.cloudformation.engine.v2.change_set_model_visitor import (
3136
ChangeSetModelVisitor,
3237
)
38+
from localstack.services.cloudformation.stores import get_cloudformation_store
3339
from localstack.services.cloudformation.v2.entities import ChangeSet
3440
from localstack.utils.aws.arns import get_partition
3541
from localstack.utils.urls import localstack_host
@@ -489,6 +495,78 @@ def visit_node_intrinsic_function_fn_not(
489495
# Implicit change type computation.
490496
return PreprocEntityDelta(before=before, after=after)
491497

498+
def _compute_fn_transform(self, args: dict[str, Any]) -> Any:
499+
# TODO: add typing to arguments before this level.
500+
# TODO: add schema validation
501+
# TODO: add support for other transform types
502+
503+
account_id = self._change_set.account_id
504+
region_name = self._change_set.region_name
505+
transform_name: str = args.get("Name")
506+
if not isinstance(transform_name, str):
507+
raise RuntimeError("Invalid or missing Fn::Transform 'Name' argument")
508+
transform_parameters: dict = args.get("Parameters")
509+
if not isinstance(transform_parameters, dict):
510+
raise RuntimeError("Invalid or missing Fn::Transform 'Parameters' argument")
511+
512+
if transform_name in transformers:
513+
# TODO: port and refactor this 'transformers' logic to this package.
514+
builtin_transformer_class = transformers[transform_name]
515+
builtin_transformer: Transformer = builtin_transformer_class()
516+
transform_output: Any = builtin_transformer.transform(
517+
account_id=account_id, region_name=region_name, parameters=transform_parameters
518+
)
519+
return transform_output
520+
521+
macros_store = get_cloudformation_store(
522+
account_id=account_id, region_name=region_name
523+
).macros
524+
if transform_name in macros_store:
525+
# TODO: this formatting of stack parameters is odd but required to integrate with v1 execute_macro util.
526+
# consider porting this utils and passing the plain list of parameters instead.
527+
stack_parameters = {
528+
parameter["ParameterKey"]: parameter
529+
for parameter in self._change_set.stack.parameters
530+
}
531+
transform_output: Any = execute_macro(
532+
account_id=account_id,
533+
region_name=region_name,
534+
parsed_template=dict(), # TODO: review the requirements for this argument.
535+
macro=args, # TODO: review support for non dict bindings (v1).
536+
stack_parameters=stack_parameters,
537+
transformation_parameters=transform_parameters,
538+
is_intrinsic=True,
539+
)
540+
return transform_output
541+
542+
raise RuntimeError(
543+
f"Unsupported transform function '{transform_name}' in '{self._change_set.stack.stack_name}'"
544+
)
545+
546+
def visit_node_intrinsic_function_fn_transform(
547+
self, node_intrinsic_function: NodeIntrinsicFunction
548+
) -> PreprocEntityDelta:
549+
arguments_delta = self.visit(node_intrinsic_function.arguments)
550+
arguments_before = arguments_delta.before
551+
arguments_after = arguments_delta.after
552+
553+
# TODO: review the use of cache in self.precessed from the 'before' run to
554+
# ensure changes to the lambda (such as after UpdateFunctionCode) do not
555+
# generalise tot he before value at this depth (thus making it seems as
556+
# though for this transformation before==after). Another options may be to
557+
# have specialised caching for transformations.
558+
559+
# TODO: add tests to review the behaviour of CFN with changes to transformation
560+
# function code and no changes to the template.
561+
562+
before = None
563+
if arguments_before:
564+
before = self._compute_fn_transform(args=arguments_before)
565+
after = None
566+
if arguments_after:
567+
after = self._compute_fn_transform(args=arguments_after)
568+
return PreprocEntityDelta(before=before, after=after)
569+
492570
def visit_node_intrinsic_function_fn_sub(
493571
self, node_intrinsic_function: NodeIntrinsicFunction
494572
) -> PreprocEntityDelta:

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ def visit_node_intrinsic_function_fn_equals(
113113
):
114114
self.visit_children(node_intrinsic_function)
115115

116+
def visit_node_intrinsic_function_fn_transform(
117+
self, node_intrinsic_function: NodeIntrinsicFunction
118+
):
119+
self.visit_children(node_intrinsic_function)
120+
116121
def visit_node_intrinsic_function_fn_sub(self, node_intrinsic_function: NodeIntrinsicFunction):
117122
self.visit_children(node_intrinsic_function)
118123

tests/aws/services/cloudformation/v2/ported_from_v1/api/test_stacks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ def test_update_stack_with_same_template_withoutchange(
283283

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

286-
@pytest.mark.skip(reason="CFNV2:Transform")
286+
@pytest.mark.skip(reason="CFNV2:Other")
287287
@markers.aws.validated
288288
def test_update_stack_with_same_template_withoutchange_transformation(
289289
self, deploy_cfn_template, aws_client

tests/aws/services/cloudformation/v2/ported_from_v1/api/test_transformers.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
reason="Only targeting the new engine",
1313
)
1414

15-
pytestmark = pytest.mark.skip(reason="CFNV2:Transform")
16-
1715

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

7573

74+
@pytest.mark.skip(
75+
reason=(
76+
"CFNV2:AWS::Include the transformation is run however the "
77+
"physical resource id for the resource is not available"
78+
)
79+
)
7680
@markers.aws.validated
7781
def test_transformer_property_level(deploy_cfn_template, s3_bucket, aws_client, snapshot):
7882
api_spec = textwrap.dedent("""
@@ -125,6 +129,12 @@ def test_transformer_property_level(deploy_cfn_template, s3_bucket, aws_client,
125129
snapshot.match("processed_template", processed_template)
126130

127131

132+
@pytest.mark.skip(
133+
reason=(
134+
"CFNV2:AWS::Include the transformation is run however the "
135+
"physical resource id for the resource is not available"
136+
)
137+
)
128138
@markers.aws.validated
129139
def test_transformer_individual_resource_level(deploy_cfn_template, s3_bucket, aws_client):
130140
api_spec = textwrap.dedent("""

tests/aws/services/cloudformation/v2/ported_from_v1/engine/test_mappings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ def test_mapping_ref_map_key(self, deploy_cfn_template, aws_client, map_key, sho
249249

250250
aws_client.sns.get_topic_attributes(TopicArn=topic_arn)
251251

252+
# @pytest.mark.skip(reason="CFNV2:Mappings")
252253
@markers.aws.validated
253254
def test_aws_refs_in_mappings(self, deploy_cfn_template, account_id):
254255
"""

0 commit comments

Comments
 (0)