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
@@ -0,0 +1,574 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Final, Generic, Optional, TypeVar
4
+
5
+ from localstack.services.cloudformation.engine.v2.change_set_model import (
6
+ ChangeSetEntity,
7
+ ChangeType,
8
+ NodeArray,
9
+ NodeCondition,
10
+ NodeDivergence,
11
+ NodeIntrinsicFunction,
12
+ NodeMapping,
13
+ NodeObject,
14
+ NodeOutput,
15
+ NodeOutputs,
16
+ NodeParameter,
17
+ NodeProperties,
18
+ NodeProperty,
19
+ NodeResource,
20
+ NodeTemplate,
21
+ NothingType,
22
+ Scope,
23
+ TerminalValue,
24
+ TerminalValueCreated,
25
+ TerminalValueModified,
26
+ TerminalValueRemoved,
27
+ TerminalValueUnchanged,
28
+ )
29
+ from localstack.services.cloudformation.engine.v2.change_set_model_visitor import (
30
+ ChangeSetModelVisitor,
31
+ )
32
+
33
+ TBefore = TypeVar("TBefore")
34
+ TAfter = TypeVar("TAfter")
35
+
36
+
37
+ class PreprocEntityDelta(Generic[TBefore, TAfter]):
38
+ before: Optional[TBefore]
39
+ after: Optional[TAfter]
40
+
41
+ def __init__(self, before: Optional[TBefore] = None, after: Optional[TAfter] = None):
42
+ self.before = before
43
+ self.after = after
44
+
45
+ def __eq__(self, other):
46
+ if not isinstance(other, PreprocEntityDelta):
47
+ return False
48
+ return self.before == other.before and self.after == other.after
49
+
50
+
51
+ class PreprocProperties:
52
+ properties: dict[str, Any]
53
+
54
+ def __init__(self, properties: dict[str, Any]):
55
+ self.properties = properties
56
+
57
+ def __eq__(self, other):
58
+ if not isinstance(other, PreprocProperties):
59
+ return False
60
+ return self.properties == other.properties
61
+
62
+
63
+ class PreprocResource:
64
+ name: str
65
+ condition: Optional[bool]
66
+ resource_type: str
67
+ properties: PreprocProperties
68
+
69
+ def __init__(
70
+ self,
71
+ name: str,
72
+ condition: Optional[bool],
73
+ resource_type: str,
74
+ properties: PreprocProperties,
75
+ ):
76
+ self.condition = condition
77
+ self.name = name
78
+ self.resource_type = resource_type
79
+ self.properties = properties
80
+
81
+ def __eq__(self, other):
82
+ if not isinstance(other, PreprocResource):
83
+ return False
84
+ return all(
85
+ [
86
+ self.name == other.name,
87
+ self.condition == other.condition,
88
+ self.resource_type == other.resource_type,
89
+ self.properties == other.properties,
90
+ ]
91
+ )
92
+
93
+
94
+ class PreprocOutput:
95
+ name: str
96
+ value: Any
97
+ export: Optional[Any]
98
+ condition: Optional[bool]
99
+
100
+ def __init__(self, name: str, value: Any, export: Optional[Any], condition: Optional[bool]):
101
+ self.name = name
102
+ self.value = value
103
+ self.export = export
104
+ self.condition = condition
105
+
106
+ def __eq__(self, other):
107
+ if not isinstance(other, PreprocOutput):
108
+ return False
109
+ return all(
110
+ [
111
+ self.name == other.name,
112
+ self.value == other.value,
113
+ self.export == other.export,
114
+ self.condition == other.condition,
115
+ ]
116
+ )
117
+
118
+
119
+ class ChangeSetModelPreproc(ChangeSetModelVisitor):
120
+ _node_template: Final[NodeTemplate]
121
+ _processed: dict[Scope, Any]
122
+
123
+ def __init__(self, node_template: NodeTemplate):
124
+ self._node_template = node_template
125
+ self._processed = dict()
126
+
127
+ def process(self) -> None:
128
+ self._processed.clear()
129
+ self.visit(self._node_template)
130
+
131
+ def _get_node_resource_for(
132
+ self, resource_name: str, node_template: NodeTemplate
133
+ ) -> NodeResource:
134
+ # TODO: this could be improved with hashmap lookups if the Node contained bindings and not lists.
135
+ for node_resource in node_template.resources.resources:
136
+ if node_resource.name == resource_name:
137
+ return node_resource
138
+ # TODO
139
+ raise RuntimeError()
140
+
141
+ def _get_node_property_for(
142
+ self, property_name: str, node_resource: NodeResource
143
+ ) -> NodeProperty:
144
+ # TODO: this could be improved with hashmap lookups if the Node contained bindings and not lists.
145
+ for node_property in node_resource.properties.properties:
146
+ if node_property.name == property_name:
147
+ return node_property
148
+ # TODO
149
+ raise RuntimeError()
150
+
151
+ def _get_node_mapping(self, map_name: str) -> NodeMapping:
152
+ mappings: list[NodeMapping] = self._node_template.mappings.mappings
153
+ # TODO: another scenarios suggesting property lookups might be preferable.
154
+ for mapping in mappings:
155
+ if mapping.name == map_name:
156
+ return mapping
157
+ # TODO
158
+ raise RuntimeError()
159
+
160
+ def _get_node_parameter_if_exists(self, parameter_name: str) -> Optional[NodeParameter]:
161
+ parameters: list[NodeParameter] = self._node_template.parameters.parameters
162
+ # TODO: another scenarios suggesting property lookups might be preferable.
163
+ for parameter in parameters:
164
+ if parameter.name == parameter_name:
165
+ return parameter
166
+ return None
167
+
168
+ def _get_node_condition_if_exists(self, condition_name: str) -> Optional[NodeCondition]:
169
+ conditions: list[NodeCondition] = self._node_template.conditions.conditions
170
+ # TODO: another scenarios suggesting property lookups might be preferable.
171
+ for condition in conditions:
172
+ if condition.name == condition_name:
173
+ return condition
174
+ return None
175
+
176
+ def _resolve_reference(self, logica_id: str) -> PreprocEntityDelta:
177
+ node_condition = self._get_node_condition_if_exists(condition_name=logica_id)
178
+ if isinstance(node_condition, NodeCondition):
179
+ condition_delta = self.visit(node_condition)
180
+ return condition_delta
181
+
182
+ node_parameter = self._get_node_parameter_if_exists(parameter_name=logica_id)
183
+ if isinstance(node_parameter, NodeParameter):
184
+ parameter_delta = self.visit(node_parameter)
185
+ return parameter_delta
186
+
187
+ # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments.
188
+ node_resource = self._get_node_resource_for(
189
+ resource_name=logica_id, node_template=self._node_template
190
+ )
191
+ resource_delta = self.visit(node_resource)
192
+ before = resource_delta.before
193
+ after = resource_delta.after
194
+ return PreprocEntityDelta(before=before, after=after)
195
+
196
+ def _resolve_mapping(
197
+ self, map_name: str, top_level_key: str, second_level_key
198
+ ) -> PreprocEntityDelta:
199
+ # TODO: add support for nested intrinsic functions, and KNOWN AFTER APPLY logical ids.
200
+ node_mapping: NodeMapping = self._get_node_mapping(map_name=map_name)
201
+ top_level_value = node_mapping.bindings.bindings.get(top_level_key)
202
+ if not isinstance(top_level_value, NodeObject):
203
+ raise RuntimeError()
204
+ second_level_value = top_level_value.bindings.get(second_level_key)
205
+ mapping_value_delta = self.visit(second_level_value)
206
+ return mapping_value_delta
207
+
208
+ def _resolve_reference_binding(
209
+ self, before_logical_id: Optional[str], after_logical_id: Optional[str]
210
+ ) -> PreprocEntityDelta:
211
+ before = None
212
+ if before_logical_id is not None:
213
+ before_delta = self._resolve_reference(logica_id=before_logical_id)
214
+ before = before_delta.before
215
+ after = None
216
+ if after_logical_id is not None:
217
+ after_delta = self._resolve_reference(logica_id=after_logical_id)
218
+ after = after_delta.after
219
+ return PreprocEntityDelta(before=before, after=after)
220
+
221
+ def visit(self, change_set_entity: ChangeSetEntity) -> PreprocEntityDelta:
222
+ delta = self._processed.get(change_set_entity.scope)
223
+ if delta is not None:
224
+ return delta
225
+ delta = super().visit(change_set_entity=change_set_entity)
226
+ self._processed[change_set_entity.scope] = delta
227
+ return delta
228
+
229
+ def visit_terminal_value_modified(
230
+ self, terminal_value_modified: TerminalValueModified
231
+ ) -> PreprocEntityDelta:
232
+ return PreprocEntityDelta(
233
+ before=terminal_value_modified.value,
234
+ after=terminal_value_modified.modified_value,
235
+ )
236
+
237
+ def visit_terminal_value_created(
238
+ self, terminal_value_created: TerminalValueCreated
239
+ ) -> PreprocEntityDelta:
240
+ return PreprocEntityDelta(after=terminal_value_created.value)
241
+
242
+ def visit_terminal_value_removed(
243
+ self, terminal_value_removed: TerminalValueRemoved
244
+ ) -> PreprocEntityDelta:
245
+ return PreprocEntityDelta(before=terminal_value_removed.value)
246
+
247
+ def visit_terminal_value_unchanged(
248
+ self, terminal_value_unchanged: TerminalValueUnchanged
249
+ ) -> PreprocEntityDelta:
250
+ return PreprocEntityDelta(
251
+ before=terminal_value_unchanged.value,
252
+ after=terminal_value_unchanged.value,
253
+ )
254
+
255
+ def visit_node_divergence(self, node_divergence: NodeDivergence) -> PreprocEntityDelta:
256
+ before_delta = self.visit(node_divergence.value)
257
+ after_delta = self.visit(node_divergence.divergence)
258
+ return PreprocEntityDelta(before=before_delta.before, after=after_delta.after)
259
+
260
+ def visit_node_object(self, node_object: NodeObject) -> PreprocEntityDelta:
261
+ before = dict()
262
+ after = dict()
263
+ for name, change_set_entity in node_object.bindings.items():
264
+ delta: PreprocEntityDelta = self.visit(change_set_entity=change_set_entity)
265
+ match change_set_entity.change_type:
266
+ case ChangeType.MODIFIED:
267
+ before[name] = delta.before
268
+ after[name] = delta.after
269
+ case ChangeType.CREATED:
270
+ after[name] = delta.after
271
+ case ChangeType.REMOVED:
272
+ before[name] = delta.before
273
+ case ChangeType.UNCHANGED:
274
+ before[name] = delta.before
275
+ after[name] = delta.before
276
+ return PreprocEntityDelta(before=before, after=after)
277
+
278
+ def visit_node_intrinsic_function_fn_get_att(
279
+ self, node_intrinsic_function: NodeIntrinsicFunction
280
+ ) -> PreprocEntityDelta:
281
+ arguments_delta = self.visit(node_intrinsic_function.arguments)
282
+ # TODO: validate the return value according to the spec.
283
+ before_argument_list = arguments_delta.before
284
+ after_argument_list = arguments_delta.after
285
+
286
+ before = None
287
+ if before_argument_list:
288
+ before_logical_name_of_resource = before_argument_list[0]
289
+ before_attribute_name = before_argument_list[1]
290
+ before_node_resource = self._get_node_resource_for(
291
+ resource_name=before_logical_name_of_resource, node_template=self._node_template
292
+ )
293
+ before_node_property = self._get_node_property_for(
294
+ property_name=before_attribute_name, node_resource=before_node_resource
295
+ )
296
+ before_property_delta = self.visit(before_node_property)
297
+ before = before_property_delta.before
298
+
299
+ after = None
300
+ if after_argument_list:
301
+ # TODO: when are values only accessible at runtime?
302
+ after_logical_name_of_resource = after_argument_list[0]
303
+ after_attribute_name = after_argument_list[1]
304
+ after_node_resource = self._get_node_resource_for(
305
+ resource_name=after_logical_name_of_resource, node_template=self._node_template
306
+ )
307
+ after_node_property = self._get_node_property_for(
308
+ property_name=after_attribute_name, node_resource=after_node_resource
309
+ )
310
+ after_property_delta = self.visit(after_node_property)
311
+ after = after_property_delta.after
312
+
313
+ return PreprocEntityDelta(before=before, after=after)
314
+
315
+ def visit_node_intrinsic_function_fn_equals(
316
+ self, node_intrinsic_function: NodeIntrinsicFunction
317
+ ) -> PreprocEntityDelta:
318
+ # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments.
319
+ arguments_delta = self.visit(node_intrinsic_function.arguments)
320
+ before_values = arguments_delta.before
321
+ after_values = arguments_delta.after
322
+ before = None
323
+ if before_values:
324
+ before = before_values[0] == before_values[1]
325
+ after = None
326
+ if after_values:
327
+ after = after_values[0] == after_values[1]
328
+ return PreprocEntityDelta(before=before, after=after)
329
+
330
+ def visit_node_intrinsic_function_fn_if(
331
+ self, node_intrinsic_function: NodeIntrinsicFunction
332
+ ) -> PreprocEntityDelta:
333
+ # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments.
334
+ arguments_delta = self.visit(node_intrinsic_function.arguments)
335
+
336
+ def _compute_delta_for_if_statement(args: list[Any]) -> PreprocEntityDelta:
337
+ condition_name = args[0]
338
+ boolean_expression_delta = self._resolve_reference(logica_id=condition_name)
339
+ return PreprocEntityDelta(
340
+ before=args[1] if boolean_expression_delta.before else args[2],
341
+ after=args[1] if boolean_expression_delta.after else args[2],
342
+ )
343
+
344
+ # TODO: add support for this being created or removed.
345
+ before_outcome_delta = _compute_delta_for_if_statement(arguments_delta.before)
346
+ before = before_outcome_delta.before
347
+ after_outcome_delta = _compute_delta_for_if_statement(arguments_delta.after)
348
+ after = after_outcome_delta.after
349
+ return PreprocEntityDelta(before=before, after=after)
350
+
351
+ def visit_node_intrinsic_function_fn_not(
352
+ self, node_intrinsic_function: NodeIntrinsicFunction
353
+ ) -> PreprocEntityDelta:
354
+ # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments.
355
+ # TODO: add type checking/validation for result unit?
356
+ arguments_delta = self.visit(node_intrinsic_function.arguments)
357
+ before_condition = arguments_delta.before
358
+ after_condition = arguments_delta.after
359
+ if before_condition:
360
+ before_condition_outcome = before_condition[0]
361
+ before = not before_condition_outcome
362
+ else:
363
+ before = None
364
+
365
+ if after_condition:
366
+ after_condition_outcome = after_condition[0]
367
+ after = not after_condition_outcome
368
+ else:
369
+ after = None
370
+ # Implicit change type computation.
371
+ return PreprocEntityDelta(before=before, after=after)
372
+
373
+ def visit_node_intrinsic_function_fn_find_in_map(
374
+ self, node_intrinsic_function: NodeIntrinsicFunction
375
+ ) -> PreprocEntityDelta:
376
+ # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments.
377
+ # TODO: add type checking/validation for result unit?
378
+ arguments_delta = self.visit(node_intrinsic_function.arguments)
379
+ before_arguments = arguments_delta.before
380
+ after_arguments = arguments_delta.after
381
+ if before_arguments:
382
+ before_value_delta = self._resolve_mapping(*before_arguments)
383
+ before = before_value_delta.before
384
+ else:
385
+ before = None
386
+ if after_arguments:
387
+ after_value_delta = self._resolve_mapping(*after_arguments)
388
+ after = after_value_delta.after
389
+ else:
390
+ after = None
391
+ return PreprocEntityDelta(before=before, after=after)
392
+
393
+ def visit_node_mapping(self, node_mapping: NodeMapping) -> PreprocEntityDelta:
394
+ bindings_delta = self.visit(node_mapping.bindings)
395
+ return bindings_delta
396
+
397
+ def visit_node_parameter(self, node_parameter: NodeParameter) -> PreprocEntityDelta:
398
+ # TODO: add support for default value sampling
399
+ dynamic_value = node_parameter.dynamic_value
400
+ delta = self.visit(dynamic_value)
401
+ return delta
402
+
403
+ def visit_node_condition(self, node_condition: NodeCondition) -> PreprocEntityDelta:
404
+ delta = self.visit(node_condition.body)
405
+ return delta
406
+
407
+ def _reduce_intrinsic_function_ref_value(self, preproc_value: Any) -> Any:
408
+ if isinstance(preproc_value, PreprocResource):
409
+ value = preproc_value.name
410
+ else:
411
+ value = preproc_value
412
+ return value
413
+
414
+ def visit_node_intrinsic_function_ref(
415
+ self, node_intrinsic_function: NodeIntrinsicFunction
416
+ ) -> PreprocEntityDelta:
417
+ arguments_delta = self.visit(node_intrinsic_function.arguments)
418
+
419
+ # TODO: add tests with created and deleted parameters and verify this logic holds.
420
+ before_logical_id = arguments_delta.before
421
+ before = None
422
+ if before_logical_id is not None:
423
+ before_delta = self._resolve_reference(logica_id=before_logical_id)
424
+ before_value = before_delta.before
425
+ before = self._reduce_intrinsic_function_ref_value(before_value)
426
+
427
+ after_logical_id = arguments_delta.after
428
+ after = None
429
+ if after_logical_id is not None:
430
+ after_delta = self._resolve_reference(logica_id=after_logical_id)
431
+ after_value = after_delta.after
432
+ after = self._reduce_intrinsic_function_ref_value(after_value)
433
+
434
+ return PreprocEntityDelta(before=before, after=after)
435
+
436
+ def visit_node_array(self, node_array: NodeArray) -> PreprocEntityDelta:
437
+ before = list()
438
+ after = list()
439
+ for change_set_entity in node_array.array:
440
+ delta: PreprocEntityDelta = self.visit(change_set_entity=change_set_entity)
441
+ if delta.before:
442
+ before.append(delta.before)
443
+ if delta.after:
444
+ after.append(delta.after)
445
+ return PreprocEntityDelta(before=before, after=after)
446
+
447
+ def visit_node_property(self, node_property: NodeProperty) -> PreprocEntityDelta:
448
+ return self.visit(node_property.value)
449
+
450
+ def visit_node_properties(
451
+ self, node_properties: NodeProperties
452
+ ) -> PreprocEntityDelta[PreprocProperties, PreprocProperties]:
453
+ before_bindings: dict[str, Any] = dict()
454
+ after_bindings: dict[str, Any] = dict()
455
+ for node_property in node_properties.properties:
456
+ delta = self.visit(node_property)
457
+ property_name = node_property.name
458
+ if node_property.change_type != ChangeType.CREATED:
459
+ before_bindings[property_name] = delta.before
460
+ if node_property.change_type != ChangeType.REMOVED:
461
+ after_bindings[property_name] = delta.after
462
+ before = None
463
+ if before_bindings:
464
+ before = PreprocProperties(properties=before_bindings)
465
+ after = None
466
+ if after_bindings:
467
+ after = PreprocProperties(properties=after_bindings)
468
+ return PreprocEntityDelta(before=before, after=after)
469
+
470
+ def _resolve_resource_condition_reference(self, reference: TerminalValue) -> PreprocEntityDelta:
471
+ reference_delta = self.visit(reference)
472
+ before_reference = reference_delta.before
473
+ after_reference = reference_delta.after
474
+ condition_delta = self._resolve_reference_binding(
475
+ before_logical_id=before_reference, after_logical_id=after_reference
476
+ )
477
+ before = condition_delta.before if not isinstance(before_reference, NothingType) else True
478
+ after = condition_delta.after if not isinstance(after_reference, NothingType) else True
479
+ return PreprocEntityDelta(before=before, after=after)
480
+
481
+ def visit_node_resource(
482
+ self, node_resource: NodeResource
483
+ ) -> PreprocEntityDelta[PreprocResource, PreprocResource]:
484
+ change_type = node_resource.change_type
485
+ condition_before = None
486
+ condition_after = None
487
+ if node_resource.condition_reference is not None:
488
+ condition_delta = self._resolve_resource_condition_reference(
489
+ node_resource.condition_reference
490
+ )
491
+ condition_before = condition_delta.before
492
+ condition_after = condition_delta.after
493
+ if not condition_before and condition_after:
494
+ change_type = ChangeType.CREATED
495
+ elif condition_before and not condition_after:
496
+ change_type = ChangeType.REMOVED
497
+
498
+ type_delta = self.visit(node_resource.type_)
499
+ properties_delta: PreprocEntityDelta[PreprocProperties, PreprocProperties] = self.visit(
500
+ node_resource.properties
501
+ )
502
+
503
+ before = None
504
+ after = None
505
+ if change_type != ChangeType.CREATED:
506
+ before = PreprocResource(
507
+ name=node_resource.name,
508
+ condition=condition_before,
509
+ resource_type=type_delta.before,
510
+ properties=properties_delta.before,
511
+ )
512
+ if change_type != ChangeType.REMOVED:
513
+ after = PreprocResource(
514
+ name=node_resource.name,
515
+ condition=condition_after,
516
+ resource_type=type_delta.after,
517
+ properties=properties_delta.after,
518
+ )
519
+ return PreprocEntityDelta(before=before, after=after)
520
+
521
+ def visit_node_output(
522
+ self, node_output: NodeOutput
523
+ ) -> PreprocEntityDelta[PreprocOutput, PreprocOutput]:
524
+ change_type = node_output.change_type
525
+ value_delta = self.visit(node_output.value)
526
+
527
+ condition_delta = None
528
+ if node_output.condition_reference is not None:
529
+ condition_delta = self._resolve_resource_condition_reference(
530
+ node_output.condition_reference
531
+ )
532
+ condition_before = condition_delta.before
533
+ condition_after = condition_delta.after
534
+ if not condition_before and condition_after:
535
+ change_type = ChangeType.CREATED
536
+ elif condition_before and not condition_after:
537
+ change_type = ChangeType.REMOVED
538
+
539
+ export_delta = None
540
+ if node_output.export is not None:
541
+ export_delta = self.visit(node_output.export)
542
+
543
+ before: Optional[PreprocOutput] = None
544
+ if change_type != ChangeType.CREATED:
545
+ before = PreprocOutput(
546
+ name=node_output.name,
547
+ value=value_delta.before,
548
+ export=export_delta.before if export_delta else None,
549
+ condition=condition_delta.before if condition_delta else None,
550
+ )
551
+ after: Optional[PreprocOutput] = None
552
+ if change_type != ChangeType.REMOVED:
553
+ after = PreprocOutput(
554
+ name=node_output.name,
555
+ value=value_delta.after,
556
+ export=export_delta.after if export_delta else None,
557
+ condition=condition_delta.after if condition_delta else None,
558
+ )
559
+ return PreprocEntityDelta(before=before, after=after)
560
+
561
+ def visit_node_outputs(
562
+ self, node_outputs: NodeOutputs
563
+ ) -> PreprocEntityDelta[list[PreprocOutput], list[PreprocOutput]]:
564
+ before: list[PreprocOutput] = list()
565
+ after: list[PreprocOutput] = list()
566
+ for node_output in node_outputs.outputs:
567
+ output_delta: PreprocEntityDelta[PreprocOutput, PreprocOutput] = self.visit(node_output)
568
+ output_before = output_delta.before
569
+ output_after = output_delta.after
570
+ if output_before:
571
+ before.append(output_before)
572
+ if output_after:
573
+ after.append(output_after)
574
+ return PreprocEntityDelta(before=before, after=after)
@@ -49,18 +49,18 @@ class ChangeSetModelVisitor(abc.ABC):
49
49
  def visit_node_template(self, node_template: NodeTemplate):
50
50
  self.visit_children(node_template)
51
51
 
52
- def visit_node_mapping(self, node_mapping: NodeMapping):
53
- self.visit_children(node_mapping)
54
-
55
- def visit_node_mappings(self, node_mappings: NodeMappings):
56
- self.visit_children(node_mappings)
57
-
58
52
  def visit_node_outputs(self, node_outputs: NodeOutputs):
59
53
  self.visit_children(node_outputs)
60
54
 
61
55
  def visit_node_output(self, node_output: NodeOutput):
62
56
  self.visit_children(node_output)
63
57
 
58
+ def visit_node_mapping(self, node_mapping: NodeMapping):
59
+ self.visit_children(node_mapping)
60
+
61
+ def visit_node_mappings(self, node_mappings: NodeMappings):
62
+ self.visit_children(node_mappings)
63
+
64
64
  def visit_node_parameters(self, node_parameters: NodeParameters):
65
65
  self.visit_children(node_parameters)
66
66
 
@@ -1,8 +1,11 @@
1
+ import json
1
2
  import logging
2
3
  from copy import deepcopy
4
+ from typing import Any
3
5
 
4
6
  from localstack.aws.api import RequestContext, handler
5
7
  from localstack.aws.api.cloudformation import (
8
+ Changes,
6
9
  ChangeSetNameOrId,
7
10
  ChangeSetNotFoundException,
8
11
  ChangeSetType,
@@ -188,6 +191,30 @@ class CloudformationProviderV2(CloudformationProvider):
188
191
  resolved_parameters=resolved_parameters,
189
192
  )
190
193
 
194
+ # TODO: reconsider the way parameters are modelled in the update graph process.
195
+ # The options might be reduce to using the current style, or passing the extra information
196
+ # as a metadata object. The choice should be made considering when the extra information
197
+ # is needed for the update graph building, or only looked up in downstream tasks (metadata).
198
+ request_parameters = request.get("Parameters", list())
199
+ after_parameters: dict[str, Any] = {
200
+ parameter["ParameterKey"]: parameter["ParameterValue"]
201
+ for parameter in request_parameters
202
+ }
203
+ before_parameters: dict[str, Any] = {
204
+ parameter["ParameterKey"]: parameter["ParameterValue"]
205
+ for parameter in old_parameters.values()
206
+ }
207
+
208
+ # TODO: update this logic to always pass the clean template object if one exists. The
209
+ # current issue with relaying on stack.template_original is that this appears to have
210
+ # its parameters and conditions populated.
211
+ before_template = None
212
+ if change_set_type == ChangeSetType.UPDATE:
213
+ before_template = json.loads(
214
+ stack.template_body
215
+ ) # template_original is sometimes invalid
216
+ after_template = template
217
+
191
218
  # create change set for the stack and apply changes
192
219
  change_set = StackChangeSet(
193
220
  context.account_id,
@@ -199,9 +226,14 @@ class CloudformationProviderV2(CloudformationProvider):
199
226
  )
200
227
  # only set parameters for the changeset, then switch to stack on execute_change_set
201
228
  change_set.template_body = template_body
202
- change_set.populate_update_graph(stack.template, transformed_template)
229
+ change_set.populate_update_graph(
230
+ before_template=before_template,
231
+ after_template=after_template,
232
+ before_parameters=before_parameters,
233
+ after_parameters=after_parameters,
234
+ )
203
235
 
204
- # TODO: evaluate conditions
236
+ # TODO: move this logic of condition resolution with metadata to the ChangeSetModelPreproc or Executor
205
237
  raw_conditions = transformed_template.get("Conditions", {})
206
238
  resolved_stack_conditions = resolve_stack_conditions(
207
239
  account_id=context.account_id,
@@ -212,6 +244,7 @@ class CloudformationProviderV2(CloudformationProvider):
212
244
  stack_name=stack_name,
213
245
  )
214
246
  change_set.set_resolved_stack_conditions(resolved_stack_conditions)
247
+ change_set.set_resolved_parameters(resolved_parameters)
215
248
 
216
249
  # a bit gross but use the template ordering to validate missing resources
217
250
  try:
@@ -326,9 +359,10 @@ class CloudformationProviderV2(CloudformationProvider):
326
359
  raise ChangeSetNotFoundException(f"ChangeSet [{change_set_name}] does not exist")
327
360
 
328
361
  change_set_describer = ChangeSetModelDescriber(
329
- node_template=change_set.update_graph, include_property_values=include_property_values
362
+ node_template=change_set.update_graph,
363
+ include_property_values=bool(include_property_values),
330
364
  )
331
- resource_changes = change_set_describer.get_changes()
365
+ changes: Changes = change_set_describer.get_changes()
332
366
 
333
367
  attrs = [
334
368
  "ChangeSetType",
@@ -343,5 +377,5 @@ class CloudformationProviderV2(CloudformationProvider):
343
377
  result["Parameters"] = [
344
378
  mask_no_echo(strip_parameter_type(p)) for p in result.get("Parameters", [])
345
379
  ]
346
- result["Changes"] = resource_changes
380
+ result["Changes"] = changes
347
381
  return result
@@ -0,0 +1,5 @@
1
+ from localstack import config
2
+
3
+
4
+ def is_v2_engine() -> bool:
5
+ return config.SERVICE_PROVIDER_CONFIG.get_provider("cloudformation") == "engine-v2"