localstack-core 4.3.1.dev35__py3-none-any.whl → 4.3.1.dev37__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 (25) hide show
  1. localstack/config.py +0 -6
  2. localstack/deprecations.py +14 -0
  3. localstack/services/cloudformation/engine/entities.py +9 -4
  4. localstack/services/cloudformation/engine/v2/change_set_model.py +32 -67
  5. localstack/services/cloudformation/engine/v2/change_set_model_describer.py +119 -487
  6. localstack/services/cloudformation/engine/v2/change_set_model_executor.py +107 -70
  7. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +574 -0
  8. localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +6 -6
  9. localstack/services/cloudformation/v2/provider.py +39 -5
  10. localstack/services/cloudformation/v2/utils.py +5 -0
  11. localstack/services/sns/resource_providers/aws_sns_topic.py +1 -0
  12. localstack/testing/pytest/cloudformation/__init__.py +0 -0
  13. localstack/testing/pytest/cloudformation/fixtures.py +169 -0
  14. localstack/version.py +2 -2
  15. {localstack_core-4.3.1.dev35.dist-info → localstack_core-4.3.1.dev37.dist-info}/METADATA +1 -1
  16. {localstack_core-4.3.1.dev35.dist-info → localstack_core-4.3.1.dev37.dist-info}/RECORD +24 -20
  17. localstack_core-4.3.1.dev37.dist-info/plux.json +1 -0
  18. localstack_core-4.3.1.dev35.dist-info/plux.json +0 -1
  19. {localstack_core-4.3.1.dev35.data → localstack_core-4.3.1.dev37.data}/scripts/localstack +0 -0
  20. {localstack_core-4.3.1.dev35.data → localstack_core-4.3.1.dev37.data}/scripts/localstack-supervisor +0 -0
  21. {localstack_core-4.3.1.dev35.data → localstack_core-4.3.1.dev37.data}/scripts/localstack.bat +0 -0
  22. {localstack_core-4.3.1.dev35.dist-info → localstack_core-4.3.1.dev37.dist-info}/WHEEL +0 -0
  23. {localstack_core-4.3.1.dev35.dist-info → localstack_core-4.3.1.dev37.dist-info}/entry_points.txt +0 -0
  24. {localstack_core-4.3.1.dev35.dist-info → localstack_core-4.3.1.dev37.dist-info}/licenses/LICENSE.txt +0 -0
  25. {localstack_core-4.3.1.dev35.dist-info → localstack_core-4.3.1.dev37.dist-info}/top_level.txt +0 -0
@@ -1,230 +1,50 @@
1
1
  from __future__ import annotations
2
2
 
3
- import abc
4
- from typing import Any, Final, Optional
3
+ import json
4
+ from typing import Final, Optional
5
5
 
6
6
  import localstack.aws.api.cloudformation as cfn_api
7
7
  from localstack.services.cloudformation.engine.v2.change_set_model import (
8
- ChangeSetEntity,
9
- ChangeType,
10
- ConditionKey,
11
- ExportKey,
12
- NodeArray,
13
- NodeCondition,
14
- NodeDivergence,
15
8
  NodeIntrinsicFunction,
16
- NodeMapping,
17
- NodeObject,
18
- NodeOutput,
19
- NodeOutputs,
20
- NodeParameter,
21
- NodeProperties,
22
- NodeProperty,
23
9
  NodeResource,
24
10
  NodeTemplate,
25
- NothingType,
26
11
  PropertiesKey,
27
- Scope,
28
- TerminalValue,
29
- TerminalValueCreated,
30
- TerminalValueModified,
31
- TerminalValueRemoved,
32
- TerminalValueUnchanged,
33
- ValueKey,
34
12
  )
35
- from localstack.services.cloudformation.engine.v2.change_set_model_visitor import (
36
- ChangeSetModelVisitor,
13
+ from localstack.services.cloudformation.engine.v2.change_set_model_preproc import (
14
+ ChangeSetModelPreproc,
15
+ PreprocEntityDelta,
16
+ PreprocProperties,
17
+ PreprocResource,
37
18
  )
38
19
 
39
20
  CHANGESET_KNOWN_AFTER_APPLY: Final[str] = "{{changeSet:KNOWN_AFTER_APPLY}}"
40
21
 
41
22
 
42
- class DescribeUnit(abc.ABC):
43
- before_context: Optional[Any] = None
44
- after_context: Optional[Any] = None
45
-
46
- def __init__(self, before_context: Optional[Any] = None, after_context: Optional[Any] = None):
47
- self.before_context = before_context
48
- self.after_context = after_context
49
-
50
-
51
- class ChangeSetModelDescriber(ChangeSetModelVisitor):
52
- _node_template: Final[NodeTemplate]
23
+ class ChangeSetModelDescriber(ChangeSetModelPreproc):
24
+ _include_property_values: Final[bool]
53
25
  _changes: Final[cfn_api.Changes]
54
- _describe_unit_cache: dict[Scope, DescribeUnit]
55
- _include_property_values: Final[cfn_api.IncludePropertyValues | None]
56
26
 
57
- def __init__(
58
- self,
59
- node_template: NodeTemplate,
60
- include_property_values: cfn_api.IncludePropertyValues | None = None,
61
- ):
62
- self._node_template = node_template
63
- self._changes = list()
64
- self._describe_unit_cache = dict()
27
+ def __init__(self, node_template: NodeTemplate, include_property_values: bool):
28
+ super().__init__(node_template=node_template)
65
29
  self._include_property_values = include_property_values
30
+ self._changes = list()
66
31
 
67
32
  def get_changes(self) -> cfn_api.Changes:
68
- self.visit(self._node_template)
33
+ self._changes.clear()
34
+ self.process()
69
35
  return self._changes
70
36
 
71
- @staticmethod
72
- def _get_node_resource_for(resource_name: str, node_template: NodeTemplate) -> NodeResource:
73
- # TODO: this could be improved with hashmap lookups if the Node contained bindings and not lists.
74
- for node_resource in node_template.resources.resources:
75
- if node_resource.name == resource_name:
76
- return node_resource
77
- # TODO
78
- raise RuntimeError()
79
-
80
- @staticmethod
81
- def _get_node_property_for(property_name: str, node_resource: NodeResource) -> NodeProperty:
82
- # TODO: this could be improved with hashmap lookups if the Node contained bindings and not lists.
83
- for node_property in node_resource.properties.properties:
84
- if node_property.name == property_name:
85
- return node_property
86
- # TODO
87
- raise RuntimeError()
88
-
89
- def _get_node_mapping(self, map_name: str) -> NodeMapping:
90
- mappings: list[NodeMapping] = self._node_template.mappings.mappings
91
- # TODO: another scenarios suggesting property lookups might be preferable.
92
- for mapping in mappings:
93
- if mapping.name == map_name:
94
- return mapping
95
- # TODO
96
- raise RuntimeError()
97
-
98
- def _get_node_parameter_if_exists(self, parameter_name: str) -> Optional[NodeParameter]:
99
- parameters: list[NodeParameter] = self._node_template.parameters.parameters
100
- # TODO: another scenarios suggesting property lookups might be preferable.
101
- for parameter in parameters:
102
- if parameter.name == parameter_name:
103
- return parameter
104
- return None
105
-
106
- def _get_node_condition_if_exists(self, condition_name: str) -> Optional[NodeCondition]:
107
- conditions: list[NodeCondition] = self._node_template.conditions.conditions
108
- # TODO: another scenarios suggesting property lookups might be preferable.
109
- for condition in conditions:
110
- if condition.name == condition_name:
111
- return condition
112
- return None
113
-
114
- def _resolve_reference(self, logica_id: str) -> DescribeUnit:
115
- node_condition = self._get_node_condition_if_exists(condition_name=logica_id)
116
- if isinstance(node_condition, NodeCondition):
117
- condition_unit = self.visit(node_condition)
118
- return condition_unit
119
-
120
- node_parameter = self._get_node_parameter_if_exists(parameter_name=logica_id)
121
- if isinstance(node_parameter, NodeParameter):
122
- parameter_unit = self.visit(node_parameter)
123
- return parameter_unit
124
-
125
- # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments.
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)
133
-
134
- def _resolve_mapping(self, map_name: str, top_level_key: str, second_level_key) -> DescribeUnit:
135
- # TODO: add support for nested intrinsic functions, and KNOWN AFTER APPLY logical ids.
136
- node_mapping: NodeMapping = self._get_node_mapping(map_name=map_name)
137
- top_level_value = node_mapping.bindings.bindings.get(top_level_key)
138
- if not isinstance(top_level_value, NodeObject):
139
- raise RuntimeError()
140
- second_level_value = top_level_value.bindings.get(second_level_key)
141
- mapping_value_unit = self.visit(second_level_value)
142
- return mapping_value_unit
143
-
144
- def _resolve_reference_binding(
145
- self, before_logical_id: str, after_logical_id: str
146
- ) -> DescribeUnit:
147
- before_unit = self._resolve_reference(logica_id=before_logical_id)
148
- after_unit = self._resolve_reference(logica_id=after_logical_id)
149
- return DescribeUnit(
150
- before_context=before_unit.before_context, after_context=after_unit.after_context
151
- )
152
-
153
- def visit(self, change_set_entity: ChangeSetEntity) -> DescribeUnit:
154
- describe_unit = self._describe_unit_cache.get(change_set_entity.scope)
155
- if describe_unit is not None:
156
- return describe_unit
157
- describe_unit = super().visit(change_set_entity=change_set_entity)
158
- self._describe_unit_cache[change_set_entity.scope] = describe_unit
159
- return describe_unit
160
-
161
- def visit_terminal_value_modified(
162
- self, terminal_value_modified: TerminalValueModified
163
- ) -> DescribeUnit:
164
- return DescribeUnit(
165
- before_context=terminal_value_modified.value,
166
- after_context=terminal_value_modified.modified_value,
167
- )
168
-
169
- def visit_terminal_value_created(
170
- self, terminal_value_created: TerminalValueCreated
171
- ) -> DescribeUnit:
172
- return DescribeUnit(after_context=terminal_value_created.value)
173
-
174
- def visit_terminal_value_removed(
175
- self, terminal_value_removed: TerminalValueRemoved
176
- ) -> DescribeUnit:
177
- return DescribeUnit(before_context=terminal_value_removed.value)
178
-
179
- def visit_terminal_value_unchanged(
180
- self, terminal_value_unchanged: TerminalValueUnchanged
181
- ) -> DescribeUnit:
182
- return DescribeUnit(
183
- before_context=terminal_value_unchanged.value,
184
- after_context=terminal_value_unchanged.value,
185
- )
186
-
187
- def visit_node_divergence(self, node_divergence: NodeDivergence) -> DescribeUnit:
188
- before_unit = self.visit(node_divergence.value)
189
- after_unit = self.visit(node_divergence.divergence)
190
- return DescribeUnit(
191
- before_context=before_unit.before_context, after_context=after_unit.after_context
192
- )
193
-
194
- def visit_node_object(self, node_object: NodeObject) -> DescribeUnit:
195
- # TODO: improve check syntax
196
- if len(node_object.bindings) == 1:
197
- binding_values = list(node_object.bindings.values())
198
- unique_value = binding_values[0]
199
- if isinstance(unique_value, NodeIntrinsicFunction):
200
- return self.visit(unique_value)
201
-
202
- before_context = dict()
203
- after_context = dict()
204
- for name, change_set_entity in node_object.bindings.items():
205
- describe_unit: DescribeUnit = self.visit(change_set_entity=change_set_entity)
206
- match change_set_entity.change_type:
207
- case ChangeType.MODIFIED:
208
- before_context[name] = describe_unit.before_context
209
- after_context[name] = describe_unit.after_context
210
- case ChangeType.CREATED:
211
- after_context[name] = describe_unit.after_context
212
- case ChangeType.REMOVED:
213
- before_context[name] = describe_unit.before_context
214
- case ChangeType.UNCHANGED:
215
- before_context[name] = describe_unit.before_context
216
- after_context[name] = describe_unit.before_context
217
- return DescribeUnit(before_context=before_context, after_context=after_context)
218
-
219
37
  def visit_node_intrinsic_function_fn_get_att(
220
38
  self, node_intrinsic_function: NodeIntrinsicFunction
221
- ) -> DescribeUnit:
222
- arguments_unit = self.visit(node_intrinsic_function.arguments)
223
- # TODO: validate the return value according to the spec.
224
- before_argument_list = arguments_unit.before_context
225
- after_argument_list = arguments_unit.after_context
39
+ ) -> PreprocEntityDelta:
40
+ # TODO: If we can properly compute the before and after value, why should we
41
+ # artificially limit the precision of our output to match AWS's?
226
42
 
227
- before_context = None
43
+ arguments_delta = self.visit(node_intrinsic_function.arguments)
44
+ before_argument_list = arguments_delta.before
45
+ after_argument_list = arguments_delta.after
46
+
47
+ before = None
228
48
  if before_argument_list:
229
49
  before_logical_name_of_resource = before_argument_list[0]
230
50
  before_attribute_name = before_argument_list[1]
@@ -234,300 +54,112 @@ class ChangeSetModelDescriber(ChangeSetModelVisitor):
234
54
  before_node_property = self._get_node_property_for(
235
55
  property_name=before_attribute_name, node_resource=before_node_resource
236
56
  )
237
- before_property_unit = self.visit(before_node_property)
238
- before_context = before_property_unit.before_context
57
+ before_property_delta = self.visit(before_node_property)
58
+ before = before_property_delta.before
239
59
 
240
- after_context = None
60
+ after = None
241
61
  if after_argument_list:
242
- after_context = CHANGESET_KNOWN_AFTER_APPLY
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
255
-
256
- return DescribeUnit(before_context=before_context, after_context=after_context)
257
-
258
- def visit_node_intrinsic_function_fn_equals(
259
- self, node_intrinsic_function: NodeIntrinsicFunction
260
- ) -> DescribeUnit:
261
- # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments.
262
- arguments_unit = self.visit(node_intrinsic_function.arguments)
263
- before_values = arguments_unit.before_context
264
- after_values = arguments_unit.after_context
265
- before_context = None
266
- if before_values:
267
- before_context = before_values[0] == before_values[1]
268
- after_context = None
269
- if after_values:
270
- after_context = after_values[0] == after_values[1]
271
- match node_intrinsic_function.change_type:
272
- case ChangeType.MODIFIED:
273
- return DescribeUnit(before_context=before_context, after_context=after_context)
274
- case ChangeType.CREATED:
275
- return DescribeUnit(after_context=after_context)
276
- case ChangeType.REMOVED:
277
- return DescribeUnit(before_context=before_context)
278
- # Unchanged
279
- return DescribeUnit(before_context=before_context, after_context=after_context)
280
-
281
- def visit_node_intrinsic_function_fn_if(
282
- self, node_intrinsic_function: NodeIntrinsicFunction
283
- ) -> DescribeUnit:
284
- # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments.
285
- arguments_unit = self.visit(node_intrinsic_function.arguments)
286
-
287
- def _compute_unit_for_if_statement(args: list[Any]) -> DescribeUnit:
288
- condition_name = args[0]
289
- boolean_expression_unit = self._resolve_reference(logica_id=condition_name)
290
- return DescribeUnit(
291
- before_context=args[1] if boolean_expression_unit.before_context else args[2],
292
- after_context=args[1] if boolean_expression_unit.after_context else args[2],
62
+ after_logical_name_of_resource = after_argument_list[0]
63
+ after_attribute_name = after_argument_list[1]
64
+ after_node_resource = self._get_node_resource_for(
65
+ resource_name=after_logical_name_of_resource, node_template=self._node_template
293
66
  )
294
-
295
- # TODO: add support for this being created or removed.
296
- before_outcome_unit = _compute_unit_for_if_statement(arguments_unit.before_context)
297
- before_context = before_outcome_unit.before_context
298
- after_outcome_unit = _compute_unit_for_if_statement(arguments_unit.after_context)
299
- after_context = after_outcome_unit.after_context
300
- return DescribeUnit(before_context=before_context, after_context=after_context)
301
-
302
- def visit_node_intrinsic_function_fn_not(
303
- self, node_intrinsic_function: NodeIntrinsicFunction
304
- ) -> DescribeUnit:
305
- # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments.
306
- # TODO: add type checking/validation for result unit?
307
- arguments_unit = self.visit(node_intrinsic_function.arguments)
308
- before_condition = arguments_unit.before_context
309
- after_condition = arguments_unit.after_context
310
- if before_condition:
311
- before_condition_outcome = before_condition[0]
312
- before_context = not before_condition_outcome
313
- else:
314
- before_context = None
315
-
316
- if after_condition:
317
- after_condition_outcome = after_condition[0]
318
- after_context = not after_condition_outcome
319
- else:
320
- after_context = None
321
- # Implicit change type computation.
322
- return DescribeUnit(before_context=before_context, after_context=after_context)
323
-
324
- def visit_node_intrinsic_function_fn_find_in_map(
325
- self, node_intrinsic_function: NodeIntrinsicFunction
326
- ) -> DescribeUnit:
327
- # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments.
328
- # TODO: add type checking/validation for result unit?
329
- arguments_unit = self.visit(node_intrinsic_function.arguments)
330
- before_arguments = arguments_unit.before_context
331
- after_arguments = arguments_unit.after_context
332
- if before_arguments:
333
- before_value_unit = self._resolve_mapping(*before_arguments)
334
- before_context = before_value_unit.before_context
335
- else:
336
- before_context = None
337
- if after_arguments:
338
- after_value_unit = self._resolve_mapping(*after_arguments)
339
- after_context = after_value_unit.after_context
340
- else:
341
- after_context = None
342
- return DescribeUnit(before_context=before_context, after_context=after_context)
343
-
344
- def visit_node_mapping(self, node_mapping: NodeMapping) -> DescribeUnit:
345
- bindings_unit = self.visit(node_mapping.bindings)
346
- return bindings_unit
347
-
348
- def visit_node_parameter(self, node_parameter: NodeParameter) -> DescribeUnit:
349
- # TODO: add support for default value sampling
350
- dynamic_value = node_parameter.dynamic_value
351
- describe_unit = self.visit(dynamic_value)
352
- return describe_unit
353
-
354
- def visit_node_condition(self, node_condition: NodeCondition) -> DescribeUnit:
355
- describe_unit = self.visit(node_condition.body)
356
- return describe_unit
357
-
358
- def visit_node_intrinsic_function_ref(
359
- self, node_intrinsic_function: NodeIntrinsicFunction
360
- ) -> DescribeUnit:
361
- arguments_unit = self.visit(node_intrinsic_function.arguments)
362
-
363
- # TODO: add tests with created and deleted parameters and verify this logic holds.
364
- before_logical_id = arguments_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
369
-
370
- after_logical_id = arguments_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
375
-
376
- return DescribeUnit(before_context=before_context, after_context=after_context)
377
-
378
- def visit_node_array(self, node_array: NodeArray) -> DescribeUnit:
379
- before_context = list()
380
- after_context = list()
381
- for change_set_entity in node_array.array:
382
- describe_unit: DescribeUnit = self.visit(change_set_entity=change_set_entity)
383
- match change_set_entity.change_type:
384
- case ChangeType.MODIFIED:
385
- before_context.append(describe_unit.before_context)
386
- after_context.append(describe_unit.after_context)
387
- case ChangeType.CREATED:
388
- after_context.append(describe_unit.after_context)
389
- case ChangeType.REMOVED:
390
- before_context.append(describe_unit.before_context)
391
- case ChangeType.UNCHANGED:
392
- before_context.append(describe_unit.before_context)
393
- after_context.append(describe_unit.before_context)
394
- return DescribeUnit(before_context=before_context, after_context=after_context)
395
-
396
- def visit_node_properties(self, node_properties: NodeProperties) -> DescribeUnit:
397
- before_context: dict[str, Any] = dict()
398
- after_context: dict[str, Any] = dict()
399
- for node_property in node_properties.properties:
400
- describe_unit = self.visit(node_property.value)
401
- property_name = node_property.name
402
- match node_property.change_type:
403
- case ChangeType.MODIFIED:
404
- before_context[property_name] = describe_unit.before_context
405
- after_context[property_name] = describe_unit.after_context
406
- case ChangeType.CREATED:
407
- after_context[property_name] = describe_unit.after_context
408
- case ChangeType.REMOVED:
409
- before_context[property_name] = describe_unit.before_context
410
- case ChangeType.UNCHANGED:
411
- before_context[property_name] = describe_unit.before_context
412
- after_context[property_name] = describe_unit.before_context
413
- # TODO: this object can probably be well-typed instead of a free dict(?)
414
- before_context = {PropertiesKey: before_context}
415
- after_context = {PropertiesKey: after_context}
416
- return DescribeUnit(before_context=before_context, after_context=after_context)
417
-
418
- def _resolve_resource_condition_reference(self, reference: TerminalValue) -> DescribeUnit:
419
- reference_unit = self.visit(reference)
420
- before_reference = reference_unit.before_context
421
- after_reference = reference_unit.after_context
422
- condition_unit = self._resolve_reference_binding(
423
- before_logical_id=before_reference, after_logical_id=after_reference
424
- )
425
- before_context = (
426
- condition_unit.before_context if not isinstance(before_reference, NothingType) else True
427
- )
428
- after_context = (
429
- condition_unit.after_context if not isinstance(after_reference, NothingType) else True
430
- )
431
- return DescribeUnit(before_context=before_context, after_context=after_context)
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
67
+ after_node_property = self._get_node_property_for(
68
+ property_name=after_attribute_name, node_resource=after_node_resource
443
69
  )
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
70
+ after_property_delta = self.visit(after_node_property)
71
+ if after_property_delta.before == after_property_delta.after:
72
+ after = after_property_delta.after
73
+ else:
74
+ after = CHANGESET_KNOWN_AFTER_APPLY
450
75
 
451
- export_unit = None
452
- if node_output.export is not None:
453
- export_unit = self.visit(node_output.export)
76
+ return PreprocEntityDelta(before=before, after=after)
454
77
 
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
-
486
- def visit_node_resource(self, node_resource: NodeResource) -> DescribeUnit:
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
78
+ def _register_resource_change(
79
+ self,
80
+ logical_id: str,
81
+ type_: str,
82
+ before_properties: Optional[PreprocProperties],
83
+ after_properties: Optional[PreprocProperties],
84
+ ) -> None:
85
+ # unchanged: nothing to do.
86
+ if before_properties == after_properties:
87
+ return
88
+
89
+ action = cfn_api.ChangeAction.Modify
90
+ if before_properties is None:
91
+ action = cfn_api.ChangeAction.Add
92
+ elif after_properties is None:
93
+ action = cfn_api.ChangeAction.Remove
498
94
 
499
95
  resource_change = cfn_api.ResourceChange()
500
- resource_change["LogicalResourceId"] = node_resource.name
501
-
502
- # TODO: investigate effects on type changes
503
- type_describe_unit = self.visit(node_resource.type_)
504
- resource_change["ResourceType"] = (
505
- type_describe_unit.before_context or type_describe_unit.after_context
96
+ resource_change["Action"] = action
97
+ resource_change["LogicalResourceId"] = logical_id
98
+ resource_change["ResourceType"] = type_
99
+ if self._include_property_values and before_properties is not None:
100
+ before_context_properties = {PropertiesKey: before_properties.properties}
101
+ before_context_properties_json_str = json.dumps(before_context_properties)
102
+ resource_change["BeforeContext"] = before_context_properties_json_str
103
+ if self._include_property_values and after_properties is not None:
104
+ after_context_properties = {PropertiesKey: after_properties.properties}
105
+ after_context_properties_json_str = json.dumps(after_context_properties)
106
+ resource_change["AfterContext"] = after_context_properties_json_str
107
+ self._changes.append(
108
+ cfn_api.Change(Type=cfn_api.ChangeType.Resource, ResourceChange=resource_change)
506
109
  )
507
110
 
508
- properties_describe_unit = self.visit(node_resource.properties)
509
-
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)
111
+ def _describe_resource_change(
112
+ self, name: str, before: Optional[PreprocResource], after: Optional[PreprocResource]
113
+ ) -> None:
114
+ if before is not None and after is not None:
115
+ # Case: change on same type.
116
+ if before.resource_type == after.resource_type:
117
+ # Register a Modified if changed.
118
+ self._register_resource_change(
119
+ logical_id=name,
120
+ type_=before.resource_type,
121
+ before_properties=before.properties,
122
+ after_properties=after.properties,
123
+ )
124
+ # Case: type migration.
125
+ # TODO: Add test to assert that on type change the resources are replaced.
126
+ else:
127
+ # Register a Removed for the previous type.
128
+ self._register_resource_change(
129
+ logical_id=name,
130
+ type_=before.resource_type,
131
+ before_properties=before.properties,
132
+ after_properties=None,
133
+ )
134
+ # Register a Create for the next type.
135
+ self._register_resource_change(
136
+ logical_id=name,
137
+ type_=after.resource_type,
138
+ before_properties=None,
139
+ after_properties=after.properties,
140
+ )
141
+ elif before is not None:
142
+ # Case: removal
143
+ self._register_resource_change(
144
+ logical_id=name,
145
+ type_=before.resource_type,
146
+ before_properties=before.properties,
147
+ after_properties=None,
148
+ )
149
+ elif after is not None:
150
+ # Case: addition
151
+ self._register_resource_change(
152
+ logical_id=name,
153
+ type_=after.resource_type,
154
+ before_properties=None,
155
+ after_properties=after.properties,
524
156
  )
525
157
 
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)
158
+ def visit_node_resource(
159
+ self, node_resource: NodeResource
160
+ ) -> PreprocEntityDelta[PreprocResource, PreprocResource]:
161
+ delta = super().visit_node_resource(node_resource=node_resource)
162
+ self._describe_resource_change(
163
+ name=node_resource.name, before=delta.before, after=delta.after
164
+ )
165
+ return delta