Skip to content

Commit 4600910

Browse files
authored
CloudFormation: POC Support for Modeling of Outputs Blocks in the Update Graph, Improved Handling of Intrinsic Function Types (#12443)
1 parent c6340e2 commit 4600910

File tree

4 files changed

+517
-98
lines changed

4 files changed

+517
-98
lines changed

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

Lines changed: 148 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ class NodeTemplate(ChangeSetNode):
117117
parameters: Final[NodeParameters]
118118
conditions: Final[NodeConditions]
119119
resources: Final[NodeResources]
120+
outputs: Final[NodeOutputs]
120121

121122
def __init__(
122123
self,
@@ -126,12 +127,14 @@ def __init__(
126127
parameters: NodeParameters,
127128
conditions: NodeConditions,
128129
resources: NodeResources,
130+
outputs: NodeOutputs,
129131
):
130132
super().__init__(scope=scope, change_type=change_type)
131133
self.mappings = mappings
132134
self.parameters = parameters
133135
self.conditions = conditions
134136
self.resources = resources
137+
self.outputs = outputs
135138

136139

137140
class NodeDivergence(ChangeSetNode):
@@ -189,6 +192,36 @@ def __init__(self, scope: Scope, change_type: ChangeType, mappings: list[NodeMap
189192
self.mappings = mappings
190193

191194

195+
class NodeOutput(ChangeSetNode):
196+
name: Final[str]
197+
value: Final[ChangeSetEntity]
198+
export: Final[Optional[ChangeSetEntity]]
199+
condition_reference: Final[Optional[TerminalValue]]
200+
201+
def __init__(
202+
self,
203+
scope: Scope,
204+
change_type: ChangeType,
205+
name: str,
206+
value: ChangeSetEntity,
207+
export: Optional[ChangeSetEntity],
208+
conditional_reference: Optional[TerminalValue],
209+
):
210+
super().__init__(scope=scope, change_type=change_type)
211+
self.name = name
212+
self.value = value
213+
self.export = export
214+
self.condition_reference = conditional_reference
215+
216+
217+
class NodeOutputs(ChangeSetNode):
218+
outputs: Final[list[NodeOutput]]
219+
220+
def __init__(self, scope: Scope, change_type: ChangeType, outputs: list[NodeOutput]):
221+
super().__init__(scope=scope, change_type=change_type)
222+
self.outputs = outputs
223+
224+
192225
class NodeCondition(ChangeSetNode):
193226
name: Final[str]
194227
body: Final[ChangeSetEntity]
@@ -218,7 +251,7 @@ def __init__(self, scope: Scope, change_type: ChangeType, resources: list[NodeRe
218251
class NodeResource(ChangeSetNode):
219252
name: Final[str]
220253
type_: Final[ChangeSetTerminal]
221-
condition_reference: Final[TerminalValue]
254+
condition_reference: Final[Optional[TerminalValue]]
222255
properties: Final[NodeProperties]
223256

224257
def __init__(
@@ -325,6 +358,9 @@ def __init__(self, scope: Scope, value: Any):
325358
ResourcesKey: Final[str] = "Resources"
326359
PropertiesKey: Final[str] = "Properties"
327360
ParametersKey: Final[str] = "Parameters"
361+
ValueKey: Final[str] = "Value"
362+
ExportKey: Final[str] = "Export"
363+
OutputsKey: Final[str] = "Outputs"
328364
# TODO: expand intrinsic functions set.
329365
RefKey: Final[str] = "Ref"
330366
FnIf: Final[str] = "Fn::If"
@@ -567,17 +603,9 @@ def _visit_object(
567603
binding_scope, (before_value, after_value) = self._safe_access_in(
568604
scope, binding_name, before_object, after_object
569605
)
570-
if self._is_intrinsic_function_name(function_name=binding_name):
571-
value = self._visit_intrinsic_function(
572-
scope=binding_scope,
573-
intrinsic_function=binding_name,
574-
before_arguments=before_value,
575-
after_arguments=after_value,
576-
)
577-
else:
578-
value = self._visit_value(
579-
scope=binding_scope, before_value=before_value, after_value=after_value
580-
)
606+
value = self._visit_value(
607+
scope=binding_scope, before_value=before_value, after_value=after_value
608+
)
581609
bindings[binding_name] = value
582610
change_type = change_type.for_child(value.change_type)
583611
node_object = NodeObject(scope=scope, change_type=change_type, bindings=bindings)
@@ -601,8 +629,11 @@ def _visit_value(
601629
value = self._visited_scopes.get(scope)
602630
if isinstance(value, ChangeSetEntity):
603631
return value
632+
633+
before_type_name = self._type_name_of(before_value)
634+
after_type_name = self._type_name_of(after_value)
604635
unset = object()
605-
if type(before_value) is type(after_value):
636+
if before_type_name == after_type_name:
606637
dominant_value = before_value
607638
elif self._is_created(before=before_value, after=after_value):
608639
dominant_value = after_value
@@ -611,6 +642,7 @@ def _visit_value(
611642
else:
612643
dominant_value = unset
613644
if dominant_value is not unset:
645+
dominant_type_name = self._type_name_of(dominant_value)
614646
if self._is_terminal(value=dominant_value):
615647
value = self._visit_terminal_value(
616648
scope=scope, before_value=before_value, after_value=after_value
@@ -623,6 +655,16 @@ def _visit_value(
623655
value = self._visit_array(
624656
scope=scope, before_array=before_value, after_array=after_value
625657
)
658+
elif self._is_intrinsic_function_name(dominant_type_name):
659+
intrinsic_function_scope, (before_arguments, after_arguments) = (
660+
self._safe_access_in(scope, dominant_type_name, before_value, after_value)
661+
)
662+
value = self._visit_intrinsic_function(
663+
scope=scope,
664+
intrinsic_function=dominant_type_name,
665+
before_arguments=before_arguments,
666+
after_arguments=after_arguments,
667+
)
626668
else:
627669
raise RuntimeError(f"Unsupported type {type(dominant_value)}")
628670
# Case: type divergence.
@@ -717,12 +759,15 @@ def _visit_resource(
717759
# TODO: investigate behaviour with type changes, for now this is filler code.
718760
_, type_str = self._safe_access_in(scope, TypeKey, before_resource)
719761

762+
condition_reference = None
720763
scope_condition, (before_condition, after_condition) = self._safe_access_in(
721764
scope, ConditionKey, before_resource, after_resource
722765
)
723-
condition_reference = self._visit_terminal_value(
724-
scope_condition, before_condition, after_condition
725-
)
766+
# TODO: condition references should be resolved for the condition's change_type?
767+
if before_condition or after_condition:
768+
condition_reference = self._visit_terminal_value(
769+
scope_condition, before_condition, after_condition
770+
)
726771

727772
scope_properties, (before_properties, after_properties) = self._safe_access_in(
728773
scope, PropertiesKey, before_resource, after_resource
@@ -887,18 +932,9 @@ def _visit_condition(
887932
node_condition = self._visited_scopes.get(scope)
888933
if isinstance(node_condition, NodeCondition):
889934
return node_condition
890-
891-
# TODO: is schema validation/check necessary or can we trust the input at this point?
892-
function_names: list[str] = self._safe_keys_of(before_condition, after_condition)
893-
if len(function_names) == 1:
894-
body = self._visit_object(
895-
scope=scope, before_object=before_condition, after_object=after_condition
896-
)
897-
else:
898-
body = self._visit_divergence(
899-
scope=scope, before_value=before_condition, after_value=after_condition
900-
)
901-
935+
body = self._visit_value(
936+
scope=scope, before_value=before_condition, after_value=after_condition
937+
)
902938
node_condition = NodeCondition(
903939
scope=scope, change_type=body.change_type, name=condition_name, body=body
904940
)
@@ -932,6 +968,64 @@ def _visit_conditions(
932968
self._visited_scopes[scope] = node_conditions
933969
return node_conditions
934970

971+
def _visit_output(
972+
self, scope: Scope, name: str, before_output: Maybe[dict], after_output: Maybe[dict]
973+
) -> NodeOutput:
974+
change_type = ChangeType.UNCHANGED
975+
scope_value, (before_value, after_value) = self._safe_access_in(
976+
scope, ValueKey, before_output, after_output
977+
)
978+
value = self._visit_value(scope_value, before_value, after_value)
979+
change_type = change_type.for_child(value.change_type)
980+
981+
export: Optional[ChangeSetEntity] = None
982+
scope_export, (before_export, after_export) = self._safe_access_in(
983+
scope, ExportKey, before_output, after_output
984+
)
985+
if before_export or after_export:
986+
export = self._visit_value(scope_export, before_export, after_export)
987+
change_type = change_type.for_child(export.change_type)
988+
989+
# TODO: condition references should be resolved for the condition's change_type?
990+
condition_reference: Optional[TerminalValue] = None
991+
scope_condition, (before_condition, after_condition) = self._safe_access_in(
992+
scope, ConditionKey, before_output, after_output
993+
)
994+
if before_condition or after_condition:
995+
condition_reference = self._visit_terminal_value(
996+
scope_condition, before_condition, after_condition
997+
)
998+
change_type = change_type.for_child(condition_reference.change_type)
999+
1000+
return NodeOutput(
1001+
scope=scope,
1002+
change_type=change_type,
1003+
name=name,
1004+
value=value,
1005+
export=export,
1006+
conditional_reference=condition_reference,
1007+
)
1008+
1009+
def _visit_outputs(
1010+
self, scope: Scope, before_outputs: Maybe[dict], after_outputs: Maybe[dict]
1011+
) -> NodeOutputs:
1012+
change_type = ChangeType.UNCHANGED
1013+
outputs: list[NodeOutput] = list()
1014+
output_names: list[str] = self._safe_keys_of(before_outputs, after_outputs)
1015+
for output_name in output_names:
1016+
scope_output, (before_output, after_output) = self._safe_access_in(
1017+
scope, output_name, before_outputs, after_outputs
1018+
)
1019+
output = self._visit_output(
1020+
scope=scope_output,
1021+
name=output_name,
1022+
before_output=before_output,
1023+
after_output=after_output,
1024+
)
1025+
outputs.append(output)
1026+
change_type = change_type.for_child(output.change_type)
1027+
return NodeOutputs(scope=scope, change_type=change_type, outputs=outputs)
1028+
9351029
def _model(self, before_template: Maybe[dict], after_template: Maybe[dict]) -> NodeTemplate:
9361030
root_scope = Scope()
9371031
# TODO: visit other child types
@@ -970,6 +1064,13 @@ def _model(self, before_template: Maybe[dict], after_template: Maybe[dict]) -> N
9701064
after_resources=after_resources,
9711065
)
9721066

1067+
outputs_scope, (before_outputs, after_outputs) = self._safe_access_in(
1068+
root_scope, OutputsKey, before_template, after_template
1069+
)
1070+
outputs = self._visit_outputs(
1071+
scope=outputs_scope, before_outputs=before_outputs, after_outputs=after_outputs
1072+
)
1073+
9731074
# TODO: compute the change_type of the template properly.
9741075
return NodeTemplate(
9751076
scope=root_scope,
@@ -978,6 +1079,7 @@ def _model(self, before_template: Maybe[dict], after_template: Maybe[dict]) -> N
9781079
parameters=parameters,
9791080
conditions=conditions,
9801081
resources=resources,
1082+
outputs=outputs,
9811083
)
9821084

9831085
def _retrieve_condition_if_exists(self, condition_name: str) -> Optional[NodeCondition]:
@@ -1090,13 +1192,30 @@ def _change_type_for_parent_of(change_types: list[ChangeType]) -> ChangeType:
10901192
break
10911193
return parent_change_type
10921194

1195+
@staticmethod
1196+
def _name_if_intrinsic_function(value: Maybe[Any]) -> Optional[str]:
1197+
if isinstance(value, dict):
1198+
keys = ChangeSetModel._safe_keys_of(value)
1199+
if len(keys) == 1:
1200+
key_name = keys[0]
1201+
if ChangeSetModel._is_intrinsic_function_name(key_name):
1202+
return key_name
1203+
return None
1204+
1205+
@staticmethod
1206+
def _type_name_of(value: Maybe[Any]) -> str:
1207+
maybe_intrinsic_function_name = ChangeSetModel._name_if_intrinsic_function(value)
1208+
if maybe_intrinsic_function_name is not None:
1209+
return maybe_intrinsic_function_name
1210+
return type(value).__name__
1211+
10931212
@staticmethod
10941213
def _is_terminal(value: Any) -> bool:
10951214
return type(value) in {int, float, bool, str, None, NothingType}
10961215

10971216
@staticmethod
10981217
def _is_object(value: Any) -> bool:
1099-
return isinstance(value, dict)
1218+
return isinstance(value, dict) and ChangeSetModel._name_if_intrinsic_function(value) is None
11001219

11011220
@staticmethod
11021221
def _is_array(value: Any) -> bool:

0 commit comments

Comments
 (0)