Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 66937e0

Browse files
committed
v1 tests port, test annotations, batch of parity improvements
1 parent 648fcf1 commit 66937e0

File tree

149 files changed

+22126
-64
lines changed

Some content is hidden

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

149 files changed

+22126
-64
lines changed

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,6 @@ 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.
527526
if not isinstance(arguments, TerminalValue):
528527
return arguments.change_type
529528

@@ -1170,7 +1169,7 @@ def _retrieve_parameter_if_exists(self, parameter_name: str) -> Optional[NodePar
11701169
parameters_scope, parameter_name, before_parameters, after_parameters
11711170
)
11721171
node_parameter = self._visit_parameter(
1173-
parameters_scope,
1172+
parameter_scope,
11741173
parameter_name,
11751174
before_parameter=before_parameter,
11761175
after_parameter=after_parameter,

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

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

4748
arguments_delta = self.visit(node_intrinsic_function.arguments)
48-
before_argument_list = arguments_delta.before
49-
after_argument_list = arguments_delta.after
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(".")
5055

5156
before = None
52-
if before_argument_list:
53-
before_logical_name_of_resource = before_argument_list[0]
54-
before_attribute_name = before_argument_list[1]
57+
if before_argument:
58+
before_logical_name_of_resource = before_argument[0]
59+
before_attribute_name = before_argument[1]
5560
before_node_resource = self._get_node_resource_for(
5661
resource_name=before_logical_name_of_resource, node_template=self._node_template
5762
)
58-
before_node_property = self._get_node_property_for(
63+
before_node_property: Optional[NodeProperty] = self._get_node_property_for(
5964
property_name=before_attribute_name, node_resource=before_node_resource
6065
)
61-
before_property_delta = self.visit(before_node_property)
62-
before = before_property_delta.before
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+
)
6374

6475
after = None
65-
if after_argument_list:
66-
after_logical_name_of_resource = after_argument_list[0]
67-
after_attribute_name = after_argument_list[1]
76+
if after_argument:
77+
after_logical_name_of_resource = after_argument[0]
78+
after_attribute_name = after_argument[1]
6879
after_node_resource = self._get_node_resource_for(
6980
resource_name=after_logical_name_of_resource, node_template=self._node_template
7081
)
@@ -74,12 +85,18 @@ def visit_node_intrinsic_function_fn_get_att(
7485
)
7586
if after_node_property is not None:
7687
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
7792
else:
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
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
83100

84101
return PreprocEntityDelta(before=before, after=after)
85102

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

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -103,40 +103,44 @@ def visit_node_resource(
103103
`after` delta with the physical resource ID, if side effects resulted in an update.
104104
"""
105105
delta = super().visit_node_resource(node_resource=node_resource)
106-
self._execute_on_resource_change(
107-
name=node_resource.name, before=delta.before, after=delta.after
108-
)
109-
after_resource = delta.after
110-
if after_resource is not None and delta.before != delta.after:
111-
after_logical_id = after_resource.logical_id
112-
after_physical_id: Optional[str] = self._after_resource_physical_id(
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(
113125
resource_logical_id=after_logical_id
114126
)
115-
if after_physical_id is None:
116-
raise RuntimeError(
117-
f"No PhysicalResourceId was found for resource '{after_physical_id}' post-update."
118-
)
119-
after_resource.physical_resource_id = after_physical_id
127+
after.physical_resource_id = after_physical_id
120128
return delta
121129

122130
def visit_node_output(
123131
self, node_output: NodeOutput
124132
) -> PreprocEntityDelta[PreprocOutput, PreprocOutput]:
125133
delta = super().visit_node_output(node_output=node_output)
126-
if delta.after is None:
127-
# handling deletion so the output does not really matter
128-
# TODO: are there other situations?
134+
after = delta.after
135+
if after is None or (isinstance(after, PreprocOutput) and after.condition is False):
129136
return delta
130-
131137
self.outputs[delta.after.name] = delta.after.value
132138
return delta
133139

134-
def _execute_on_resource_change(
140+
def _execute_resource_change(
135141
self, name: str, before: Optional[PreprocResource], after: Optional[PreprocResource]
136142
) -> None:
137-
if before == after:
138-
# unchanged: nothing to do.
139-
return
143+
# Changes are to be made about this resource.
140144
# TODO: this logic is a POC and should be revised.
141145
if before is not None and after is not None:
142146
# Case: change on same type.
@@ -257,11 +261,34 @@ def _execute_resource_action(
257261
case OperationStatus.SUCCESS:
258262
# merge the resources state with the external state
259263
# 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+
260272
self.resources[logical_resource_id]["Properties"].update(event.resource_model)
261273
self.resources[logical_resource_id].update(extra_resource_properties)
262274
# XXX for legacy delete_stack compatibility
263275
self.resources[logical_resource_id]["LogicalResourceId"] = logical_resource_id
264276
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+
265292
case OperationStatus.FAILED:
266293
reason = event.message
267294
LOG.warning(

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

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ 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)
171172
return node_resource
172173
raise RuntimeError(f"No resource '{resource_name}' was found")
173174

@@ -177,6 +178,7 @@ def _get_node_property_for(
177178
# TODO: this could be improved with hashmap lookups if the Node contained bindings and not lists.
178179
for node_property in node_resource.properties.properties:
179180
if node_property.name == property_name:
181+
self.visit(node_property)
180182
return node_property
181183
return None
182184

@@ -189,11 +191,9 @@ def _deployed_property_value_of(
189191
# process the resource if this wasn't processed already. Ideally, values should only
190192
# be accessible through delta objects, to ensure computation is always complete at
191193
# every level.
192-
node_resource = self._get_node_resource_for(
194+
_ = self._get_node_resource_for(
193195
resource_name=resource_logical_id, node_template=self._node_template
194196
)
195-
self.visit(node_resource)
196-
197197
resolved_resource = resolved_resources.get(resource_logical_id)
198198
if resolved_resource is None:
199199
raise RuntimeError(
@@ -228,6 +228,7 @@ def _get_node_mapping(self, map_name: str) -> NodeMapping:
228228
# TODO: another scenarios suggesting property lookups might be preferable.
229229
for mapping in mappings:
230230
if mapping.name == map_name:
231+
self.visit(mapping)
231232
return mapping
232233
# TODO
233234
raise RuntimeError()
@@ -237,6 +238,7 @@ def _get_node_parameter_if_exists(self, parameter_name: str) -> Optional[NodePar
237238
# TODO: another scenarios suggesting property lookups might be preferable.
238239
for parameter in parameters:
239240
if parameter.name == parameter_name:
241+
self.visit(parameter)
240242
return parameter
241243
return None
242244

@@ -245,6 +247,7 @@ def _get_node_condition_if_exists(self, condition_name: str) -> Optional[NodeCon
245247
# TODO: another scenarios suggesting property lookups might be preferable.
246248
for condition in conditions:
247249
if condition.name == condition_name:
250+
self.visit(condition)
248251
return condition
249252
return None
250253

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

380387
before = None
381-
if before_argument_list:
382-
before_logical_name_of_resource = before_argument_list[0]
383-
before_attribute_name = before_argument_list[1]
388+
if before_argument:
389+
before_logical_name_of_resource = before_argument[0]
390+
before_attribute_name = before_argument[1]
384391

385392
before_node_resource = self._get_node_resource_for(
386393
resource_name=before_logical_name_of_resource, node_template=self._node_template
@@ -401,9 +408,9 @@ def visit_node_intrinsic_function_fn_get_att(
401408
)
402409

403410
after = None
404-
if after_argument_list:
405-
after_logical_name_of_resource = after_argument_list[0]
406-
after_attribute_name = after_argument_list[1]
411+
if after_argument:
412+
after_logical_name_of_resource = after_argument[0]
413+
after_attribute_name = after_argument[1]
407414
after_node_resource = self._get_node_resource_for(
408415
resource_name=after_logical_name_of_resource, node_template=self._node_template
409416
)
@@ -452,10 +459,14 @@ def _compute_delta_for_if_statement(args: list[Any]) -> PreprocEntityDelta:
452459
)
453460

454461
# TODO: add support for this being created or removed.
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
462+
before = None
463+
if arguments_delta.before:
464+
before_outcome_delta = _compute_delta_for_if_statement(arguments_delta.before)
465+
before = before_outcome_delta.before
466+
after = None
467+
if arguments_delta.after:
468+
after_outcome_delta = _compute_delta_for_if_statement(arguments_delta.after)
469+
after = after_outcome_delta.after
459470
return PreprocEntityDelta(before=before, after=after)
460471

461472
def visit_node_intrinsic_function_fn_not(
@@ -520,6 +531,8 @@ def _compute_sub(args: str | list[Any], select_before: bool = False) -> str:
520531
template_variable_value = (
521532
resource_delta.before if select_before else resource_delta.after
522533
)
534+
if isinstance(template_variable_value, PreprocResource):
535+
template_variable_value = template_variable_value.logical_id
523536
except RuntimeError:
524537
raise RuntimeError(
525538
f"Undefined variable name in Fn::Sub string template '{template_variable_name}'"
@@ -558,7 +571,7 @@ def _compute_join(args: list[Any]) -> str:
558571
delimiter: str = str(args[0])
559572
values: list[Any] = args[1]
560573
if not isinstance(values, list):
561-
raise RuntimeError("Invalid arguments list definition for Fn::Join")
574+
raise RuntimeError(f"Invalid arguments list definition for Fn::Join: '{args}'")
562575
join_result = delimiter.join(map(str, values))
563576
return join_result
564577

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

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

5050
def visit_node_template(self, node_template: NodeTemplate):
51-
self.visit_children(node_template)
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)
5257

5358
def visit_node_outputs(self, node_outputs: NodeOutputs):
5459
self.visit_children(node_outputs)

‎localstack-core/localstack/testing/pytest/cloudformation/fixtures.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pytest
66

7-
from localstack.aws.api.cloudformation import StackEvent
7+
from localstack.aws.api.cloudformation import DescribeChangeSetOutput, StackEvent
88
from localstack.aws.connect import ServiceLevelClientFactory
99
from localstack.utils.functions import call_safe
1010
from localstack.utils.strings import short_uid
@@ -29,6 +29,12 @@ def capture(stack_name: str) -> PerResourceStackEvents:
2929
return capture
3030

3131

32+
def _normalise_describe_change_set_output(value: DescribeChangeSetOutput) -> None:
33+
value.get("Changes", list()).sort(
34+
key=lambda change: change.get("ResourceChange", dict()).get("LogicalResourceId", str())
35+
)
36+
37+
3238
@pytest.fixture
3339
def capture_update_process(aws_client_no_retry, cleanups, capture_per_resource_events):
3440
"""
@@ -84,12 +90,15 @@ def inner(
8490
ChangeSetName=change_set_id, IncludePropertyValues=True
8591
)
8692
)
93+
_normalise_describe_change_set_output(describe_change_set_with_prop_values)
8794
snapshot.match("describe-change-set-1-prop-values", describe_change_set_with_prop_values)
95+
8896
describe_change_set_without_prop_values = (
8997
aws_client_no_retry.cloudformation.describe_change_set(
9098
ChangeSetName=change_set_id, IncludePropertyValues=False
9199
)
92100
)
101+
_normalise_describe_change_set_output(describe_change_set_without_prop_values)
93102
snapshot.match("describe-change-set-1", describe_change_set_without_prop_values)
94103

95104
execute_results = aws_client_no_retry.cloudformation.execute_change_set(
@@ -132,12 +141,15 @@ def inner(
132141
ChangeSetName=change_set_id, IncludePropertyValues=True
133142
)
134143
)
144+
_normalise_describe_change_set_output(describe_change_set_with_prop_values)
135145
snapshot.match("describe-change-set-2-prop-values", describe_change_set_with_prop_values)
146+
136147
describe_change_set_without_prop_values = (
137148
aws_client_no_retry.cloudformation.describe_change_set(
138149
ChangeSetName=change_set_id, IncludePropertyValues=False
139150
)
140151
)
152+
_normalise_describe_change_set_output(describe_change_set_without_prop_values)
141153
snapshot.match("describe-change-set-2", describe_change_set_without_prop_values)
142154

143155
execute_results = aws_client_no_retry.cloudformation.execute_change_set(

0 commit comments

Comments
 (0)