8000 Add deletion support · localstack/localstack@0e6c490 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0e6c490

Browse files
committed
Add deletion support
1 parent 7656937 commit 0e6c490

File tree

144 files changed

+235
-21468
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

144 files changed

+235
-21468
lines changed

localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,7 @@ def _resolve_intrinsic_function_fn_get_att(self, arguments: ChangeSetEntity) ->
523523
def _resolve_intrinsic_function_ref(self, arguments: ChangeSetEntity) -> ChangeType:
524524
if arguments.change_type != ChangeType.UNCHANGED:
525525
return arguments.change_type
526+
# TODO: add support for nested functions, here we assume the argument is a logicalID.
526527
if not isinstance(arguments, TerminalValue):
527528
return arguments.change_type
528529

@@ -1169,7 +1170,7 @@ def _retrieve_parameter_if_exists(self, parameter_name: str) -> Optional[NodePar
11691170
parameters_scope, parameter_name, before_parameters, after_parameters
11701171
)
11711172
node_parameter = self._visit_parameter(
1172-
parameter_scope,
1173+
parameters_scope,
11731174
parameter_name,
11741175
before_parameter=before_parameter,
11751176
after_parameter=after_parameter,

localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import localstack.aws.api.cloudformation as cfn_api
77
from localstack.services.cloudformation.engine.v2.change_set_model import (
88
NodeIntrinsicFunction,
9-
NodeProperty,
109
NodeResource,
1110
PropertiesKey,
1211
)
@@ -46,36 +45,26 @@ def visit_node_intrinsic_function_fn_get_att(
4645
# artificially limit the precision of our output to match AWS's?
4746

4847
arguments_delta = self.visit(node_intrinsic_function.arguments)
49-
before_argument: Optional[list[str]] = arguments_delta.before
50-
if isinstance(before_argument, str):
51-
before_argument = before_argument.split(".")
52-
after_argument: Optional[list[str]] = arguments_delta.after
53-
if isinstance(after_argument, str):
54-
after_argument = after_argument.split(".")
48+
before_argument_list = arguments_delta.before
49+
after_argument_list = arguments_delta.after
5550

5651
before = None
57-
if before_argument:
58-
before_logical_name_of_resource = before_argument[0]
59-
before_attribute_name = before_argument[1]
52+
if before_argument_list:
53+
before_logical_name_of_resource = before_argument_list[0]
54+
before_attribute_name = before_argument_list[1]
6055
before_node_resource = self._get_node_resource_for(
6156
resource_name=before_logical_name_of_resource, node_template=self._node_template
6257
)
63-
before_node_property: Optional[NodeProperty] = self._get_node_property_for(
58+
before_node_property = self._get_node_property_for(
6459
property_name=before_attribute_name, node_resource=before_node_resource
6560
)
66-
if before_node_property is not None:
67-
before_property_delta = self.visit(before_node_property)
68-
before = before_property_delta.before
69-
else:
70-
before = self._before_deployed_property_value_of(
71-
resource_logical_id=before_logical_name_of_resource,
72-
property_name=before_attribute_name,
73-
)
61+
before_property_delta = self.visit(before_node_property)
62+
before = before_property_delta.before
7463

7564
after = None
76-
if after_argument:
77-
after_logical_name_of_resource = after_argument[0]
78-
after_attribute_name = after_argument[1]
65+
if after_argument_list:
66+
after_logical_name_of_resource = after_argument_list[0]
67+
after_attribute_name = after_argument_list[1]
7968
after_node_resource = self._get_node_resource_for(
8069
resource_name=after_logical_name_of_resource, node_template=self._node_template
8170
)
@@ -85,18 +74,12 @@ def visit_node_intrinsic_function_fn_get_att(
8574
)
8675
if after_node_property is not None:
8776
after_property_delta = self.visit(after_node_property)
88-
if after_property_delta.before == after_property_delta.after:
89-
after = after_property_delta.after
90-
else:
91-
after = CHANGESET_KNOWN_AFTER_APPLY
9277
else:
93-
try:
94-
after = self._after_deployed_property_value_of(
95-
resource_logical_id=after_logical_name_of_resource,
96-
property_name=after_attribute_name,
97-
)
98-
except RuntimeError:
99-
after = CHANGESET_KNOWN_AFTER_APPLY
78+
after_property_delta = PreprocEntityDelta(after=CHANGESET_KNOWN_AFTER_APPLY)
79+
if after_property_delta.before == after_property_delta.after:
80+
after = after_property_delta.after
81+
else:
82+
after = CHANGESET_KNOWN_AFTER_APPLY
10083

10184
return PreprocEntityDelta(before=before, after=after)
10285

localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py

Lines changed: 22 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ def __init__(self, change_set: ChangeSet):
5050
self.outputs = dict()
5151
self.resolved_parameters = dict()
5252

53-
# TODO: use a structured type for the return value
5453
def execute(self) -> ChangeSetModelExecutorResult:
5554
self.process()
5655
return ChangeSetModelExecutorResult(
@@ -103,44 +102,42 @@ def visit_node_resource(
103102
`after` delta with the physical resource ID, if side effects resulted in an update.
104103
"""
105104
delta = super().visit_node_resource(node_resource=node_resource)
106-
before = delta.before
107-
after = delta.after
108-
109-
if before != after:
110-
# There are changes for this resource.
111-
self._execute_resource_change(name=node_resource.name, before=before, after=after)
112-
else:
113-
# There are no updates for this resource; iff the resource was previously
114-
# deployed, then the resolved details are copied in the current state for
115-
# references or other downstream operations.
116-
if before is not None:
117-
before_logical_id = delta.before.logical_id
118-
before_resource = self._before_resolved_resources.get(before_logical_id, dict())
119-
self.resources[before_logical_id] = before_resource
120-
121-
# Update the latest version of this resource for downstream references.
122-
if after is not None:
123-
after_logical_id = after.logical_id
124-
after_physical_id: str = self._after_resource_physical_id(
105+
self._execute_on_resource_change(
106+
name=node_resource.name, before=delta.before, after=delta.after
107+
)
108+
after_resource = delta.after
109+
if after_resource is not None and delta.before != delta.after:
110+
after_logical_id = after_resource.logical_id
111+
after_physical_id: Optional[str] = self._after_resource_physical_id(
125112
resource_logical_id=after_logical_id
126113
)
127-
after.physical_resource_id = after_physical_id
114+
if after_physical_id is None:
115+
raise RuntimeError(
116+
f"No PhysicalResourceId was found for resource '{after_physical_id}' post-update."
117+
)
118+
after_resource.physical_resource_id = after_physical_id
128119
return delta
129120

130121
def visit_node_output(
131122
self, node_output: NodeOutput
132123
) -> PreprocEntityDelta[PreprocOutput, PreprocOutput]:
133124
delta = super().visit_node_output(node_output=node_output)
134-
after = delta.after
135-
if after is None or (isinstance(after, PreprocOutput) and after.condition is False):
125+
if delta.after is None:
126+
# handling deletion so the output does not really matter
127+
# TODO: are there other situations?
136128
return delta
129+
137130
self.outputs[delta.after.name] = delta.after.value
138131
return delta
139132

140-
def _execute_resource_change(
133+
def _execute_on_resource_change(
141134
self, name: str, before: Optional[PreprocResource], after: Optional[PreprocResource]
142135
) -> None:
143-
# Changes are to be made about this resource.
136+
if before == after:
137+
# unchanged: nothing to do but propagate the previous state to the new state.
138+
if before_properties := self._before_resolved_resources.get(name):
139+
self.resources[name] = before_properties.copy()
140+
return
144141
# TODO: this logic is a POC and should be revised.
145142
if before is not None and after is not None:
146143
# Case: change on same type.
@@ -261,34 +258,11 @@ def _execute_resource_action(
261258
case OperationStatus.SUCCESS:
262259
# merge the resources state with the external state
263260
# TODO: this is likely a duplicate of updating from extra_resource_properties
264-
265-
# TODO: add typing
266-
# TODO: avoid the use of string literals for sampling from the object, use typed classes instead
267-
# TODO: avoid sampling from resources and use tmp var reference
268-
# TODO: add utils functions to abstract this logic away (resource.update(..))
269-
# TODO: avoid the use of setdefault (debuggability/readability)
270-
# TODO: review the use of merge
271-
272261
self.resources[logical_resource_id]["Properties"].update(event.resource_model)
273262
self.resources[logical_resource_id].update(extra_resource_properties)
274263
# XXX for legacy delete_stack compatibility
275264
self.resources[logical_resource_id]["LogicalResourceId"] = logical_resource_id
276265
self.resources[logical_resource_id]["Type"] = resource_type
277-
278-
# TODO: review why the physical id is returned as None during updates
279-
# TODO: abstract this in member function of resource classes instead
280-
physical_resource_id = None
281-
try:
282-
physical_resource_id = self._after_resource_physical_id(logical_resource_id)
283-
except RuntimeError:
284-
# The physical id is missing or is set to None, which is invalid.
285-
pass
286-
if physical_resource_id is None:
287-
# The physical resource id is None after an update that didn't rewrite the resource, the previous
288-
# resource id is therefore the current physical id of this resource.
289-
physical_resource_id = self._before_resource_physical_id(logical_resource_id)
290-
self.resources[logical_resource_id]["PhysicalResourceId"] = physical_resource_id
291-
292266
case OperationStatus.FAILED:
293267
reason = event.message
294268
LOG.warning(

localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,6 @@ def _get_node_resource_for(
168168
# TODO: this could be improved with hashmap lookups if the Node contained bindings and not lists.
169169
for node_resource in node_template.resources.resources:
170170
if node_resource.name == resource_name:
171-
self.visit(node_resource)
172171
return node_resource
173172
raise RuntimeError(f"No resource '{resource_name}' was found")
174173

@@ -178,7 +177,6 @@ def _get_node_property_for(
178177
# TODO: this could be improved with hashmap lookups if the Node contained bindings and not lists.
179178
for node_property in node_resource.properties.properties:
180179
if node_property.name == property_name:
181-
self.visit(node_property)
182180
return node_property
183181
return None
184182

@@ -191,9 +189,10 @@ def _deployed_property_value_of(
191189
# process the resource if this wasn't processed already. Ideally, values should only
192190
# be accessible through delta objects, to ensure computation is always complete at
193191
# every level.
194-
_ = self._get_node_resource_for(
192+
node_resource = self._get_node_resource_for(
195193
resource_name=resource_logical_id, node_template=self._node_ F438 template
196194
)
195+
self.visit(node_resource)
197196

198197
resolved_resource = resolved_resources.get(resource_logical_id)
199198
if resolved_resource is None:
@@ -229,7 +228,6 @@ def _get_node_mapping(self, map_name: str) -> NodeMapping:
229228
# TODO: another scenarios suggesting property lookups might be preferable.
230229
for mapping in mappings:
231230
if mapping.name == map_name:
232-
self.visit(mapping)
233231
return mapping
234232
# TODO
235233
raise RuntimeError()
@@ -239,7 +237,6 @@ def _get_node_parameter_if_exists(self, parameter_name: str) -> Optional[NodePar
239237
# TODO: another scenarios suggesting property lookups might be preferable.
240238
for parameter in parameters:
241239
if parameter.name == parameter_name:
242-
self.visit(parameter)
243240
return parameter
244241
return None
245242

@@ -248,7 +245,6 @@ def _get_node_condition_if_exists(self, condition_name: str) -> Optional[NodeCon
248245
# TODO: another scenarios suggesting property lookups might be preferable.
249246
for condition in conditions:
250247
if condition.name == condition_name:
251-
self.visit(condition)
252248
return condition
253249
return None
254250

@@ -376,19 +372,15 @@ def visit_node_object(self, node_object: NodeObject) -> PreprocEntityDelta:
376372
def visit_node_intrinsic_function_fn_get_att(
377373
self, node_intrinsic_function: NodeIntrinsicFunction
378374
) -> PreprocEntityDelta:
379-
# TODO: validate the return value according to the spec.
380375
arguments_delta = self.visit(node_intrinsic_function.arguments)
381-
before_argument: Optional[list[str]] = arguments_delta.before
382-
if isinstance(before_argument, str):
383-
before_argument = before_argument.split(".")
384-
after_argument: Optional[list[str]] = arguments_delta.after
385-
if isinstance(after_argument, str):
386-
after_argument = after_argument.split(".")
376+
# TODO: validate the return value according to the spec.
377+
before_argument_list = arguments_delta.before
378+
after_argument_list = arguments_delta.after
387379

388380
before = None
389-
if before_argument:
390-
before_logical_name_of_resource = before_argument[0]
391-
before_attribute_name = before_argument[1]
381+
if before_argument_list:
382+
before_logical_name_of_resource = before_argument_list[0]
383+
before_attribute_name = before_argument_list[1]
392384

393385
before_node_resource = self._get_node_resource_for(
394386
resource_name=before_logical_name_of_resource, node_template=self._node_template
@@ -409,9 +401,9 @@ def visit_node_intrinsic_function_fn_get_att(
409401
)
410402

411403
after = None
412-
if after_argument:
413-
after_logical_name_of_resource = after_argument[0]
414-
after_attribute_name = after_argument[1]
404+
if after_argument_list:
405+
after_logical_name_of_resource = after_argument_list[0]
406+
after_attribute_name = after_argument_list[1]
415407
after_node_resource = self._get_node_resource_for(
416408
resource_name=after_logical_name_of_resource, node_template=self._node_template
417409
)
@@ -460,14 +452,10 @@ def _compute_delta_for_if_statement(args: list[Any]) -> PreprocEntityDelta:
460452
)
461453

462454
# TODO: add support for this being created or removed.
463-
before = None
464-
if arguments_delta.before:
465-
before_outcome_delta = _compute_delta_for_if_statement(arguments_delta.before)
466-
before = before_outcome_delta.before
467-
after = None
468-
if arguments_delta.after:
469-
after_outcome_delta = _compute_delta_for_if_statement(arguments_delta.after)
470-
after = after_outcome_delta.after
455+
before_outcome_delta = _compute_delta_for_if_statement(arguments_delta.before)
456+
before = before_outcome_delta.before
457+
after_outcome_delta = _compute_delta_for_if_statement(arguments_delta.after)
458+
after = after_outcome_delta.after
471459
return PreprocEntityDelta(before=before, after=after)
472460

473461
def visit_node_intrinsic_function_fn_not(
@@ -570,7 +558,7 @@ def _compute_join(args: list[Any]) -> str:
570558
delimiter: str = str(args[0])
571559
values: list[Any] = args[1]
572560
if not isinstance(values, list):
573-
raise RuntimeError(f"Invalid arguments list definition for Fn::Join: '{args}'")
561+
raise RuntimeError("Invalid arguments list definition for Fn::Join")
574562
join_result = delimiter.join(map(str, values))
575563
return join_result
576564

localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_visitor.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,7 @@ def visit_children(self, change_set_entity: ChangeSetEntity):
4848
self.visit(child)
4949

5050
def visit_node_template(self, node_template: NodeTemplate):
51-
# Visit the resources, which will lazily evaluate all the referenced (direct and indirect)
52-
# entities (parameters, mappings, conditions, etc.). Then compute the output fields; computing
53-
# only the output fields would only result in the deployment logic of the referenced outputs
54-
# being evaluated, hence enforce the visiting of all the resources first.
55-
self.visit(node_template.resources)
56-
self.visit(node_template.outputs)
51+
self.visit_children(node_template)
5752

5853
def visit_node_outputs(self, node_outputs: NodeOutputs):
5954
self.visit_children(node_outputs)

localstack-core/localstack/services/cloudformation/v2/provider.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -425,5 +425,37 @@ def delete_stack(
425425
# aws will silently ignore invalid stack names - we should do the same
426426
return
427427

428-
# TODO: actually delete
429-
stack.set_stack_status(StackStatus.DELETE_COMPLETE)
428+
before_parameters = stack.resolved_parameters
429+
after_parameters = None
430+
before_template = stack.template
431+
after_template = None
432+
433+
# create a dummy change set
434+
change_set = ChangeSet(
435+
stack,
436+
{
437+
"ChangeSetName": "delete-change-set",
438+
},
439+
)
440+
change_set.populate_update_graph(
441+
before_template=before_template,
442+
after_template=after_template,
443+
before_parameters=before_parameters,
444+
after_parameters=after_parameters,
445+
)
446+
447+
change_set_executor = ChangeSetModelExecutor(
448+
change_set,
449+
)
450+
451+
def _run(*args):
452+
try:
453+
stack.set_stack_status(StackStatus.DELETE_IN_PROGRESS)
454+
change_set_executor.execute()
455+
stack.set_stack_status(StackStatus.DELETE_COMPLETE)
456+
except Exception as e:
457+
LOG.warning(
458+
"failed to delete stack: %s", e, exc_info=LOG.isEnabledFor(logging.DEBUG)
459+
)
460+
461+
start_worker_thread(_run)

0 commit comments

Comments
 (0)
0