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

Skip to content

CloudFormation V2 Engine: Support for DependsOn Blocks #12644

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 1 commit into from
May 21, 2025
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -256,21 +256,24 @@ class NodeResource(ChangeSetNode):
type_: Final[ChangeSetTerminal]
condition_reference: Final[Optional[TerminalValue]]
properties: Final[NodeProperties]
depends_on: Final[Optional[NodeDependsOn]]

def __init__(
self,
scope: Scope,
change_type: ChangeType,
name: str,
type_: ChangeSetTerminal,
condition_reference: TerminalValue,
properties: NodeProperties,
condition_reference: Optional[TerminalValue],
depends_on: Optional[NodeDependsOn],
):
super().__init__(scope=scope, change_type=change_type)
self.name = name
self.type_ = type_
self.condition_reference = condition_reference
self.properties = properties
self.condition_reference = condition_reference
self.depends_on = depends_on


class NodeProperties(ChangeSetNode):
Expand All @@ -281,6 +284,14 @@ def __init__(self, scope: Scope, change_type: ChangeType, properties: list[NodeP
self.properties = properties


class NodeDependsOn(ChangeSetNode):
depends_on: Final[NodeArray]

def __init__(self, scope: Scope, change_type: ChangeType, depends_on: NodeArray):
super().__init__(scope=scope, change_type=change_type)
self.depends_on = depends_on


class NodeProperty(ChangeSetNode):
name: Final[str]
value: Final[ChangeSetEntity]
Expand Down Expand Up @@ -365,6 +376,7 @@ def __init__(self, scope: Scope, value: Any):
ValueKey: Final[str] = "Value"
ExportKey: Final[str] = "Export"
OutputsKey: Final[str] = "Outputs"
DependsOnKey: Final[str] = "DependsOn"
# TODO: expand intrinsic functions set.
RefKey: Final[str] = "Ref"
FnIfKey: Final[str] = "Fn::If"
Expand Down Expand Up @@ -770,12 +782,20 @@ def _visit_resource(
scope_condition, (before_condition, after_condition) = self._safe_access_in(
scope, ConditionKey, before_resource, after_resource
)
# TODO: condition references should be resolved for the condition's change_type?
if before_condition or after_condition:
condition_reference = self._visit_terminal_value(
scope_condition, before_condition, after_condition
)

depends_on = None
scope_depends_on, (before_depends_on, after_depends_on) = self._safe_access_in(
scope, DependsOnKey, before_resource, after_resource
)
if before_depends_on or after_depends_on:
depends_on = self._visit_depends_on(
scope_depends_on, before_depends_on, after_depends_on
)

scope_properties, (before_properties, after_properties) = self._safe_access_in(
scope, PropertiesKey, before_resource, after_resource
)
Expand All @@ -793,8 +813,9 @@ def _visit_resource(
change_type=change_type,
name=resource_name,
type_=terminal_value_type,
condition_reference=condition_reference,
properties=properties,
condition_reference=condition_reference,
depends_on=depends_on,
)
self._visited_scopes[scope] = node_resource
return node_resource
Expand Down Expand Up @@ -925,6 +946,38 @@ def _visit_parameters(
self._visited_scopes[scope] = node_parameters
return node_parameters

@staticmethod
def _normalise_depends_on_value(value: Maybe[str | list[str]]) -> Maybe[list[str]]:
# To simplify downstream logics, reduce the type options to array of strings.
# TODO: Add integrations tests for DependsOn validations (invalid types, duplicate identifiers, etc.)
if isinstance(value, NothingType):
return value
if isinstance(value, str):
value = [value]
elif isinstance(value, list):
value.sort()
else:
raise RuntimeError(
f"Invalid type for DependsOn, expected a String or Array of String, but got: '{value}'"
)
return value

def _visit_depends_on(
self,
scope: Scope,
before_depends_on: Maybe[str | list[str]],
after_depends_on: Maybe[str | list[str]],
) -> NodeDependsOn:
before_depends_on = self._normalise_depends_on_value(value=before_depends_on)
after_depends_on = self._normalise_depends_on_value(value=after_depends_on)
node_array = self._visit_array(
scope=scope, before_array=before_depends_on, after_array=after_depends_on
)
node_depends_on = NodeDependsOn(
scope=scope, change_type=node_array.change_type, depends_on=node_array
)
return node_depends_on

def _visit_condition(
self,
scope: Scope,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from localstack.aws.api.cloudformation import ChangeAction, StackStatus
from localstack.constants import INTERNAL_AWS_SECRET_ACCESS_KEY
from localstack.services.cloudformation.engine.v2.change_set_model import (
NodeDependsOn,
NodeOutput,
NodeParameter,
NodeResource,
Expand Down Expand Up @@ -77,6 +78,23 @@ def _after_resource_physical_id(self, resource_logical_id: str) -> str:
logical_resource_id=resource_logical_id, resolved_resources=after_resolved_resources
)

def visit_node_depends_on(self, node_depends_on: NodeDependsOn) -> PreprocEntityDelta:
array_identifiers_delta = super().visit_node_depends_on(node_depends_on=node_depends_on)

# Visit depends_on resources before returning.
depends_on_resource_logical_ids: set[str] = set()
if array_identifiers_delta.before:
depends_on_resource_logical_ids.update(array_identifiers_delta.before)
if array_identifiers_delta.after:
depends_on_resource_logical_ids.update(array_identifiers_delta.after)
for depends_on_resource_logical_id in depends_on_resource_logical_ids:
node_resource = self._get_node_resource_for(
resource_name=depends_on_resource_logical_id, node_template=self._node_template
)
self.visit_node_resource(node_resource)

return array_identifiers_delta

def visit_node_resource(
self, node_resource: NodeResource
) -> PreprocEntityDelta[PreprocResource, PreprocResource]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
ChangeType,
NodeArray,
NodeCondition,
NodeDependsOn,
NodeDivergence,
NodeIntrinsicFunction,
NodeMapping,
Expand Down Expand Up @@ -81,6 +82,7 @@ class PreprocResource:
condition: Optional[bool]
resource_type: str
properties: PreprocProperties
depends_on: Optional[list[str]]

def __init__(
self,
Expand All @@ -89,12 +91,14 @@ def __init__(
condition: Optional[bool],
resource_type: str,
properties: PreprocProperties,
depends_on: Optional[list[str]],
):
self.logical_id = logical_id
self.physical_resource_id = physical_resource_id
self.condition = condition
self.resource_type = resource_type
self.properties = properties
self.depends_on = depends_on

@staticmethod
def _compare_conditions(c1: bool, c2: bool):
Expand Down Expand Up @@ -533,6 +537,10 @@ def visit_node_parameter(self, node_parameter: NodeParameter) -> PreprocEntityDe

return PreprocEntityDelta(before=before, after=after)

def visit_node_depends_on(self, node_depends_on: NodeDependsOn) -> PreprocEntityDelta:
array_identifiers_delta = self.visit(node_depends_on.depends_on)
return array_identifiers_delta

def visit_node_condition(self, node_condition: NodeCondition) -> PreprocEntityDelta:
delta = self.visit(node_condition.body)
return delta
Expand Down Expand Up @@ -638,6 +646,13 @@ def visit_node_resource(
condition_before = condition_delta.before
condition_after = condition_delta.after

depends_on_before = None
depends_on_after = None
if node_resource.depends_on is not None:
depends_on_delta = self.visit_node_depends_on(node_resource.depends_on)
depends_on_before = depends_on_delta.before
depends_on_after = depends_on_delta.after

type_delta = self.visit(node_resource.type_)
properties_delta: PreprocEntityDelta[PreprocProperties, PreprocProperties] = self.visit(
node_resource.properties
Expand All @@ -656,6 +671,7 @@ def visit_node_resource(
condition=condition_before,
resource_type=type_delta.before,
properties=properties_delta.before,
depends_on=depends_on_before,
)
if change_type != ChangeType.REMOVED and condition_after is None or condition_after:
logical_resource_id = node_resource.name
Expand All @@ -671,6 +687,7 @@ def visit_node_resource(
condition=condition_after,
resource_type=type_delta.after,
properties=properties_delta.after,
depends_on=depends_on_after,
)
return PreprocEntityDelta(before=before, after=after)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
NodeArray,
NodeCondition,
NodeConditions,
NodeDependsOn,
NodeDivergence,
NodeIntrinsicFunction,
NodeMapping,
Expand Down Expand Up @@ -73,6 +74,9 @@ def visit_node_conditions(self, node_conditions: NodeConditions):
def visit_node_condition(self, node_condition: NodeCondition):
self.visit_children(node_condition)

def visit_node_depends_on(self, node_depends_on: NodeDependsOn):
self.visit_children(node_depends_on)

def visit_node_resources(self, node_resources: NodeResources):
self.visit_children(node_resources)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@
"""


@pytest.mark.skip(reason="no support for DependsOn")
# this is an `only_localstack` test because it makes use of _custom_id_ tag
@markers.aws.only_localstack
def test_cfn_apigateway_aws_integration(deploy_cfn_template, aws_client):
Expand Down Expand Up @@ -143,7 +142,10 @@ def test_cfn_apigateway_swagger_import(deploy_cfn_template, echo_http_server_pos
assert content["url"].endswith("/post")


@pytest.mark.skip(reason="No support for DependsOn")
@pytest.mark.skip(
reason="The v2 provider appears to instead return the correct url: "
"https://e1i3grfiws.execute-api.us-east-1.localhost.localstack.cloud/prod/"
)
@markers.aws.only_localstack
def test_url_output(httpserver, deploy_cfn_template):
httpserver.expect_request("").respond_with_data(b"", 200)
Expand Down Expand Up @@ -225,7 +227,7 @@ def test_cfn_with_apigateway_resources(deploy_cfn_template, aws_client, snapshot
# assert not apis


@pytest.mark.skip(reason="DependsOn is unsupported")
@pytest.mark.skip(reason="NotFoundException Invalid Method identifier specified")
@markers.aws.validated
@markers.snapshot.skip_snapshot_verify(
paths=[
Expand Down Expand Up @@ -279,7 +281,6 @@ def test_cfn_deploy_apigateway_models(deploy_cfn_template, snapshot, aws_client)
assert result.status_code == 400


@pytest.mark.skip(reason="DependsOn is unsupported")
@markers.aws.validated
def test_cfn_deploy_apigateway_integration(deploy_cfn_template, snapshot, aws_client):
snapshot.add_transformer(snapshot.transform.key_value("cacheNamespace"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"per-resource-events..*",
"delete-describe..*",
#
"$..ChangeSetId", # An issue for the WIP executor
# Before/After Context
"$..Capabilities",
"$..NotificationARNs",
Expand Down
Loading
Loading