Skip to content

Commit dbca36f

Browse files
committed
base support for update graph modeling and describe of mappings and findinmap
1 parent 0d63c5e commit dbca36f

File tree

4 files changed

+406
-2
lines changed

4 files changed

+406
-2
lines changed

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

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ class ChangeSetTerminal(ChangeSetEntity, abc.ABC): ...
113113

114114

115115
class NodeTemplate(ChangeSetNode):
116+
mappings: Final[NodeMappings]
116117
parameters: Final[NodeParameters]
117118
conditions: Final[NodeConditions]
118119
resources: Final[NodeResources]
@@ -121,11 +122,13 @@ def __init__(
121122
self,
122123
scope: Scope,
123124
change_type: ChangeType,
125+
mappings: NodeMappings,
124126
parameters: NodeParameters,
125127
conditions: NodeConditions,
126128
resources: NodeResources,
127129
):
128130
super().__init__(scope=scope, change_type=change_type)
131+
self.mappings = mappings
129132
self.parameters = parameters
130133
self.conditions = conditions
131134
self.resources = resources
@@ -168,6 +171,24 @@ def __init__(self, scope: Scope, change_type: ChangeType, parameters: list[NodeP
168171
self.parameters = parameters
169172

170173

174+
class NodeMapping(ChangeSetNode):
175+
name: Final[str]
176+
bindings: Final[NodeObject]
177+
178+
def __init__(self, scope: Scope, change_type: ChangeType, name: str, bindings: NodeObject):
179+
super().__init__(scope=scope, change_type=change_type)
180+
self.name = name
181+
self.bindings = bindings
182+
183+
184+
class NodeMappings(ChangeSetNode):
185+
mappings: Final[list[NodeMapping]]
186+
187+
def __init__(self, scope: Scope, change_type: ChangeType, mappings: list[NodeMapping]):
188+
super().__init__(scope=scope, change_type=change_type)
189+
self.mappings = mappings
190+
191+
171192
class NodeCondition(ChangeSetNode):
172193
name: Final[str]
173194
body: Final[ChangeSetEntity]
@@ -300,6 +321,7 @@ def __init__(self, scope: Scope, value: Any):
300321
TypeKey: Final[str] = "Type"
301322
ConditionKey: Final[str] = "Condition"
302323
ConditionsKey: Final[str] = "Conditions"
324+
MappingsKey: Final[str] = "Mappings"
303325
ResourcesKey: Final[str] = "Resources"
304326
PropertiesKey: Final[str] = "Properties"
305327
ParametersKey: Final[str] = "Parameters"
@@ -309,7 +331,15 @@ def __init__(self, scope: Scope, value: Any):
309331
FnNot: Final[str] = "Fn::Not"
310332
FnGetAttKey: Final[str] = "Fn::GetAtt"
311333
FnEqualsKey: Final[str] = "Fn::Equals"
312-
INTRINSIC_FUNCTIONS: Final[set[str]] = {RefKey, FnIf, FnNot, FnEqualsKey, FnGetAttKey}
334+
FnFindInMapKey: Final[str] = "Fn::FindInMap"
335+
INTRINSIC_FUNCTIONS: Final[set[str]] = {
336+
RefKey,
337+
FnIf,
338+
FnNot,
339+
FnEqualsKey,
340+
FnGetAttKey,
341+
FnFindInMapKey,
342+
}
313343

314344

315345
class ChangeSetModel:
@@ -455,6 +485,36 @@ def _resolve_intrinsic_function_ref(self, arguments: ChangeSetEntity) -> ChangeT
455485
node_resource = self._retrieve_or_visit_resource(resource_name=logical_id)
456486
return node_resource.change_type
457487

488+
def _resolve_intrinsic_function_fn_find_in_map(self, arguments: ChangeSetEntity) -> ChangeType:
489+
if arguments.change_type != ChangeType.UNCHANGED:
490+
return arguments.change_type
491+
# TODO: validate arguments structure and type.
492+
# TODO: add support for nested functions, here we assume the arguments are string literals.
493+
494+
if not isinstance(arguments, NodeArray) or not arguments.array:
495+
raise RuntimeError()
496+
argument_mapping_name = arguments.array[0]
497+
if not isinstance(argument_mapping_name, TerminalValue):
498+
raise NotImplementedError()
499+
argument_top_level_key = arguments.array[1]
500+
if not isinstance(argument_top_level_key, TerminalValue):
501+
raise NotImplementedError()
502+
argument_second_level_key = arguments.array[2]
503+
if not isinstance(argument_second_level_key, TerminalValue):
504+
raise NotImplementedError()
505+
mapping_name = argument_mapping_name.value
506+
top_level_key = argument_top_level_key.value
507+
second_level_key = argument_second_level_key.value
508+
509+
node_mapping = self._retrieve_mapping(mapping_name=mapping_name)
510+
# TODO: a lookup would be beneficial in this scenario too;
511+
# consider implications downstream and for replication.
512+
top_level_object = node_mapping.bindings.bindings.get(top_level_key)
513+
if not isinstance(top_level_object, NodeObject):
514+
raise RuntimeError()
515+
target_map_value = top_level_object.bindings.get(second_level_key)
516+
return target_map_value.change_type
517+
458518
def _resolve_intrinsic_function_fn_if(self, arguments: ChangeSetEntity) -> ChangeType:
459519
# TODO: validate arguments structure and type.
460520
if not isinstance(arguments, NodeArray) or not arguments.array:
@@ -705,6 +765,36 @@ def _visit_resources(
705765
change_type = change_type.for_child(resource.change_type)
706766
return NodeResources(scope=scope, change_type=change_type, resources=resources)
707767

768+
def _visit_mapping(
769+
self, scope: Scope, name: str, before_mapping: Maybe[dict], after_mapping: Maybe[dict]
770+
) -> NodeMapping:
771+
bindings = self._visit_object(
772+
scope=scope, before_object=before_mapping, after_object=after_mapping
773+
)
774+
return NodeMapping(
775+
scope=scope, change_type=bindings.change_type, name=name, bindings=bindings
776+
)
777+
778+
def _visit_mappings(
779+
self, scope: Scope, before_mappings: Maybe[dict], after_mappings: Maybe[dict]
780+
) -> NodeMappings:
781+
change_type = ChangeType.UNCHANGED
782+
mappings: list[NodeMapping] = list()
783+
mapping_names = self._safe_keys_of(before_mappings, after_mappings)
784+
for mapping_name in mapping_names:
785+
scope_mapping, (before_mapping, after_mapping) = self._safe_access_in(
786+
scope, mapping_name, before_mappings, after_mappings
787+
)
788+
mapping = self._visit_mapping(
789+
scope=scope,
790+
name=mapping_name,
791+
before_mapping=before_mapping,
792+
after_mapping=after_mapping,
793+
)
794+
mappings.append(mapping)
795+
change_type = change_type.for_child(mapping.change_type)
796+
return NodeMappings(scope=scope, change_type=change_type, mappings=mappings)
797+
708798
def _visit_dynamic_parameter(self, parameter_name: str) -> ChangeSetEntity:
709799
scope = Scope("Dynamic").open_scope("Parameters")
710800
scope_parameter, (before_parameter, after_parameter) = self._safe_access_in(
@@ -845,6 +935,14 @@ def _visit_conditions(
845935
def _model(self, before_template: Maybe[dict], after_template: Maybe[dict]) -> NodeTemplate:
846936
root_scope = Scope()
847937
# TODO: visit other child types
938+
939+
mappings_scope, (before_mappings, after_mappings) = self._safe_access_in(
940+
root_scope, MappingsKey, before_template, after_template
941+
)
942+
mappings = self._visit_mappings(
943+
scope=mappings_scope, before_mappings=before_mappings, after_mappings=after_mappings
944+
)
945+
848946
parameters_scope, (before_parameters, after_parameters) = self._safe_access_in(
849947
root_scope, ParametersKey, before_template, after_template
850948
)
@@ -876,6 +974,7 @@ def _model(self, before_template: Maybe[dict], after_template: Maybe[dict]) -> N
876974
return NodeTemplate(
877975
scope=root_scope,
878976
change_type=resources.change_type,
977+
mappings=mappings,
879978
parameters=parameters,
880979
conditions=conditions,
881980
resources=resources,
@@ -919,6 +1018,23 @@ def _retrieve_parameter_if_exists(self, parameter_name: str) -> Optional[NodePar
9191018
return node_parameter
9201019
return None
9211020

1021+
def _retrieve_mapping(self, mapping_name) -> NodeMapping:
1022+
# TODO: add caching mechanism, and raise appropriate error if missing.
1023+
scope_mappings, (before_mappings, after_mappings) = self._safe_access_in(
1024+
Scope(), MappingsKey, self._before_template, self._after_template
1025+
)
1026+
before_mappings = before_mappings or dict()
1027+
after_mappings = after_mappings or dict()
1028+
if mapping_name in before_mappings or mapping_name in after_mappings:
1029+
scope_mapping, (before_mapping, after_mapping) = self._safe_access_in(
1030+
scope_mappings, mapping_name, before_mappings, after_mappings
1031+
)
1032+
node_mapping = self._visit_mapping(
1033+
scope_mapping, mapping_name, before_mapping, after_mapping
1034+
)
1035+
return node_mapping
1036+
raise RuntimeError()
1037+
9221038
def _retrieve_or_visit_resource(self, resource_name: str) -> NodeResource:
9231039
resources_scope, (before_resources, after_resources) = self._safe_access_in(
9241040
Scope(),

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

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
NodeCondition,
1212
NodeDivergence,
1313
NodeIntrinsicFunction,
14+
NodeMapping,
1415
NodeObject,
1516
NodeParameter,
1617
NodeProperties,
@@ -74,6 +75,15 @@ def _get_node_property_for(property_name: str, node_resource: NodeResource) -> N
7475
# TODO
7576
raise RuntimeError()
7677

78+
def _get_node_mapping(self, map_name: str) -> NodeMapping:
79+
mappings: list[NodeMapping] = self._node_template.mappings.mappings
80+
# TODO: another scenarios suggesting property lookups might be preferable.
81+
for mapping in mappings:
82+
if mapping.name == map_name:
83+
return mapping
84+
# TODO
85+
raise RuntimeError()
86+
7787
def _get_node_parameter_if_exists(self, parameter_name: str) -> Optional[NodeParameter]:
7888
parameters: list[NodeParameter] = self._node_template.parameters.parameters
7989
# TODO: another scenarios suggesting property lookups might be preferable.
@@ -109,6 +119,16 @@ def _resolve_reference(self, logica_id: str) -> DescribeUnit:
109119
resource_unit = DescribeUnit(before_context=limitation_str, after_context=limitation_str)
110120
return resource_unit
111121

122+
def _resolve_mapping(self, map_name: str, top_level_key: str, second_level_key) -> DescribeUnit:
123+
# TODO: add support for nested intrinsic functions, and KNOWN AFTER APPLY logical ids.
124+
node_mapping: NodeMapping = self._get_node_mapping(map_name=map_name)
125+
top_level_value = node_mapping.bindings.bindings.get(top_level_key)
126+
if not isinstance(top_level_value, NodeObject):
127+
raise RuntimeError()
128+
second_level_value = top_level_value.bindings.get(second_level_key)
129+
mapping_value_unit = self.visit(second_level_value)
130+
return mapping_value_unit
131+
112132
def _resolve_reference_binding(
113133
self, before_logical_id: str, after_logical_id: str
114134
) -> DescribeUnit:
@@ -281,8 +301,31 @@ def visit_node_intrinsic_function_fn_not(
281301
# Implicit change type computation.
282302
return DescribeUnit(before_context=before_context, after_context=after_context)
283303

304+
def visit_node_intrinsic_function_fn_find_in_map(
305+
self, node_intrinsic_function: NodeIntrinsicFunction
306+
) -> DescribeUnit:
307+
# TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments.
308+
# TODO: add type checking/validation for result unit?
309+
arguments_unit = self.visit(node_intrinsic_function.arguments)
310+
before_arguments = arguments_unit.before_context
311+
after_arguments = arguments_unit.after_context
312+
if before_arguments:
313+
before_value_unit = self._resolve_mapping(*before_arguments)
314+
before_context = before_value_unit.before_context
315+
else:
316+
before_context = None
317+
if after_arguments:
318+
after_value_unit = self._resolve_mapping(*after_arguments)
319+
after_context = after_value_unit.after_context
320+
else:
321+
after_context = None
322+
return DescribeUnit(before_context=before_context, after_context=after_context)
323+
324+
def visit_node_mapping(self, node_mapping: NodeMapping) -> DescribeUnit:
325+
bindings_unit = self.visit(node_mapping.bindings)
326+
return bindings_unit
327+
284328
def visit_node_parameter(self, node_parameter: NodeParameter) -> DescribeUnit:
285-
# TODO: add caching for these operation, parameters may be referenced more than once.
286329
# TODO: add support for default value sampling
287330
dynamic_value = node_parameter.dynamic_value
288331
describe_unit = self.visit(dynamic_value)

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
NodeConditions,
88
NodeDivergence,
99
NodeIntrinsicFunction,
10+
NodeMapping,
11+
NodeMappings,
1012
NodeObject,
1113
NodeParameter,
1214
NodeParameters,
@@ -45,6 +47,12 @@ def visit_children(self, change_set_entity: ChangeSetEntity):
4547
def visit_node_template(self, node_template: NodeTemplate):
4648
self.visit_children(node_template)
4749

50+
def visit_node_mapping(self, node_mapping: NodeMapping):
51+
self.visit_children(node_mapping)
52+
53+
def visit_node_mappings(self, node_mappings: NodeMappings):
54+
self.visit_children(node_mappings)
55+
4856
def visit_node_parameters(self, node_parameters: NodeParameters):
4957
self.visit_children(node_parameters)
5058

@@ -94,6 +102,11 @@ def visit_node_intrinsic_function_fn_if(self, node_intrinsic_function: NodeIntri
94102
def visit_node_intrinsic_function_fn_not(self, node_intrinsic_function: NodeIntrinsicFunction):
95103
self.visit_children(node_intrinsic_function)
96104

105+
def visit_node_intrinsic_function_fn_find_in_map(
106+
self, node_intrinsic_function: NodeIntrinsicFunction
107+
):
108+
self.visit_children(node_intrinsic_function)
109+
97110
def visit_node_intrinsic_function_ref(self, node_intrinsic_function: NodeIntrinsicFunction):
98111
self.visit_children(node_intrinsic_function)
99112

0 commit comments

Comments
 (0)