localstack-core 4.3.1.dev6__py3-none-any.whl → 4.3.1.dev28__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 (44) hide show
  1. localstack/aws/api/ec2/__init__.py +597 -0
  2. localstack/aws/api/events/__init__.py +18 -12
  3. localstack/aws/api/route53/__init__.py +2 -0
  4. localstack/aws/api/s3control/__init__.py +89 -0
  5. localstack/aws/api/transcribe/__init__.py +1 -0
  6. localstack/services/cloudformation/engine/entities.py +18 -1
  7. localstack/services/cloudformation/engine/template_deployer.py +0 -9
  8. localstack/services/cloudformation/engine/v2/change_set_model.py +164 -35
  9. localstack/services/cloudformation/engine/v2/change_set_model_describer.py +143 -69
  10. localstack/services/cloudformation/engine/v2/change_set_model_executor.py +170 -0
  11. localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +8 -0
  12. localstack/services/cloudformation/v2/provider.py +72 -6
  13. localstack/services/ec2/patches.py +31 -3
  14. localstack/services/events/provider.py +6 -1
  15. localstack/services/kms/models.py +1 -1
  16. localstack/services/lambda_/event_source_mapping/pollers/dynamodb_poller.py +2 -0
  17. localstack/services/lambda_/event_source_mapping/pollers/kinesis_poller.py +2 -0
  18. localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py +4 -2
  19. localstack/services/lambda_/invocation/assignment.py +4 -2
  20. localstack/services/lambda_/invocation/execution_environment.py +16 -4
  21. localstack/services/lambda_/invocation/logs.py +28 -4
  22. localstack/services/lambda_/provider.py +18 -3
  23. localstack/services/lambda_/runtimes.py +15 -2
  24. localstack/services/s3/presigned_url.py +15 -11
  25. localstack/services/secretsmanager/provider.py +13 -4
  26. localstack/services/sqs/models.py +22 -3
  27. localstack/services/sqs/utils.py +16 -7
  28. localstack/services/ssm/resource_providers/aws_ssm_parameter.py +1 -5
  29. localstack/services/stepfunctions/asl/utils/json_path.py +9 -0
  30. localstack/testing/snapshots/transformer_utility.py +13 -0
  31. localstack/utils/aws/client_types.py +8 -0
  32. localstack/utils/docker_utils.py +2 -2
  33. localstack/version.py +2 -2
  34. {localstack_core-4.3.1.dev6.dist-info → localstack_core-4.3.1.dev28.dist-info}/METADATA +5 -5
  35. {localstack_core-4.3.1.dev6.dist-info → localstack_core-4.3.1.dev28.dist-info}/RECORD +43 -42
  36. localstack_core-4.3.1.dev28.dist-info/plux.json +1 -0
  37. localstack_core-4.3.1.dev6.dist-info/plux.json +0 -1
  38. {localstack_core-4.3.1.dev6.data → localstack_core-4.3.1.dev28.data}/scripts/localstack +0 -0
  39. {localstack_core-4.3.1.dev6.data → localstack_core-4.3.1.dev28.data}/scripts/localstack-supervisor +0 -0
  40. {localstack_core-4.3.1.dev6.data → localstack_core-4.3.1.dev28.data}/scripts/localstack.bat +0 -0
  41. {localstack_core-4.3.1.dev6.dist-info → localstack_core-4.3.1.dev28.dist-info}/WHEEL +0 -0
  42. {localstack_core-4.3.1.dev6.dist-info → localstack_core-4.3.1.dev28.dist-info}/entry_points.txt +0 -0
  43. {localstack_core-4.3.1.dev6.dist-info → localstack_core-4.3.1.dev28.dist-info}/licenses/LICENSE.txt +0 -0
  44. {localstack_core-4.3.1.dev6.dist-info → localstack_core-4.3.1.dev28.dist-info}/top_level.txt +0 -0
@@ -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
@@ -117,6 +127,7 @@ class NodeTemplate(ChangeSetNode):
117
127
  parameters: Final[NodeParameters]
118
128
  conditions: Final[NodeConditions]
119
129
  resources: Final[NodeResources]
130
+ outputs: Final[NodeOutputs]
120
131
 
121
132
  def __init__(
122
133
  self,
@@ -126,12 +137,14 @@ class NodeTemplate(ChangeSetNode):
126
137
  parameters: NodeParameters,
127
138
  conditions: NodeConditions,
128
139
  resources: NodeResources,
140
+ outputs: NodeOutputs,
129
141
  ):
130
142
  super().__init__(scope=scope, change_type=change_type)
131
143
  self.mappings = mappings
132
144
  self.parameters = parameters
133
145
  self.conditions = conditions
134
146
  self.resources = resources
147
+ self.outputs = outputs
135
148
 
136
149
 
137
150
  class NodeDivergence(ChangeSetNode):
@@ -189,6 +202,36 @@ class NodeMappings(ChangeSetNode):
189
202
  self.mappings = mappings
190
203
 
191
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
+
192
235
  class NodeCondition(ChangeSetNode):
193
236
  name: Final[str]
194
237
  body: Final[ChangeSetEntity]
@@ -218,7 +261,7 @@ class NodeResources(ChangeSetNode):
218
261
  class NodeResource(ChangeSetNode):
219
262
  name: Final[str]
220
263
  type_: Final[ChangeSetTerminal]
221
- condition_reference: Final[TerminalValue]
264
+ condition_reference: Final[Optional[TerminalValue]]
222
265
  properties: Final[NodeProperties]
223
266
 
224
267
  def __init__(
@@ -325,6 +368,9 @@ MappingsKey: Final[str] = "Mappings"
325
368
  ResourcesKey: Final[str] = "Resources"
326
369
  PropertiesKey: Final[str] = "Properties"
327
370
  ParametersKey: Final[str] = "Parameters"
371
+ ValueKey: Final[str] = "Value"
372
+ ExportKey: Final[str] = "Export"
373
+ OutputsKey: Final[str] = "Outputs"
328
374
  # TODO: expand intrinsic functions set.
329
375
  RefKey: Final[str] = "Ref"
330
376
  FnIf: Final[str] = "Fn::If"
@@ -567,17 +613,9 @@ class ChangeSetModel:
567
613
  binding_scope, (before_value, after_value) = self._safe_access_in(
568
614
  scope, binding_name, before_object, after_object
569
615
  )
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
- )
616
+ value = self._visit_value(
617
+ scope=binding_scope, before_value=before_value, after_value=after_value
618
+ )
581
619
  bindings[binding_name] = value
582
620
  change_type = change_type.for_child(value.change_type)
583
621
  node_object = NodeObject(scope=scope, change_type=change_type, bindings=bindings)
@@ -601,8 +639,11 @@ class ChangeSetModel:
601
639
  value = self._visited_scopes.get(scope)
602
640
  if isinstance(value, ChangeSetEntity):
603
641
  return value
642
+
643
+ before_type_name = self._type_name_of(before_value)
644
+ after_type_name = self._type_name_of(after_value)
604
645
  unset = object()
605
- if type(before_value) is type(after_value):
646
+ if before_type_name == after_type_name:
606
647
  dominant_value = before_value
607
648
  elif self._is_created(before=before_value, after=after_value):
608
649
  dominant_value = after_value
@@ -611,6 +652,7 @@ class ChangeSetModel:
611
652
  else:
612
653
  dominant_value = unset
613
654
  if dominant_value is not unset:
655
+ dominant_type_name = self._type_name_of(dominant_value)
614
656
  if self._is_terminal(value=dominant_value):
615
657
  value = self._visit_terminal_value(
616
658
  scope=scope, before_value=before_value, after_value=after_value
@@ -623,6 +665,16 @@ class ChangeSetModel:
623
665
  value = self._visit_array(
624
666
  scope=scope, before_array=before_value, after_array=after_value
625
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
+ )
626
678
  else:
627
679
  raise RuntimeError(f"Unsupported type {type(dominant_value)}")
628
680
  # Case: type divergence.
@@ -644,24 +696,24 @@ class ChangeSetModel:
644
696
  if isinstance(node_property, NodeProperty):
645
697
  return node_property
646
698
 
699
+ value = self._visit_value(
700
+ scope=scope, before_value=before_property, after_value=after_property
701
+ )
647
702
  if self._is_created(before=before_property, after=after_property):
648
703
  node_property = NodeProperty(
649
704
  scope=scope,
650
705
  change_type=ChangeType.CREATED,
651
706
  name=property_name,
652
- value=TerminalValueCreated(scope=scope, value=after_property),
707
+ value=value,
653
708
  )
654
709
  elif self._is_removed(before=before_property, after=after_property):
655
710
  node_property = NodeProperty(
656
711
  scope=scope,
657
712
  change_type=ChangeType.REMOVED,
658
713
  name=property_name,
659
- value=TerminalValueRemoved(scope=scope, value=before_property),
714
+ value=value,
660
715
  )
661
716
  else:
662
- value = self._visit_value(
663
- scope=scope, before_value=before_property, after_value=after_property
664
- )
665
717
  node_property = NodeProperty(
666
718
  scope=scope, change_type=value.change_type, name=property_name, value=value
667
719
  )
@@ -715,14 +767,17 @@ class ChangeSetModel:
715
767
  change_type = ChangeType.UNCHANGED
716
768
 
717
769
  # TODO: investigate behaviour with type changes, for now this is filler code.
718
- _, type_str = self._safe_access_in(scope, TypeKey, before_resource)
770
+ _, type_str = self._safe_access_in(scope, TypeKey, after_resource)
719
771
 
772
+ condition_reference = None
720
773
  scope_condition, (before_condition, after_condition) = self._safe_access_in(
721
774
  scope, ConditionKey, before_resource, after_resource
722
775
  )
723
- condition_reference = self._visit_terminal_value(
724
- scope_condition, before_condition, after_condition
725
- )
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
+ )
726
781
 
727
782
  scope_properties, (before_properties, after_properties) = self._safe_access_in(
728
783
  scope, PropertiesKey, before_resource, after_resource
@@ -887,18 +942,9 @@ class ChangeSetModel:
887
942
  node_condition = self._visited_scopes.get(scope)
888
943
  if isinstance(node_condition, NodeCondition):
889
944
  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
-
945
+ body = self._visit_value(
946
+ scope=scope, before_value=before_condition, after_value=after_condition
947
+ )
902
948
  node_condition = NodeCondition(
903
949
  scope=scope, change_type=body.change_type, name=condition_name, body=body
904
950
  )
@@ -932,6 +978,64 @@ class ChangeSetModel:
932
978
  self._visited_scopes[scope] = node_conditions
933
979
  return node_conditions
934
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
+
935
1039
  def _model(self, before_template: Maybe[dict], after_template: Maybe[dict]) -> NodeTemplate:
936
1040
  root_scope = Scope()
937
1041
  # TODO: visit other child types
@@ -970,6 +1074,13 @@ class ChangeSetModel:
970
1074
  after_resources=after_resources,
971
1075
  )
972
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
+
973
1084
  # TODO: compute the change_type of the template properly.
974
1085
  return NodeTemplate(
975
1086
  scope=root_scope,
@@ -978,6 +1089,7 @@ class ChangeSetModel:
978
1089
  parameters=parameters,
979
1090
  conditions=conditions,
980
1091
  resources=resources,
1092
+ outputs=outputs,
981
1093
  )
982
1094
 
983
1095
  def _retrieve_condition_if_exists(self, condition_name: str) -> Optional[NodeCondition]:
@@ -1090,13 +1202,30 @@ class ChangeSetModel:
1090
1202
  break
1091
1203
  return parent_change_type
1092
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
+
1093
1222
  @staticmethod
1094
1223
  def _is_terminal(value: Any) -> bool:
1095
1224
  return type(value) in {int, float, bool, str, None, NothingType}
1096
1225
 
1097
1226
  @staticmethod
1098
1227
  def _is_object(value: Any) -> bool:
1099
- return isinstance(value, dict)
1228
+ return isinstance(value, dict) and ChangeSetModel._name_if_intrinsic_function(value) is None
1100
1229
 
1101
1230
  @staticmethod
1102
1231
  def _is_array(value: Any) -> bool:
@@ -7,12 +7,16 @@ import localstack.aws.api.cloudformation as cfn_api
7
7
  from localstack.services.cloudformation.engine.v2.change_set_model import (
8
8
  ChangeSetEntity,
9
9
  ChangeType,
10
+ ConditionKey,
11
+ ExportKey,
10
12
  NodeArray,
11
13
  NodeCondition,
12
14
  NodeDivergence,
13
15
  NodeIntrinsicFunction,
14
16
  NodeMapping,
15
17
  NodeObject,
18
+ NodeOutput,
19
+ NodeOutputs,
16
20
  NodeParameter,
17
21
  NodeProperties,
18
22
  NodeProperty,
@@ -26,6 +30,7 @@ from localstack.services.cloudformation.engine.v2.change_set_model import (
26
30
  TerminalValueModified,
27
31
  TerminalValueRemoved,
28
32
  TerminalValueUnchanged,
33
+ ValueKey,
29
34
  )
30
35
  from localstack.services.cloudformation.engine.v2.change_set_model_visitor import (
31
36
  ChangeSetModelVisitor,
@@ -47,14 +52,20 @@ class ChangeSetModelDescriber(ChangeSetModelVisitor):
47
52
  _node_template: Final[NodeTemplate]
48
53
  _changes: Final[cfn_api.Changes]
49
54
  _describe_unit_cache: dict[Scope, DescribeUnit]
55
+ _include_property_values: Final[cfn_api.IncludePropertyValues | None]
50
56
 
51
- def __init__(self, node_template: NodeTemplate):
57
+ def __init__(
58
+ self,
59
+ node_template: NodeTemplate,
60
+ include_property_values: cfn_api.IncludePropertyValues | None = None,
61
+ ):
52
62
  self._node_template = node_template
53
63
  self._changes = list()
54
64
  self._describe_unit_cache = dict()
55
- self.visit(self._node_template)
65
+ self._include_property_values = include_property_values
56
66
 
57
67
  def get_changes(self) -> cfn_api.Changes:
68
+ self.visit(self._node_template)
58
69
  return self._changes
59
70
 
60
71
  @staticmethod
@@ -112,12 +123,13 @@ class ChangeSetModelDescriber(ChangeSetModelVisitor):
112
123
  return parameter_unit
113
124
 
114
125
  # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments.
115
- # node_resource = self._get_node_resource_for(
116
- # resource_name=logica_id, node_template=self._node_template
117
- # )
118
- limitation_str = "Cannot yet compute Ref values for Resources"
119
- resource_unit = DescribeUnit(before_context=limitation_str, after_context=limitation_str)
120
- return resource_unit
126
+ node_resource = self._get_node_resource_for(
127
+ resource_name=logica_id, node_template=self._node_template
128
+ )
129
+ resource_unit = self.visit(node_resource)
130
+ before_context = resource_unit.before_context
131
+ after_context = resource_unit.after_context
132
+ return DescribeUnit(before_context=before_context, after_context=after_context)
121
133
 
122
134
  def _resolve_mapping(self, map_name: str, top_level_key: str, second_level_key) -> DescribeUnit:
123
135
  # TODO: add support for nested intrinsic functions, and KNOWN AFTER APPLY logical ids.
@@ -210,29 +222,37 @@ class ChangeSetModelDescriber(ChangeSetModelVisitor):
210
222
  arguments_unit = self.visit(node_intrinsic_function.arguments)
211
223
  # TODO: validate the return value according to the spec.
212
224
  before_argument_list = arguments_unit.before_context
213
- before_logical_name_of_resource = before_argument_list[0]
214
- before_attribute_name = before_argument_list[1]
215
- before_node_resource = self._get_node_resource_for(
216
- resource_name=before_logical_name_of_resource, node_template=self._node_template
217
- )
218
- node_property: TerminalValue = self._get_node_property_for(
219
- property_name=before_attribute_name, node_resource=before_node_resource
220
- )
225
+ after_argument_list = arguments_unit.after_context
226
+
227
+ before_context = None
228
+ if before_argument_list:
229
+ before_logical_name_of_resource = before_argument_list[0]
230
+ before_attribute_name = before_argument_list[1]
231
+ before_node_resource = self._get_node_resource_for(
232
+ resource_name=before_logical_name_of_resource, node_template=self._node_template
233
+ )
234
+ before_node_property = self._get_node_property_for(
235
+ property_name=before_attribute_name, node_resource=before_node_resource
236
+ )
237
+ before_property_unit = self.visit(before_node_property)
238
+ before_context = before_property_unit.before_context
221
239
 
222
- before_context = node_property.value.value
223
- if node_property.change_type != ChangeType.UNCHANGED:
240
+ after_context = None
241
+ if after_argument_list:
224
242
  after_context = CHANGESET_KNOWN_AFTER_APPLY
225
- else:
226
- after_context = node_property.value.value
243
+ # TODO: the following is the logic to resolve the attribute in the `after` template
244
+ # this should be moved to the new base class and then be masked in this describer.
245
+ # after_logical_name_of_resource = after_argument_list[0]
246
+ # after_attribute_name = after_argument_list[1]
247
+ # after_node_resource = self._get_node_resource_for(
248
+ # resource_name=after_logical_name_of_resource, node_template=self._node_template
249
+ # )
250
+ # after_node_property = self._get_node_property_for(
251
+ # property_name=after_attribute_name, node_resource=after_node_resource
252
+ # )
253
+ # after_property_unit = self.visit(after_node_property)
254
+ # after_context = after_property_unit.after_context
227
255
 
228
- match node_intrinsic_function.change_type:
229
- case ChangeType.MODIFIED:
230
- return DescribeUnit(before_context=before_context, after_context=after_context)
231
- case ChangeType.CREATED:
232
- return DescribeUnit(after_context=after_context)
233
- case ChangeType.REMOVED:
234
- return DescribeUnit(before_context=before_context)
235
- # Unchanged
236
256
  return DescribeUnit(before_context=before_context, after_context=after_context)
237
257
 
238
258
  def visit_node_intrinsic_function_fn_equals(
@@ -342,12 +362,16 @@ class ChangeSetModelDescriber(ChangeSetModelVisitor):
342
362
 
343
363
  # TODO: add tests with created and deleted parameters and verify this logic holds.
344
364
  before_logical_id = arguments_unit.before_context
345
- before_unit = self._resolve_reference(logica_id=before_logical_id)
346
- before_context = before_unit.before_context
365
+ before_context = None
366
+ if before_logical_id is not None:
367
+ before_unit = self._resolve_reference(logica_id=before_logical_id)
368
+ before_context = before_unit.before_context
347
369
 
348
370
  after_logical_id = arguments_unit.after_context
349
- after_unit = self._resolve_reference(logica_id=after_logical_id)
350
- after_context = after_unit.after_context
371
+ after_context = None
372
+ if after_logical_id is not None:
373
+ after_unit = self._resolve_reference(logica_id=after_logical_id)
374
+ after_context = after_unit.after_context
351
375
 
352
376
  return DescribeUnit(before_context=before_context, after_context=after_context)
353
377
 
@@ -406,21 +430,71 @@ class ChangeSetModelDescriber(ChangeSetModelVisitor):
406
430
  )
407
431
  return DescribeUnit(before_context=before_context, after_context=after_context)
408
432
 
433
+ def visit_node_output(self, node_output: NodeOutput) -> DescribeUnit:
434
+ # This logic is not required for Describe operations,
435
+ # and should be ported a new base for this class type.
436
+ change_type = node_output.change_type
437
+ value_unit = self.visit(node_output.value)
438
+
439
+ condition_unit = None
440
+ if node_output.condition_reference is not None:
441
+ condition_unit = self._resolve_resource_condition_reference(
442
+ node_output.condition_reference
443
+ )
444
+ condition_before = condition_unit.before_context
445
+ condition_after = condition_unit.after_context
446
+ if not condition_before and condition_after:
447
+ change_type = ChangeType.CREATED
448
+ elif condition_before and not condition_after:
449
+ change_type = ChangeType.REMOVED
450
+
451
+ export_unit = None
452
+ if node_output.export is not None:
453
+ export_unit = self.visit(node_output.export)
454
+
455
+ before_context = None
456
+ after_context = None
457
+ if change_type != ChangeType.REMOVED:
458
+ after_context = {"Name": node_output.name, ValueKey: value_unit.after_context}
459
+ if export_unit:
460
+ after_context[ExportKey] = export_unit.after_context
461
+ if condition_unit:
462
+ after_context[ConditionKey] = condition_unit.after_context
463
+ if change_type != ChangeType.CREATED:
464
+ before_context = {"Name": node_output.name, ValueKey: value_unit.before_context}
465
+ if export_unit:
466
+ before_context[ExportKey] = export_unit.before_context
467
+ if condition_unit:
468
+ before_context[ConditionKey] = condition_unit.before_context
469
+ return DescribeUnit(before_context=before_context, after_context=after_context)
470
+
471
+ def visit_node_outputs(self, node_outputs: NodeOutputs) -> DescribeUnit:
472
+ # This logic is not required for Describe operations,
473
+ # and should be ported a new base for this class type.
474
+ before_context = list()
475
+ after_context = list()
476
+ for node_output in node_outputs.outputs:
477
+ output_unit = self.visit(node_output)
478
+ output_before = output_unit.before_context
479
+ output_after = output_unit.after_context
480
+ if output_before:
481
+ before_context.append(output_before)
482
+ if output_after:
483
+ after_context.append(output_after)
484
+ return DescribeUnit(before_context=before_context, after_context=after_context)
485
+
409
486
  def visit_node_resource(self, node_resource: NodeResource) -> DescribeUnit:
410
- condition_unit = self._resolve_resource_condition_reference(
411
- node_resource.condition_reference
412
- )
413
- condition_before = condition_unit.before_context
414
- condition_after = condition_unit.after_context
415
- if not condition_before and condition_after:
416
- change_type = ChangeType.CREATED
417
- elif condition_before and not condition_after:
418
- change_type = ChangeType.REMOVED
419
- else:
420
- change_type = node_resource.change_type
421
- if change_type == ChangeType.UNCHANGED:
422
- # TODO
423
- return None
487
+ change_type = node_resource.change_type
488
+ if node_resource.condition_reference is not None:
489
+ condition_unit = self._resolve_resource_condition_reference(
490
+ node_resource.condition_reference
491
+ )
492
+ condition_before = condition_unit.before_context
493
+ condition_after = condition_unit.after_context
494
+ if not condition_before and condition_after:
495
+ change_type = ChangeType.CREATED
496
+ elif condition_before and not condition_after:
497
+ change_type = ChangeType.REMOVED
424
498
 
425
499
  resource_change = cfn_api.ResourceChange()
426
500
  resource_change["LogicalResourceId"] = node_resource.name
@@ -432,28 +506,28 @@ class ChangeSetModelDescriber(ChangeSetModelVisitor):
432
506
  )
433
507
 
434
508
  properties_describe_unit = self.visit(node_resource.properties)
435
- match change_type:
436
- case ChangeType.MODIFIED:
437
- resource_change["Action"] = cfn_api.ChangeAction.Modify
438
- resource_change["BeforeContext"] = properties_describe_unit.before_context
439
- resource_change["AfterContext"] = properties_describe_unit.after_context
440
- case ChangeType.CREATED:
441
- resource_change["Action"] = cfn_api.ChangeAction.Add
442
- resource_change["AfterContext"] = properties_describe_unit.after_context
443
- case ChangeType.REMOVED:
444
- resource_change["Action"] = cfn_api.ChangeAction.Remove
445
- resource_change["BeforeContext"] = properties_describe_unit.before_context
446
-
447
- self._changes.append(
448
- cfn_api.Change(Type=cfn_api.ChangeType.Resource, ResourceChange=resource_change)
449
- )
450
509
 
451
- # TODO
452
- return None
510
+ if change_type != ChangeType.UNCHANGED:
511
+ match change_type:
512
+ case ChangeType.MODIFIED:
513
+ resource_change["Action"] = cfn_api.ChangeAction.Modify
514
+ resource_change["BeforeContext"] = properties_describe_unit.before_context
515
+ resource_change["AfterContext"] = properties_describe_unit.after_context
516
+ case ChangeType.CREATED:
517
+ resource_change["Action"] = cfn_api.ChangeAction.Add
518
+ resource_change["AfterContext"] = properties_describe_unit.after_context
519
+ case ChangeType.REMOVED:
520
+ resource_change["Action"] = cfn_api.ChangeAction.Remove
521
+ resource_change["BeforeContext"] = properties_describe_unit.before_context
522
+ self._changes.append(
523
+ cfn_api.Change(Type=cfn_api.ChangeType.Resource, ResourceChange=resource_change)
524
+ )
453
525
 
454
- # def visit_node_resources(self, node_resources: NodeResources) -> DescribeUnit:
455
- # for node_resource in node_resources.resources:
456
- # if node_resource.change_type != ChangeType.UNCHANGED:
457
- # self.visit_node_resource(node_resource=node_resource)
458
- # # TODO
459
- # return None
526
+ before_context = None
527
+ after_context = None
528
+ # TODO: reconsider what is the describe unit return value for a resource type.
529
+ if change_type != ChangeType.CREATED:
530
+ before_context = node_resource.name
531
+ if change_type != ChangeType.REMOVED:
532
+ after_context = node_resource.name
533
+ return DescribeUnit(before_context=before_context, after_context=after_context)