Skip to content

Commit

Permalink
Extract resource ordering to new submodule
Browse files Browse the repository at this point in the history
  • Loading branch information
simonrw committed Jun 5, 2024
1 parent a730f46 commit 1aa3144
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 126 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from typing import Literal, Optional, TypedDict

Action = str


class ResourceChange(TypedDict):
Action: Action
LogicalResourceId: str
PhysicalResourceId: Optional[str]
ResourceType: str
Scope: list
Details: list
Replacement: Optional[Literal["False"]]


class ChangeConfig(TypedDict):
Type: str
ResourceChange: ResourceChange
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from collections import OrderedDict

from localstack.services.cloudformation.engine.changes import ChangeConfig
from localstack.services.cloudformation.engine.parameters import StackParameter
from localstack.services.cloudformation.engine.template_utils import get_deps_for_resource


class NoResourceInStack(ValueError):
"""Raised when we preprocess the template and do not find a resource"""

def __init__(self, logical_resource_id: str):
msg = f"Template format error: Unresolved resource dependencies [{logical_resource_id}] in the Resources block of the template"

super().__init__(msg)


def order_resources(
resources: dict,
resolved_parameters: dict[str, StackParameter],
resolved_conditions: dict[str, bool],
reverse: bool = False,
) -> OrderedDict:
"""
Given a dictionary of resources, topologically sort the resources based on
inter-resource dependencies (e.g. usages of intrinsic functions).
"""
nodes: dict[str, list[str]] = {}
for logical_resource_id, properties in resources.items():
nodes.setdefault(logical_resource_id, [])
deps = get_deps_for_resource(properties, resolved_conditions)
for dep in deps:
if dep in resolved_parameters:
# we only care about other resources
continue
nodes.setdefault(dep, [])
nodes[dep].append(logical_resource_id)

# implementation from https://dev.to/leopfeiffer/topological-sort-with-kahns-algorithm-3dl1
indegrees = {k: 0 for k in nodes.keys()}
for dependencies in nodes.values():
for dependency in dependencies:
indegrees[dependency] += 1

# Place all elements with indegree 0 in queue
queue = [k for k in nodes.keys() if indegrees[k] == 0]

sorted_logical_resource_ids = []

# Continue until all nodes have been dealt with
while len(queue) > 0:
# node of current iteration is the first one from the queue
curr = queue.pop(0)
sorted_logical_resource_ids.append(curr)

# remove the current node from other dependencies
for dependency in nodes[curr]:
indegrees[dependency] -= 1

if indegrees[dependency] == 0:
queue.append(dependency)

# check for circular dependencies
if len(sorted_logical_resource_ids) != len(nodes):
raise Exception("Circular dependency found.")

sorted_mapping = []
for logical_resource_id in sorted_logical_resource_ids:
if properties := resources.get(logical_resource_id):
sorted_mapping.append((logical_resource_id, properties))
else:
if (
logical_resource_id not in resolved_parameters
and logical_resource_id not in resolved_conditions
):
raise NoResourceInStack(logical_resource_id)

if reverse:
sorted_mapping = sorted_mapping[::-1]
return OrderedDict(sorted_mapping)


def order_changes(
given_changes: list[ChangeConfig],
resources: dict,
resolved_parameters: dict[str, StackParameter],
# TODO: remove resolved conditions somehow
resolved_conditions: dict[str, bool],
reverse: bool = False,
) -> list[ChangeConfig]:
"""
Given a list of changes, a dictionary of resources and a dictionary of resolved conditions, topologically sort the
changes based on inter-resource dependencies (e.g. usages of intrinsic functions).
"""
ordered_resources = order_resources(
resources=resources,
resolved_parameters=resolved_parameters,
resolved_conditions=resolved_conditions,
reverse=reverse,
)
sorted_changes = []
for logical_resource_id in ordered_resources.keys():
for change in given_changes:
if change["ResourceChange"]["LogicalResourceId"] == logical_resource_id:
sorted_changes.append(change)
break
assert len(sorted_changes) > 0
if reverse:
sorted_changes = sorted_changes[::-1]
return sorted_changes
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
import traceback
import uuid
from abc import ABC, abstractmethod
from collections import OrderedDict
from typing import Literal, Optional, TypedDict
from typing import Optional

from localstack import config
from localstack.aws.connect import connect_to
Expand All @@ -16,9 +15,14 @@
get_action_name_for_resource_change,
remove_none_values,
)
from localstack.services.cloudformation.engine.changes import ChangeConfig, ResourceChange
from localstack.services.cloudformation.engine.entities import Stack, StackChangeSet
from localstack.services.cloudformation.engine.parameters import StackParameter
from localstack.services.cloudformation.engine.quirks import VALID_GETATT_PROPERTIES
from localstack.services.cloudformation.engine.resource_ordering import (
order_changes,
order_resources,
)
from localstack.services.cloudformation.engine.template_utils import (
AWS_URL_SUFFIX,
fn_equals_type_conversion,
Expand Down Expand Up @@ -63,15 +67,6 @@ class NoStackUpdates(Exception):
pass


class NoResourceInStack(ValueError):
"""Raised when we preprocess the template and do not find a resource"""

def __init__(self, logical_resource_id: str):
msg = f"Template format error: Unresolved resource dependencies [{logical_resource_id}] in the Resources block of the template"

super().__init__(msg)


# ---------------------
# CF TEMPLATE HANDLING
# ---------------------
Expand Down Expand Up @@ -784,119 +779,6 @@ def evaluate_resource_condition(conditions: dict[str, bool], resource: dict) ->
# -----------------------


Action = str


class ResourceChange(TypedDict):
Action: Action
LogicalResourceId: str
PhysicalResourceId: Optional[str]
ResourceType: str
Scope: list
Details: list
Replacement: Optional[Literal["False"]]


class ChangeConfig(TypedDict):
Type: str
ResourceChange: ResourceChange


def order_resources(
resources: dict,
resolved_parameters: dict[str, StackParameter],
resolved_conditions: dict[str, bool],
reverse: bool = False,
) -> OrderedDict:
"""
Given a dictionary of resources, topologically sort the resources based on
inter-resource dependencies (e.g. usages of intrinsic functions).
"""
nodes: dict[str, list[str]] = {}
for logical_resource_id, properties in resources.items():
nodes.setdefault(logical_resource_id, [])
deps = get_deps_for_resource(properties, resolved_conditions)
for dep in deps:
if dep in resolved_parameters:
# we only care about other resources
continue
nodes.setdefault(dep, [])
nodes[dep].append(logical_resource_id)

# implementation from https://dev.to/leopfeiffer/topological-sort-with-kahns-algorithm-3dl1
indegrees = {k: 0 for k in nodes.keys()}
for dependencies in nodes.values():
for dependency in dependencies:
indegrees[dependency] += 1

# Place all elements with indegree 0 in queue
queue = [k for k in nodes.keys() if indegrees[k] == 0]

sorted_logical_resource_ids = []

# Continue until all nodes have been dealt with
while len(queue) > 0:
# node of current iteration is the first one from the queue
curr = queue.pop(0)
sorted_logical_resource_ids.append(curr)

# remove the current node from other dependencies
for dependency in nodes[curr]:
indegrees[dependency] -= 1

if indegrees[dependency] == 0:
queue.append(dependency)

# check for circular dependencies
if len(sorted_logical_resource_ids) != len(nodes):
raise Exception("Circular dependency found.")

sorted_mapping = []
for logical_resource_id in sorted_logical_resource_ids:
if properties := resources.get(logical_resource_id):
sorted_mapping.append((logical_resource_id, properties))
else:
if (
logical_resource_id not in resolved_parameters
and logical_resource_id not in resolved_conditions
):
raise NoResourceInStack(logical_resource_id)

if reverse:
sorted_mapping = sorted_mapping[::-1]
return OrderedDict(sorted_mapping)


def order_changes(
given_changes: list[ChangeConfig],
resources: dict,
resolved_parameters: dict[str, StackParameter],
# TODO: remove resolved conditions somehow
resolved_conditions: dict[str, bool],
reverse: bool = False,
) -> list[ChangeConfig]:
"""
Given a list of changes, a dictionary of resources and a dictionary of resolved conditions, topologically sort the
changes based on inter-resource dependencies (e.g. usages of intrinsic functions).
"""
ordered_resources = order_resources(
resources=resources,
resolved_parameters=resolved_parameters,
resolved_conditions=resolved_conditions,
reverse=reverse,
)
sorted_changes = []
for logical_resource_id in ordered_resources.keys():
for change in given_changes:
if change["ResourceChange"]["LogicalResourceId"] == logical_resource_id:
sorted_changes.append(change)
break
assert len(sorted_changes) > 0
if reverse:
sorted_changes = sorted_changes[::-1]
return sorted_changes


class TemplateDeployerBase(ABC):
def __init__(self, account_id: str, region_name: str, stack):
self.stack = stack
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,13 @@
StackSet,
)
from localstack.services.cloudformation.engine.parameters import strip_parameter_type
from localstack.services.cloudformation.engine.template_deployer import (
from localstack.services.cloudformation.engine.resource_ordering import (
NoResourceInStack,
NoStackUpdates,
order_resources,
)
from localstack.services.cloudformation.engine.template_deployer import (
NoStackUpdates,
)
from localstack.services.cloudformation.engine.template_utils import resolve_stack_conditions
from localstack.services.cloudformation.engine.transformers import (
FailedTransformationException,
Expand Down

0 comments on commit 1aa3144

Please sign in to comment.