8000 CloudFormation v2 Engine: V1 Test Porting and Annotations and Batch of Parity Improvements by MEPalma · Pull Request #12660 · localstack/localstack · GitHub
[go: up one dir, main page]

Skip to content

CloudFormation v2 Engine: V1 Test Porting and Annotations and Batch of Parity Improvements #12660

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 2, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ def __init__(self, scope: Scope, value: Any):
FnGetAttKey: Final[str] = "Fn::GetAtt"
FnEqualsKey: Final[str] = "Fn::Equals"
FnFindInMapKey: Final[str] = "Fn::FindInMap"
FnSubKey: Final[str] = "Fn::Sub"
INTRINSIC_FUNCTIONS: Final[set[str]] = {
RefKey,
FnIfKey,
Expand All @@ -393,6 +394,7 @@ def __init__(self, scope: Scope, value: Any):
FnEqualsKey,
FnGetAttKey,
FnFindInMapKey,
FnSubKey,
}


Expand Down Expand Up @@ -521,7 +523,6 @@ def _resolve_intrinsic_function_fn_get_att(self, arguments: ChangeSetEntity) ->
def _resolve_intrinsic_function_ref(self, arguments: ChangeSetEntity) -> ChangeType:
if arguments.change_type != ChangeType.UNCHANGED:
return arguments.change_type
# TODO: add support for nested functions, here we assume the argument is a logicalID.
if not isinstance(arguments, TerminalValue):
return arguments.change_type

Expand Down Expand Up @@ -1168,7 +1169,7 @@ def _retrieve_parameter_if_exists(self, parameter_name: str) -> Optional[NodePar
parameters_scope, parameter_name, before_parameters, after_parameters
)
node_parameter = self._visit_parameter(
parameters_scope,
parameter_scope,
parameter_name,
before_parameter=before_parameter,
after_parameter=after_parameter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import localstack.aws.api.cloudformation as cfn_api
from localstack.services.cloudformation.engine.v2.change_set_model import (
NodeIntrinsicFunction,
NodeProperty,
NodeResource,
PropertiesKey,
)
Expand Down Expand Up @@ -45,26 +46,36 @@ def visit_node_intrinsic_function_fn_get_att(
# artificially limit the precision of our output to match AWS's?

arguments_delta = self.visit(node_intrinsic_function.arguments)
before_argument_list = arguments_delta.before
after_argument_list = arguments_delta.after
before_argument: Optional[list[str]] = arguments_delta.before
if isinstance(before_argument, str):
before_argument = before_argument.split(".")
after_argument: Optional[list[str]] = arguments_delta.after
if isinstance(after_argument, str):
after_argument = after_argument.split(".")

before = None
if before_argument_list:
before_logical_name_of_resource = before_argument_list[0]
before_attribute_name = before_argument_list[1]
if before_argument:
before_logical_name_of_resource = before_argument[0]
before_attribute_name = before_argument[1]
before_node_resource = self._get_node_resource_for(
resource_name=before_logical_name_of_resource, node_template=self._node_template
)
before_node_property = self._get_node_property_for(
before_node_property: Optional[NodeProperty] = self._get_node_property_for(
property_name=before_attribute_name, node_resource=before_node_resource
)
before_property_delta = self.visit(before_node_property)
before = before_property_delta.before
if before_node_property is not None:
before_property_delta = self.visit(before_node_property)
before = before_property_delta.before
else:
before = self._before_deployed_property_value_of(
resource_logical_id=before_logical_name_of_resource,
property_name=before_attribute_name,
)

after = None
if after_argument_list:
after_logical_name_of_resource = after_argument_list[0]
after_attribute_name = after_argument_list[1]
if after_argument:
after_logical_name_of_resource = after_argument[0]
after_attribute_name = after_argument[1]
after_node_resource = self._get_node_resource_for(
resource_name=after_logical_name_of_resource, node_template=self._node_template
)
Expand All @@ -74,12 +85,18 @@ def visit_node_intrinsic_function_fn_get_att(
)
if after_node_property is not None:
after_property_delta = self.visit(after_node_property)
if after_property_delta.before == after_property_delta.after:
after = after_property_delta.after
else:
after = CHANGESET_KNOWN_AFTER_APPLY
else:
after_property_delta = PreprocEntityDelta(after=CHANGESET_KNOWN_AFTER_APPLY)
if after_property_delta.before == after_property_delta.after:
after = after_property_delta.after
else:
after = CHANGESET_KNOWN_AFTER_APPLY
try:
after = self._after_deployed_property_value_of(
resource_logical_id=after_logical_name_of_resource,
property_name=after_attribute_name,
)
except RuntimeError:
after = CHANGESET_KNOWN_AFTER_APPLY

return PreprocEntityDelta(before=before, after=after)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,40 +103,44 @@ def visit_node_resource(
`after` delta with the physical resource ID, if side effects resulted in an update.
"""
delta = super().visit_node_resource(node_resource=node_resource)
self._execute_on_resource_change(
name=node_resource.name, before=delta.before, after=delta.after
)
after_resource = delta.after
if after_resource is not None and delta.before != delta.after:
after_logical_id = after_resource.logical_id
after_physical_id: Optional[str] = self._after_resource_physical_id(
before = delta.before
after = delta.after

if before != after:
# There are changes for this resource.
self._execute_resource_change(name=node_resource.name, before=before, after=after)
else:
# There are no updates for this resource; iff the resource was previously
# deployed, then the resolved details are copied in the current state for
# references or other downstream operations.
if before is not None:
before_logical_id = delta.before.logical_id
before_resource = self._before_resolved_resources.get(before_logical_id, dict())
self.resources[before_logical_id] = before_resource

# Update the latest version of this resource for downstream references.
if after is not None:
after_logical_id = after.logical_id
after_physical_id: str = self._after_resource_physical_id(
resource_logical_id=after_logical_id
)
if after_physical_id is None:
raise RuntimeError(
f"No PhysicalResourceId was found for resource '{after_physical_id}' post-update."
)
after_resource.physical_resource_id = after_physical_id
after.physical_resource_id = after_physical_id
return delta

def visit_node_output(
self, node_output: NodeOutput
) -> PreprocEntityDelta[PreprocOutput, PreprocOutput]:
delta = super().visit_node_output(node_output=node_output)
if delta.after is None:
# handling deletion so the output does not really matter
# TODO: are there other situations?
after = delta.after
if after is None or (isinstance(after, PreprocOutput) and after.condition is False):
return delta

self.outputs[delta.after.name] = delta.after.value
return delta

def _execute_on_resource_change(
def _execute_resource_change(
self, name: str, before: Optional[PreprocResource], after: Optional[PreprocResource]
) -> None:
if before == after:
# unchanged: nothing to do.
return
# Changes are to be made about this resource.
# TODO: this logic is a POC and should be revised.
if before is not None and after is not None:
# Case: change on same type.
Expand Down Expand Up @@ -257,11 +261,34 @@ def _execute_resource_action(
case OperationStatus.SUCCESS:
# merge the resources state with the external state
# TODO: this is likely a duplicate of updating from extra_resource_properties

# TODO: add typing
# TODO: avoid the use of string literals for sampling from the object, use typed classes instead
# TODO: avoid sampling from resources and use tmp var reference
# TODO: add utils functions to abstract this logic away (resource.update(..))
# TODO: avoid the use of setdefault (debuggability/readability)
# TODO: review the use of merge

self.resources[logical_resource_id]["Properties"].update(event.resource_model)
self.resources[logical_resource_id].update(extra_resource_properties)
# XXX for legacy delete_stack compatibility
self.resources[logical_resource_id]["LogicalResourceId"] = logical_resource_id
self.resources[logical_resource_id]["Type"] = resource_type

# TODO: review why the physical id is returned as None during updates
# TODO: abstract this in member function of resource classes instead
physical_resource_id = None
try:
physical_resource_id = self._after_resource_physical_id(logical_resource_id)
except RuntimeError:
# The physical id is missing or is set to None, which is invalid.
pass
if physical_resource_id is None:
# The physical resource id is None after an update that didn't rewrite the resource, the previous
# resource id is therefore the current physical id of this resource.
physical_resource_id = self._before_resource_physical_id(logical_resource_id)
self.resources[logical_resource_id]["PhysicalResourceId"] = physical_resource_id

case OperationStatus.FAILED:
reason = event.message
LOG.warning(
Expand Down
Loading
Loading
0