Skip to content

Commit aae7310

Browse files
committed
move transform resolution to transformer
1 parent 7a4e241 commit aae7310

File tree

3 files changed

+167
-95
lines changed

3 files changed

+167
-95
lines changed

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,21 @@ def open_index(self, index: int) -> Scope:
102102
def unwrap(self) -> list[str]:
103103
return self.split(self._SEPARATOR)
104104

105+
def to_jsonpath(self) -> str:
106+
parts = self.split("/")
107+
json_parts = []
108+
109+
for part in parts:
110+
if not part: # Skip empty strings from leading/trailing slashes
111+
continue
112+
# Wrap keys with special characters (e.g., colon) in quotes
113+
if ":" in part:
114+
json_parts.append(f'"{part}"')
115+
else:
116+
json_parts.append(part)
117+
118+
return f"$.{'.'.join(json_parts)}"
119+
105120

106121
class ChangeType(enum.Enum):
107122
UNCHANGED = "Unchanged"

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

Lines changed: 4 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,6 @@
1010
from localstack import config
1111
from localstack.aws.api.ec2 import AvailabilityZoneList, DescribeAvailabilityZonesResult
1212
from localstack.aws.connect import connect_to
13-
from localstack.services.cloudformation.engine.transformers import (
14-
Transformer,
15-
execute_macro,
16-
transformers,
17-
)
1813
from localstack.services.cloudformation.engine.v2.change_set_model import (
1914
ChangeSetEntity,
2015
ChangeType,
@@ -24,7 +19,6 @@
2419
NodeDependsOn,
2520
NodeDivergence,
2621
NodeIntrinsicFunction,
27-
NodeIntrinsicFunctionFnTransform,
2822
NodeMapping,
2923
NodeObject,
3024
NodeOutput,
@@ -50,7 +44,6 @@
5044
)
5145
from localstack.services.cloudformation.stores import (
5246
exports_map,
53-
get_cloudformation_store,
5447
)
5548
from localstack.services.cloudformation.v2.entities import ChangeSet
5649
from localstack.utils.aws.arns import get_partition
@@ -997,6 +990,7 @@ def visit_node_properties(
997990
node_change_type = node_properties.change_type
998991
before_bindings = dict() if node_change_type != ChangeType.CREATED else Nothing
999992
after_bindings = dict() if node_change_type != ChangeType.REMOVED else Nothing
993+
1000994
for node_property in node_properties.properties:
1001995
property_name = node_property.name
1002996
delta = self.visit(node_property)
@@ -1173,91 +1167,6 @@ def _compute_fn_import_value(string) -> str:
11731167
return delta
11741168

11751169
def visit_node_intrinsic_function_fn_transform(
1176-
self, node_intrinsic_function: NodeIntrinsicFunctionFnTransform
1177-
) -> PreprocEntityDelta:
1178-
def _normalize_transforms(obj):
1179-
transforms = []
1180-
1181-
if isinstance(obj, str):
1182-
transforms.append({"Name": obj, "Parameters": {}})
1183-
1184-
if isinstance(obj, dict):
1185-
transforms.append(obj)
1186-
1187-
if isinstance(obj, list):
1188-
for v in obj:
1189-
if isinstance(v, str):
1190-
transforms.append({"Name": v, "Parameters": {}})
1191-
1192-
if isinstance(v, dict):
1193-
transforms.append(v)
1194-
1195-
return transforms
1196-
1197-
def _compute_fn_transform(transform: dict):
1198-
transforms: list[dict] = _normalize_transforms(transform)
1199-
1200-
siblings = node_intrinsic_function.after_siblings
1201-
transform_output = copy.deepcopy(siblings)
1202-
transform_output.pop("Fn::Transform", "")
1203-
for transform in transforms:
1204-
transform_name = transform["Name"]
1205-
if transform_name in transforms:
1206-
builtin_transformer_class = transformers[transform_name]
1207-
builtin_transformer: Transformer = builtin_transformer_class()
1208-
transform_output.update(
1209-
builtin_transformer.transform(
1210-
account_id=self._change_set.account_id,
1211-
region_name=self._change_set.region_name,
1212-
parameters=transform["Parameters"],
1213-
)
1214-
)
1215-
else:
1216-
macros_store = get_cloudformation_store(
1217-
account_id=self._change_set.account_id,
1218-
region_name=self._change_set.region_name,
1219-
).macros
1220-
1221-
if transform["Name"] not in macros_store:
1222-
raise RuntimeError("Unsupported transform ")
1223-
1224-
transform_parameters = {
1225-
x: v["ParameterValue"] if "ParameterValue" in v else v
1226-
for x, v in transform.get("Parameters", {}).items()
1227-
}
1228-
1229-
resolved_parameters = {
1230-
**self._change_set.stack.resolved_parameters,
1231-
**self.visit_node_parameters(
1232-
self._change_set.update_model.node_template.parameters
1233-
).after,
1234-
}
1235-
1236-
template_parameters = {
1237-
**self._change_set.stack.template.get("Parameters", {}),
1238-
**(self._change_set.template or {}).get("Parameters", {}),
1239-
}
1240-
1241-
for key, value in resolved_parameters.items():
1242-
template_parameters[key]["ParameterValue"] = value
1243-
1244-
transform_output: Any = execute_macro(
1245-
account_id=self._change_set.account_id,
1246-
region_name=self._change_set.region_name,
1247-
parsed_template=transform_output, # TODO: review the requirements for this argument.
1248-
macro=transform, # TODO: review support for non dict bindings (v1).
1249-
stack_parameters=template_parameters,
1250-
transformation_parameters=transform_parameters,
1251-
is_intrinsic=True,
1252-
)
1253-
1254-
return transform_output
1255-
1256-
arguments_delta = self.visit(node_intrinsic_function.arguments)
1257-
delta = self._cached_apply(
1258-
scope=node_intrinsic_function.scope,
1259-
arguments_delta=arguments_delta,
1260-
resolver=_compute_fn_transform,
1261-
)
1262-
1263-
return delta
1170+
self, node_intrinsic_function: NodeIntrinsicFunction
1171+
):
1172+
raise RuntimeError("Fn::Transform should have been handled by the Transformer")

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

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,19 @@
88
from samtranslator.translator.transform import transform as transform_sam
99

1010
from localstack.aws.connect import connect_to
11+
from localstack.services.cloudformation.engine import transformers
1112
from localstack.services.cloudformation.engine.policy_loader import create_policy_loader
1213
from localstack.services.cloudformation.engine.template_preparer import parse_template
1314
from localstack.services.cloudformation.engine.transformers import (
1415
FailedTransformationException,
16+
Transformer,
1517
execute_macro,
1618
)
1719
from localstack.services.cloudformation.engine.v2.change_set_model import (
1820
ChangeType,
1921
Maybe,
2022
NodeGlobalTransform,
23+
NodeIntrinsicFunctionFnTransform,
2124
NodeParameter,
2225
NodeTransform,
2326
Nothing,
@@ -313,3 +316,148 @@ def visit_node_transform(
313316
if not is_nothing(after) and not is_nothing(delta_after):
314317
after.append(delta_after)
315318
return PreprocEntityDelta(before=before, after=after)
319+
320+
def _compute_fn_transform(
321+
self, macro_definition: Any, siblings: Any, resolved_parameters: Any
322+
) -> Any:
323+
# TODO: add typing to arguments before this level.
324+
# TODO: add schema validation
325+
# TODO: add support for other transform types
326+
327+
account_id = self._change_set.account_id
328+
region_name = self._change_set.region_name
329+
330+
def _normalize_transform(obj):
331+
transforms = []
332+
333+
if isinstance(obj, str):
334+
transforms.append({"Name": obj, "Parameters": {}})
335+
336+
if isinstance(obj, dict):
337+
transforms.append(obj)
338+
339+
if isinstance(obj, list):
340+
for v in obj:
341+
if isinstance(v, str):
342+
transforms.append({"Name": v, "Parameters": {}})
343+
344+
if isinstance(v, dict):
345+
transforms.append(v)
346+
347+
return transforms
348+
349+
transforms = _normalize_transform(macro_definition)
350+
transform_output = copy.deepcopy(siblings)
351+
transform_output.pop("Fn::Transform", "")
352+
for transform in transforms:
353+
transform_name = transform["Name"]
354+
if transform_name in transforms:
355+
builtin_transformer_class = transformers[transform_name]
356+
builtin_transformer: Transformer = builtin_transformer_class()
357+
transform_output.update(
358+
builtin_transformer.transform(
359+
account_id=account_id,
360+
region_name=region_name,
361+
parameters=transform["Parameters"],
362+
)
363+
)
364+
else:
365+
macros_store = get_cloudformation_store(
366+
account_id=account_id, region_name=region_name
367+
).macros
368+
369+
if transform["Name"] not in macros_store:
370+
raise RuntimeError("Unsupported transform ")
371+
372+
stack_parameters = {
373+
**self._change_set.stack.resolved_parameters,
374+
**self.visit_node_parameters(
375+
self._change_set.update_model.node_template.parameters
376+
).after,
377+
}
378+
379+
transform_output: Any = execute_macro(
380+
account_id=account_id,
381+
region_name=region_name,
382+
parsed_template=transform_output, # TODO: review the requirements for this argument.
383+
macro=transform, # TODO: review support for non dict bindings (v1).
384+
stack_parameters=stack_parameters,
385+
transformation_parameters=transform.get("Parameters"),
386+
is_intrinsic=True,
387+
)
388+
389+
return transform_output
390+
391+
def _replace_at_jsonpath(self, template, node, result):
392+
path = node.scope.to_jsonpath()
393+
parent_path = ".".join(path.split(".")[:-1])
394+
import jsonpath_ng
395+
396+
pattern = jsonpath_ng.parse(parent_path)
397+
result_template = pattern.update(template, result)
398+
399+
return result_template
400+
401+
def visit_node_intrinsic_function_fn_transform(
402+
self, node_intrinsic_function: NodeIntrinsicFunctionFnTransform
403+
) -> PreprocEntityDelta:
404+
arguments_delta = self.visit(node_intrinsic_function.arguments)
405+
parameters_delta = self.visit_node_parameters(
406+
self._change_set.update_model.node_template.parameters
407+
)
408+
409+
if not is_nothing(arguments_delta.before):
410+
before = self._compute_fn_transform(
411+
arguments_delta.before,
412+
node_intrinsic_function.before_siblings,
413+
parameters_delta.before,
414+
)
415+
updated_before_template = self._replace_at_jsonpath(
416+
self._before_template, node_intrinsic_function, before
417+
)
418+
self._after_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = updated_before_template
419+
else:
420+
before = Nothing
421+
422+
if not is_nothing(arguments_delta.after):
423+
after = self._compute_fn_transform(
424+
arguments_delta.after,
425+
node_intrinsic_function.after_siblings,
426+
parameters_delta.after,
427+
)
428+
updated_after_template = self._replace_at_jsonpath(
429+
self._after_template, node_intrinsic_function, after
430+
)
431+
self._after_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = updated_after_template
432+
else:
433+
after = Nothing
434+
435+
self._save_runtime_cache()
436+
return PreprocEntityDelta(before=before, after=after)
437+
438+
# def visit_node_properties(
439+
# self, node_properties: NodeProperties
440+
# ) -> PreprocEntityDelta[PreprocProperties, PreprocProperties]:
441+
#
442+
# before = node_properties
443+
# for property in node_properties.properties:
444+
# if property.name == "Fn::Transform":
445+
# path = "$" + ".".join(node_properties.scope.split("/")[:-1])
446+
# before_siblings = extract_jsonpath(self._before_template, path)
447+
# after_siblings = extract_jsonpath(self._after_template, path)
448+
# intrinsic_transform = NodeIntrinsicFunctionFnTransform(
449+
# change_type=property.change_type,
450+
# intrinsic_function=property.name,
451+
# scope=node_properties.scope,
452+
# arguments=property.value,
453+
# before_siblings=before_siblings,
454+
# after_siblings=after_siblings,
455+
# )
456+
# delta = self.visit(intrinsic_transform)
457+
# node_properties = delta.after
458+
#
459+
#
460+
#
461+
# return PreprocEntityDelta(before=super().visit(node_properties), after=Nothing)
462+
#
463+
#

0 commit comments

Comments
 (0)