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.
- localstack/config.py +0 -6
- localstack/deprecations.py +14 -0
- localstack/services/cloudformation/engine/entities.py +9 -4
- localstack/services/cloudformation/engine/v2/change_set_model.py +32 -67
- localstack/services/cloudformation/engine/v2/change_set_model_describer.py +119 -487
- localstack/services/cloudformation/engine/v2/change_set_model_executor.py +107 -70
- localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +574 -0
- localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +6 -6
- localstack/services/cloudformation/v2/provider.py +39 -5
- localstack/services/cloudformation/v2/utils.py +5 -0
- localstack/services/sns/resource_providers/aws_sns_topic.py +1 -0
- localstack/testing/pytest/cloudformation/__init__.py +0 -0
- localstack/testing/pytest/cloudformation/fixtures.py +169 -0
- localstack/version.py +2 -2
- {localstack_core-4.3.1.dev35.dist-info → localstack_core-4.3.1.dev37.dist-info}/METADATA +1 -1
- {localstack_core-4.3.1.dev35.dist-info → localstack_core-4.3.1.dev37.dist-info}/RECORD +24 -20
- localstack_core-4.3.1.dev37.dist-info/plux.json +1 -0
- localstack_core-4.3.1.dev35.dist-info/plux.json +0 -1
- {localstack_core-4.3.1.dev35.data → localstack_core-4.3.1.dev37.data}/scripts/localstack +0 -0
- {localstack_core-4.3.1.dev35.data → localstack_core-4.3.1.dev37.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.3.1.dev35.data → localstack_core-4.3.1.dev37.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.3.1.dev35.dist-info → localstack_core-4.3.1.dev37.dist-info}/WHEEL +0 -0
- {localstack_core-4.3.1.dev35.dist-info → localstack_core-4.3.1.dev37.dist-info}/entry_points.txt +0 -0
- {localstack_core-4.3.1.dev35.dist-info → localstack_core-4.3.1.dev37.dist-info}/licenses/LICENSE.txt +0 -0
- {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(
|
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:
|
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,
|
362
|
+
node_template=change_set.update_graph,
|
363
|
+
include_property_values=bool(include_property_values),
|
330
364
|
)
|
331
|
-
|
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"] =
|
380
|
+
result["Changes"] = changes
|
347
381
|
return result
|