localstack-core 4.3.1.dev5__py3-none-any.whl → 4.3.1.dev27__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. localstack/services/cloudformation/engine/entities.py +18 -1
  2. localstack/services/cloudformation/engine/template_deployer.py +0 -9
  3. localstack/services/cloudformation/engine/v2/change_set_model.py +281 -36
  4. localstack/services/cloudformation/engine/v2/change_set_model_describer.py +187 -70
  5. localstack/services/cloudformation/engine/v2/change_set_model_executor.py +170 -0
  6. localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +21 -0
  7. localstack/services/cloudformation/v2/provider.py +72 -6
  8. localstack/services/ec2/patches.py +31 -3
  9. localstack/services/kms/models.py +1 -1
  10. localstack/services/lambda_/event_source_mapping/pollers/dynamodb_poller.py +2 -0
  11. localstack/services/lambda_/event_source_mapping/pollers/kinesis_poller.py +2 -0
  12. localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py +4 -2
  13. localstack/services/lambda_/invocation/assignment.py +4 -2
  14. localstack/services/lambda_/invocation/execution_environment.py +16 -4
  15. localstack/services/lambda_/invocation/logs.py +28 -4
  16. localstack/services/lambda_/provider.py +18 -3
  17. localstack/services/lambda_/runtimes.py +15 -2
  18. localstack/services/s3/presigned_url.py +15 -11
  19. localstack/services/secretsmanager/provider.py +13 -4
  20. localstack/services/sqs/models.py +22 -3
  21. localstack/services/sqs/utils.py +16 -7
  22. localstack/services/ssm/resource_providers/aws_ssm_parameter.py +1 -5
  23. localstack/services/stepfunctions/asl/utils/json_path.py +9 -0
  24. localstack/testing/snapshots/transformer_utility.py +13 -0
  25. localstack/utils/aws/client_types.py +8 -0
  26. localstack/utils/docker_utils.py +2 -2
  27. localstack/version.py +2 -2
  28. {localstack_core-4.3.1.dev5.dist-info → localstack_core-4.3.1.dev27.dist-info}/METADATA +3 -3
  29. {localstack_core-4.3.1.dev5.dist-info → localstack_core-4.3.1.dev27.dist-info}/RECORD +37 -36
  30. localstack_core-4.3.1.dev27.dist-info/plux.json +1 -0
  31. localstack_core-4.3.1.dev5.dist-info/plux.json +0 -1
  32. {localstack_core-4.3.1.dev5.data → localstack_core-4.3.1.dev27.data}/scripts/localstack +0 -0
  33. {localstack_core-4.3.1.dev5.data → localstack_core-4.3.1.dev27.data}/scripts/localstack-supervisor +0 -0
  34. {localstack_core-4.3.1.dev5.data → localstack_core-4.3.1.dev27.data}/scripts/localstack.bat +0 -0
  35. {localstack_core-4.3.1.dev5.dist-info → localstack_core-4.3.1.dev27.dist-info}/WHEEL +0 -0
  36. {localstack_core-4.3.1.dev5.dist-info → localstack_core-4.3.1.dev27.dist-info}/entry_points.txt +0 -0
  37. {localstack_core-4.3.1.dev5.dist-info → localstack_core-4.3.1.dev27.dist-info}/licenses/LICENSE.txt +0 -0
  38. {localstack_core-4.3.1.dev5.dist-info → localstack_core-4.3.1.dev27.dist-info}/top_level.txt +0 -0
@@ -297,6 +297,10 @@ class Stack:
297
297
  """Return dict of resources"""
298
298
  return dict(self.template_resources)
299
299
 
300
+ @resources.setter
301
+ def resources(self, resources: dict):
302
+ self.template["Resources"] = resources
303
+
300
304
  @property
301
305
  def template_resources(self):
302
306
  return self.template.setdefault("Resources", {})
@@ -370,8 +374,17 @@ class Stack:
370
374
  # TODO: what functionality of the Stack object do we rely on here?
371
375
  class StackChangeSet(Stack):
372
376
  update_graph: NodeTemplate | None
377
+ change_set_type: ChangeSetType | None
373
378
 
374
- def __init__(self, account_id: str, region_name: str, stack: Stack, params=None, template=None):
379
+ def __init__(
380
+ self,
381
+ account_id: str,
382
+ region_name: str,
383
+ stack: Stack,
384
+ params=None,
385
+ template=None,
386
+ change_set_type: ChangeSetType | None = None,
387
+ ):
375
388
  if template is None:
376
389
  template = {}
377
390
  if params is None:
@@ -389,6 +402,7 @@ class StackChangeSet(Stack):
389
402
  self.stack = stack
390
403
  self.metadata["StackId"] = stack.stack_id
391
404
  self.metadata["Status"] = "CREATE_PENDING"
405
+ self.change_set_type = change_set_type
392
406
 
393
407
  @property
394
408
  def change_set_id(self):
@@ -412,5 +426,8 @@ class StackChangeSet(Stack):
412
426
  change_set_model = ChangeSetModel(
413
427
  before_template=before_template,
414
428
  after_template=after_template,
429
+ # TODO
430
+ before_parameters=None,
431
+ after_parameters=None,
415
432
  )
416
433
  self.update_graph = change_set_model.get_update_model()
@@ -1409,15 +1409,6 @@ class TemplateDeployer:
1409
1409
  ) # TODO: why is there a fallback?
1410
1410
  resource["ResourceType"] = get_resource_type(resource)
1411
1411
 
1412
- def _safe_lookup_is_deleted(r_id):
1413
- """handles the case where self.stack.resource_status(..) fails for whatever reason"""
1414
- try:
1415
- return self.stack.resource_status(r_id).get("ResourceStatus") == "DELETE_COMPLETE"
1416
- except Exception:
1417
- if config.CFN_VERBOSE_ERRORS:
1418
- LOG.exception("failed to lookup if resource %s is deleted", r_id)
1419
- return True # just an assumption
1420
-
1421
1412
  ordered_resource_ids = list(
1422
1413
  order_resources(
1423
1414
  resources=original_resources,
@@ -7,6 +7,7 @@ from typing import Any, Final, Generator, Optional, Union, cast
7
7
 
8
8
  from typing_extensions import TypeVar
9
9
 
10
+ from localstack.aws.api.cloudformation import ChangeAction
10
11
  from localstack.utils.strings import camel_to_snake_case
11
12
 
12
13
  T = TypeVar("T")
@@ -66,6 +67,15 @@ class ChangeType(enum.Enum):
66
67
  def __str__(self):
67
68
  return self.value
68
69
 
70
+ def to_action(self) -> ChangeAction | None:
71
+ match self:
72
+ case self.CREATED:
73
+ return ChangeAction.Add
74
+ case self.MODIFIED:
75
+ return ChangeAction.Modify
76
+ case self.REMOVED:
77
+ return ChangeAction.Remove
78
+
69
79
  def for_child(self, child_change_type: ChangeType) -> ChangeType:
70
80
  if child_change_type == self:
71
81
  return self
@@ -113,22 +123,28 @@ class ChangeSetTerminal(ChangeSetEntity, abc.ABC): ...
113
123
 
114
124
 
115
125
  class NodeTemplate(ChangeSetNode):
126
+ mappings: Final[NodeMappings]
116
127
  parameters: Final[NodeParameters]
117
128
  conditions: Final[NodeConditions]
118
129
  resources: Final[NodeResources]
130
+ outputs: Final[NodeOutputs]
119
131
 
120
132
  def __init__(
121
133
  self,
122
134
  scope: Scope,
123
135
  change_type: ChangeType,
136
+ mappings: NodeMappings,
124
137
  parameters: NodeParameters,
125
138
  conditions: NodeConditions,
126
139
  resources: NodeResources,
140
+ outputs: NodeOutputs,
127
141
  ):
128
142
  super().__init__(scope=scope, change_type=change_type)
143
+ self.mappings = mappings
129
144
  self.parameters = parameters
130
145
  self.conditions = conditions
131
146
  self.resources = resources
147
+ self.outputs = outputs
132
148
 
133
149
 
134
150
  class NodeDivergence(ChangeSetNode):
@@ -168,6 +184,54 @@ class NodeParameters(ChangeSetNode):
168
184
  self.parameters = parameters
169
185
 
170
186
 
187
+ class NodeMapping(ChangeSetNode):
188
+ name: Final[str]
189
+ bindings: Final[NodeObject]
190
+
191
+ def __init__(self, scope: Scope, change_type: ChangeType, name: str, bindings: NodeObject):
192
+ super().__init__(scope=scope, change_type=change_type)
193
+ self.name = name
194
+ self.bindings = bindings
195
+
196
+
197
+ class NodeMappings(ChangeSetNode):
198
+ mappings: Final[list[NodeMapping]]
199
+
200
+ def __init__(self, scope: Scope, change_type: ChangeType, mappings: list[NodeMapping]):
201
+ super().__init__(scope=scope, change_type=change_type)
202
+ self.mappings = mappings
203
+
204
+
205
+ class NodeOutput(ChangeSetNode):
206
+ name: Final[str]
207
+ value: Final[ChangeSetEntity]
208
+ export: Final[Optional[ChangeSetEntity]]
209
+ condition_reference: Final[Optional[TerminalValue]]
210
+
211
+ def __init__(
212
+ self,
213
+ scope: Scope,
214
+ change_type: ChangeType,
215
+ name: str,
216
+ value: ChangeSetEntity,
217
+ export: Optional[ChangeSetEntity],
218
+ conditional_reference: Optional[TerminalValue],
219
+ ):
220
+ super().__init__(scope=scope, change_type=change_type)
221
+ self.name = name
222
+ self.value = value
223
+ self.export = export
224
+ self.condition_reference = conditional_reference
225
+
226
+
227
+ class NodeOutputs(ChangeSetNode):
228
+ outputs: Final[list[NodeOutput]]
229
+
230
+ def __init__(self, scope: Scope, change_type: ChangeType, outputs: list[NodeOutput]):
231
+ super().__init__(scope=scope, change_type=change_type)
232
+ self.outputs = outputs
233
+
234
+
171
235
  class NodeCondition(ChangeSetNode):
172
236
  name: Final[str]
173
237
  body: Final[ChangeSetEntity]
@@ -197,7 +261,7 @@ class NodeResources(ChangeSetNode):
197
261
  class NodeResource(ChangeSetNode):
198
262
  name: Final[str]
199
263
  type_: Final[ChangeSetTerminal]
200
- condition_reference: Final[TerminalValue]
264
+ condition_reference: Final[Optional[TerminalValue]]
201
265
  properties: Final[NodeProperties]
202
266
 
203
267
  def __init__(
@@ -300,16 +364,28 @@ class TerminalValueUnchanged(TerminalValue):
300
364
  TypeKey: Final[str] = "Type"
301
365
  ConditionKey: Final[str] = "Condition"
302
366
  ConditionsKey: Final[str] = "Conditions"
367
+ MappingsKey: Final[str] = "Mappings"
303
368
  ResourcesKey: Final[str] = "Resources"
304
369
  PropertiesKey: Final[str] = "Properties"
305
370
  ParametersKey: Final[str] = "Parameters"
371
+ ValueKey: Final[str] = "Value"
372
+ ExportKey: Final[str] = "Export"
373
+ OutputsKey: Final[str] = "Outputs"
306
374
  # TODO: expand intrinsic functions set.
307
375
  RefKey: Final[str] = "Ref"
308
376
  FnIf: Final[str] = "Fn::If"
309
377
  FnNot: Final[str] = "Fn::Not"
310
378
  FnGetAttKey: Final[str] = "Fn::GetAtt"
311
379
  FnEqualsKey: Final[str] = "Fn::Equals"
312
- INTRINSIC_FUNCTIONS: Final[set[str]] = {RefKey, FnIf, FnNot, FnEqualsKey, FnGetAttKey}
380
+ FnFindInMapKey: Final[str] = "Fn::FindInMap"
381
+ INTRINSIC_FUNCTIONS: Final[set[str]] = {
382
+ RefKey,
383
+ FnIf,
384
+ FnNot,
385
+ FnEqualsKey,
386
+ FnGetAttKey,
387
+ FnFindInMapKey,
388
+ }
313
389
 
314
390
 
315
391
  class ChangeSetModel:
@@ -455,6 +531,36 @@ class ChangeSetModel:
455
531
  node_resource = self._retrieve_or_visit_resource(resource_name=logical_id)
456
532
  return node_resource.change_type
457
533
 
534
+ def _resolve_intrinsic_function_fn_find_in_map(self, arguments: ChangeSetEntity) -> ChangeType:
535
+ if arguments.change_type != ChangeType.UNCHANGED:
536
+ return arguments.change_type
537
+ # TODO: validate arguments structure and type.
538
+ # TODO: add support for nested functions, here we assume the arguments are string literals.
539
+
540
+ if not isinstance(arguments, NodeArray) or not arguments.array:
541
+ raise RuntimeError()
542
+ argument_mapping_name = arguments.array[0]
543
+ if not isinstance(argument_mapping_name, TerminalValue):
544
+ raise NotImplementedError()
545
+ argument_top_level_key = arguments.array[1]
546
+ if not isinstance(argument_top_level_key, TerminalValue):
547
+ raise NotImplementedError()
548
+ argument_second_level_key = arguments.array[2]
549
+ if not isinstance(argument_second_level_key, TerminalValue):
550
+ raise NotImplementedError()
551
+ mapping_name = argument_mapping_name.value
552
+ top_level_key = argument_top_level_key.value
553
+ second_level_key = argument_second_level_key.value
554
+
555
+ node_mapping = self._retrieve_mapping(mapping_name=mapping_name)
556
+ # TODO: a lookup would be beneficial in this scenario too;
557
+ # consider implications downstream and for replication.
558
+ top_level_object = node_mapping.bindings.bindings.get(top_level_key)
559
+ if not isinstance(top_level_object, NodeObject):
560
+ raise RuntimeError()
561
+ target_map_value = top_level_object.bindings.get(second_level_key)
562
+ return target_map_value.change_type
563
+
458
564
  def _resolve_intrinsic_function_fn_if(self, arguments: ChangeSetEntity) -> ChangeType:
459
565
  # TODO: validate arguments structure and type.
460
566
  if not isinstance(arguments, NodeArray) or not arguments.array:
@@ -507,17 +613,9 @@ class ChangeSetModel:
507
613
  binding_scope, (before_value, after_value) = self._safe_access_in(
508
614
  scope, binding_name, before_object, after_object
509
615
  )
510
- if self._is_intrinsic_function_name(function_name=binding_name):
511
- value = self._visit_intrinsic_function(
512
- scope=binding_scope,
513
- intrinsic_function=binding_name,
514
- before_arguments=before_value,
515
- after_arguments=after_value,
516
- )
517
- else:
518
- value = self._visit_value(
519
- scope=binding_scope, before_value=before_value, after_value=after_value
520
- )
616
+ value = self._visit_value(
617
+ scope=binding_scope, before_value=before_value, after_value=after_value
618
+ )
521
619
  bindings[binding_name] = value
522
620
  change_type = change_type.for_child(value.change_type)
523
621
  node_object = NodeObject(scope=scope, change_type=change_type, bindings=bindings)
@@ -541,8 +639,11 @@ class ChangeSetModel:
541
639
  value = self._visited_scopes.get(scope)
542
640
  if isinstance(value, ChangeSetEntity):
543
641
  return value
642
+
643
+ before_type_name = self._type_name_of(before_value)
644
+ after_type_name = self._type_name_of(after_value)
544
645
  unset = object()
545
- if type(before_value) is type(after_value):
646
+ if before_type_name == after_type_name:
546
647
  dominant_value = before_value
547
648
  elif self._is_created(before=before_value, after=after_value):
548
649
  dominant_value = after_value
@@ -551,6 +652,7 @@ class ChangeSetModel:
551
652
  else:
552
653
  dominant_value = unset
553
654
  if dominant_value is not unset:
655
+ dominant_type_name = self._type_name_of(dominant_value)
554
656
  if self._is_terminal(value=dominant_value):
555
657
  value = self._visit_terminal_value(
556
658
  scope=scope, before_value=before_value, after_value=after_value
@@ -563,6 +665,16 @@ class ChangeSetModel:
563
665
  value = self._visit_array(
564
666
  scope=scope, before_array=before_value, after_array=after_value
565
667
  )
668
+ elif self._is_intrinsic_function_name(dominant_type_name):
669
+ intrinsic_function_scope, (before_arguments, after_arguments) = (
670
+ self._safe_access_in(scope, dominant_type_name, before_value, after_value)
671
+ )
672
+ value = self._visit_intrinsic_function(
673
+ scope=scope,
674
+ intrinsic_function=dominant_type_name,
675
+ before_arguments=before_arguments,
676
+ after_arguments=after_arguments,
677
+ )
566
678
  else:
567
679
  raise RuntimeError(f"Unsupported type {type(dominant_value)}")
568
680
  # Case: type divergence.
@@ -584,24 +696,24 @@ class ChangeSetModel:
584
696
  if isinstance(node_property, NodeProperty):
585
697
  return node_property
586
698
 
699
+ value = self._visit_value(
700
+ scope=scope, before_value=before_property, after_value=after_property
701
+ )
587
702
  if self._is_created(before=before_property, after=after_property):
588
703
  node_property = NodeProperty(
589
704
  scope=scope,
590
705
  change_type=ChangeType.CREATED,
591
706
  name=property_name,
592
- value=TerminalValueCreated(scope=scope, value=after_property),
707
+ value=value,
593
708
  )
594
709
  elif self._is_removed(before=before_property, after=after_property):
595
710
  node_property = NodeProperty(
596
711
  scope=scope,
597
712
  change_type=ChangeType.REMOVED,
598
713
  name=property_name,
599
- value=TerminalValueRemoved(scope=scope, value=before_property),
714
+ value=value,
600
715
  )
601
716
  else:
602
- value = self._visit_value(
603
- scope=scope, before_value=before_property, after_value=after_property
604
- )
605
717
  node_property = NodeProperty(
606
718
  scope=scope, change_type=value.change_type, name=property_name, value=value
607
719
  )
@@ -655,14 +767,17 @@ class ChangeSetModel:
655
767
  change_type = ChangeType.UNCHANGED
656
768
 
657
769
  # TODO: investigate behaviour with type changes, for now this is filler code.
658
- _, type_str = self._safe_access_in(scope, TypeKey, before_resource)
770
+ _, type_str = self._safe_access_in(scope, TypeKey, after_resource)
659
771
 
772
+ condition_reference = None
660
773
  scope_condition, (before_condition, after_condition) = self._safe_access_in(
661
774
  scope, ConditionKey, before_resource, after_resource
662
775
  )
663
- condition_reference = self._visit_terminal_value(
664
- scope_condition, before_condition, after_condition
665
- )
776
+ # TODO: condition references should be resolved for the condition's change_type?
777
+ if before_condition or after_condition:
778
+ condition_reference = self._visit_terminal_value(
779
+ scope_condition, before_condition, after_condition
780
+ )
666
781
 
667
782
  scope_properties, (before_properties, after_properties) = self._safe_access_in(
668
783
  scope, PropertiesKey, before_resource, after_resource
@@ -705,6 +820,36 @@ class ChangeSetModel:
705
820
  change_type = change_type.for_child(resource.change_type)
706
821
  return NodeResources(scope=scope, change_type=change_type, resources=resources)
707
822
 
823
+ def _visit_mapping(
824
+ self, scope: Scope, name: str, before_mapping: Maybe[dict], after_mapping: Maybe[dict]
825
+ ) -> NodeMapping:
826
+ bindings = self._visit_object(
827
+ scope=scope, before_object=before_mapping, after_object=after_mapping
828
+ )
829
+ return NodeMapping(
830
+ scope=scope, change_type=bindings.change_type, name=name, bindings=bindings
831
+ )
832
+
833
+ def _visit_mappings(
834
+ self, scope: Scope, before_mappings: Maybe[dict], after_mappings: Maybe[dict]
835
+ ) -> NodeMappings:
836
+ change_type = ChangeType.UNCHANGED
837
+ mappings: list[NodeMapping] = list()
838
+ mapping_names = self._safe_keys_of(before_mappings, after_mappings)
839
+ for mapping_name in mapping_names:
840
+ scope_mapping, (before_mapping, after_mapping) = self._safe_access_in(
841
+ scope, mapping_name, before_mappings, after_mappings
842
+ )
843
+ mapping = self._visit_mapping(
844
+ scope=scope,
845
+ name=mapping_name,
846
+ before_mapping=before_mapping,
847
+ after_mapping=after_mapping,
848
+ )
849
+ mappings.append(mapping)
850
+ change_type = change_type.for_child(mapping.change_type)
851
+ return NodeMappings(scope=scope, change_type=change_type, mappings=mappings)
852
+
708
853
  def _visit_dynamic_parameter(self, parameter_name: str) -> ChangeSetEntity:
709
854
  scope = Scope("Dynamic").open_scope("Parameters")
710
855
  scope_parameter, (before_parameter, after_parameter) = self._safe_access_in(
@@ -797,18 +942,9 @@ class ChangeSetModel:
797
942
  node_condition = self._visited_scopes.get(scope)
798
943
  if isinstance(node_condition, NodeCondition):
799
944
  return node_condition
800
-
801
- # TODO: is schema validation/check necessary or can we trust the input at this point?
802
- function_names: list[str] = self._safe_keys_of(before_condition, after_condition)
803
- if len(function_names) == 1:
804
- body = self._visit_object(
805
- scope=scope, before_object=before_condition, after_object=after_condition
806
- )
807
- else:
808
- body = self._visit_divergence(
809
- scope=scope, before_value=before_condition, after_value=after_condition
810
- )
811
-
945
+ body = self._visit_value(
946
+ scope=scope, before_value=before_condition, after_value=after_condition
947
+ )
812
948
  node_condition = NodeCondition(
813
949
  scope=scope, change_type=body.change_type, name=condition_name, body=body
814
950
  )
@@ -842,9 +978,75 @@ class ChangeSetModel:
842
978
  self._visited_scopes[scope] = node_conditions
843
979
  return node_conditions
844
980
 
981
+ def _visit_output(
982
+ self, scope: Scope, name: str, before_output: Maybe[dict], after_output: Maybe[dict]
983
+ ) -> NodeOutput:
984
+ change_type = ChangeType.UNCHANGED
985
+ scope_value, (before_value, after_value) = self._safe_access_in(
986
+ scope, ValueKey, before_output, after_output
987
+ )
988
+ value = self._visit_value(scope_value, before_value, after_value)
989
+ change_type = change_type.for_child(value.change_type)
990
+
991
+ export: Optional[ChangeSetEntity] = None
992
+ scope_export, (before_export, after_export) = self._safe_access_in(
993
+ scope, ExportKey, before_output, after_output
994
+ )
995
+ if before_export or after_export:
996
+ export = self._visit_value(scope_export, before_export, after_export)
997
+ change_type = change_type.for_child(export.change_type)
998
+
999
+ # TODO: condition references should be resolved for the condition's change_type?
1000
+ condition_reference: Optional[TerminalValue] = None
1001
+ scope_condition, (before_condition, after_condition) = self._safe_access_in(
1002
+ scope, ConditionKey, before_output, after_output
1003
+ )
1004
+ if before_condition or after_condition:
1005
+ condition_reference = self._visit_terminal_value(
1006
+ scope_condition, before_condition, after_condition
1007
+ )
1008
+ change_type = change_type.for_child(condition_reference.change_type)
1009
+
1010
+ return NodeOutput(
1011
+ scope=scope,
1012
+ change_type=change_type,
1013
+ name=name,
1014
+ value=value,
1015
+ export=export,
1016
+ conditional_reference=condition_reference,
1017
+ )
1018
+
1019
+ def _visit_outputs(
1020
+ self, scope: Scope, before_outputs: Maybe[dict], after_outputs: Maybe[dict]
1021
+ ) -> NodeOutputs:
1022
+ change_type = ChangeType.UNCHANGED
1023
+ outputs: list[NodeOutput] = list()
1024
+ output_names: list[str] = self._safe_keys_of(before_outputs, after_outputs)
1025
+ for output_name in output_names:
1026
+ scope_output, (before_output, after_output) = self._safe_access_in(
1027
+ scope, output_name, before_outputs, after_outputs
1028
+ )
1029
+ output = self._visit_output(
1030
+ scope=scope_output,
1031
+ name=output_name,
1032
+ before_output=before_output,
1033
+ after_output=after_output,
1034
+ )
1035
+ outputs.append(output)
1036
+ change_type = change_type.for_child(output.change_type)
1037
+ return NodeOutputs(scope=scope, change_type=change_type, outputs=outputs)
1038
+
845
1039
  def _model(self, before_template: Maybe[dict], after_template: Maybe[dict]) -> NodeTemplate:
846
1040
  root_scope = Scope()
847
1041
  # TODO: visit other child types
1042
+
1043
+ mappings_scope, (before_mappings, after_mappings) = self._safe_access_in(
1044
+ root_scope, MappingsKey, before_template, after_template
1045
+ )
1046
+ mappings = self._visit_mappings(
1047
+ scope=mappings_scope, before_mappings=before_mappings, after_mappings=after_mappings
1048
+ )
1049
+
848
1050
  parameters_scope, (before_parameters, after_parameters) = self._safe_access_in(
849
1051
  root_scope, ParametersKey, before_template, after_template
850
1052
  )
@@ -872,13 +1074,22 @@ class ChangeSetModel:
872
1074
  after_resources=after_resources,
873
1075
  )
874
1076
 
1077
+ outputs_scope, (before_outputs, after_outputs) = self._safe_access_in(
1078
+ root_scope, OutputsKey, before_template, after_template
1079
+ )
1080
+ outputs = self._visit_outputs(
1081
+ scope=outputs_scope, before_outputs=before_outputs, after_outputs=after_outputs
1082
+ )
1083
+
875
1084
  # TODO: compute the change_type of the template properly.
876
1085
  return NodeTemplate(
877
1086
  scope=root_scope,
878
1087
  change_type=resources.change_type,
1088
+ mappings=mappings,
879
1089
  parameters=parameters,
880
1090
  conditions=conditions,
881
1091
  resources=resources,
1092
+ outputs=outputs,
882
1093
  )
883
1094
 
884
1095
  def _retrieve_condition_if_exists(self, condition_name: str) -> Optional[NodeCondition]:
@@ -919,6 +1130,23 @@ class ChangeSetModel:
919
1130
  return node_parameter
920
1131
  return None
921
1132
 
1133
+ def _retrieve_mapping(self, mapping_name) -> NodeMapping:
1134
+ # TODO: add caching mechanism, and raise appropriate error if missing.
1135
+ scope_mappings, (before_mappings, after_mappings) = self._safe_access_in(
1136
+ Scope(), MappingsKey, self._before_template, self._after_template
1137
+ )
1138
+ before_mappings = before_mappings or dict()
1139
+ after_mappings = after_mappings or dict()
1140
+ if mapping_name in before_mappings or mapping_name in after_mappings:
1141
+ scope_mapping, (before_mapping, after_mapping) = self._safe_access_in(
1142
+ scope_mappings, mapping_name, before_mappings, after_mappings
1143
+ )
1144
+ node_mapping = self._visit_mapping(
1145
+ scope_mapping, mapping_name, before_mapping, after_mapping
1146
+ )
1147
+ return node_mapping
1148
+ raise RuntimeError()
1149
+
922
1150
  def _retrieve_or_visit_resource(self, resource_name: str) -> NodeResource:
923
1151
  resources_scope, (before_resources, after_resources) = self._safe_access_in(
924
1152
  Scope(),
@@ -974,13 +1202,30 @@ class ChangeSetModel:
974
1202
  break
975
1203
  return parent_change_type
976
1204
 
1205
+ @staticmethod
1206
+ def _name_if_intrinsic_function(value: Maybe[Any]) -> Optional[str]:
1207
+ if isinstance(value, dict):
1208
+ keys = ChangeSetModel._safe_keys_of(value)
1209
+ if len(keys) == 1:
1210
+ key_name = keys[0]
1211
+ if ChangeSetModel._is_intrinsic_function_name(key_name):
1212
+ return key_name
1213
+ return None
1214
+
1215
+ @staticmethod
1216
+ def _type_name_of(value: Maybe[Any]) -> str:
1217
+ maybe_intrinsic_function_name = ChangeSetModel._name_if_intrinsic_function(value)
1218
+ if maybe_intrinsic_function_name is not None:
1219
+ return maybe_intrinsic_function_name
1220
+ return type(value).__name__
1221
+
977
1222
  @staticmethod
978
1223
  def _is_terminal(value: Any) -> bool:
979
1224
  return type(value) in {int, float, bool, str, None, NothingType}
980
1225
 
981
1226
  @staticmethod
982
1227
  def _is_object(value: Any) -> bool:
983
- return isinstance(value, dict)
1228
+ return isinstance(value, dict) and ChangeSetModel._name_if_intrinsic_function(value) is None
984
1229
 
985
1230
  @staticmethod
986
1231
  def _is_array(value: Any) -> bool: