Skip to content

CFNv2: add validation #12780

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

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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 @@ -7,6 +7,7 @@

from typing_extensions import TypeVar

from localstack.services.cloudformation.engine.validations import ValidationError
from localstack.utils.strings import camel_to_snake_case

T = TypeVar("T")
Expand Down Expand Up @@ -140,6 +141,14 @@ def __str__(self):
def __repr__(self):
return str(self)

def validate(self, template: NodeTemplate):
self._perform_validation(template)
for child in self.get_children():
child.validate(template)

def _perform_validation(self, template: NodeTemplate):
pass


class ChangeSetNode(ChangeSetEntity, abc.ABC): ...

Expand Down Expand Up @@ -384,6 +393,31 @@ def __init__(
self.intrinsic_function = intrinsic_function
self.arguments = arguments

def _perform_validation(self, template: NodeTemplate):
validation_fn = getattr(
self, f"_perform_validation_{self.intrinsic_function.lower()}", None
)
if validation_fn:
validation_fn(template)

def _perform_validation_ref(self, template: NodeTemplate):
target = self.arguments.value
for parameter in template.parameters.parameters:
if target == parameter.name:
return

for condition in template.conditions.conditions:
if target == condition.name:
return

for resource in template.resources.resources:
if target == resource.name:
return

raise ValidationError(
f"Template format error: Unresolved resource dependencies [{target}] in the Resources block of the template"
)


class NodeObject(ChangeSetNode):
bindings: Final[dict[str, ChangeSetEntity]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
from localstack.services.cloudformation.engine.v2.change_set_model_visitor import (
ChangeSetModelVisitor,
)
from localstack.services.cloudformation.engine.validations import ValidationError
from localstack.services.cloudformation.stores import get_cloudformation_store
from localstack.services.cloudformation.v2.entities import ChangeSet
from localstack.utils.aws.arns import get_partition
Expand Down Expand Up @@ -187,7 +188,7 @@ def _get_node_resource_for(
if node_resource.name == resource_name:
self.visit(node_resource)
return node_resource
raise RuntimeError(f"No resource '{resource_name}' was found")
raise ValidationError(f"No resource '{resource_name}' was found")

def _get_node_property_for(
self, property_name: str, node_resource: NodeResource
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from localstack.services.cloudformation.engine.v2.change_set_model import ChangeSetEntity
from localstack.services.cloudformation.engine.v2.change_set_model_preproc import (
ChangeSetModelPreproc,
)


class ChangeSetModelValidator(ChangeSetModelPreproc):
def validate(self):
self.visit(self._node_template)

def visit(self, change_set_entity: ChangeSetEntity):
scope = change_set_entity.scope
if scope in self._processed:
return
change_set_entity.validate(change_set_entity)
27 changes: 16 additions & 11 deletions localstack-core/localstack/services/cloudformation/v2/entities.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from datetime import datetime, timezone
from typing import Optional, TypedDict
from typing import NotRequired, Optional, TypedDict

from localstack.aws.api.cloudformation import (
ChangeSetStatus,
ChangeSetType,
CreateChangeSetInput,
CreateStackInput,
ExecutionStatus,
Output,
Parameter,
Expand All @@ -21,7 +22,6 @@
)
from localstack.services.cloudformation.engine.entities import (
StackIdentifier,
StackTemplate,
)
from localstack.services.cloudformation.engine.v2.change_set_model import (
NodeTemplate,
Expand All @@ -38,7 +38,6 @@ class Stack:
stack_name: str
parameters: list[Parameter]
change_set_id: str | None
change_set_name: str | None
status: StackStatus
status_reason: StackStatusReason | None
stack_id: str
Expand All @@ -56,23 +55,22 @@ def __init__(
self,
account_id: str,
region_name: str,
request_payload: CreateChangeSetInput,
template: StackTemplate | None = None,
request_payload: CreateChangeSetInput | CreateStackInput,
template: dict | None = None,
template_body: str | None = None,
change_set_ids: list[str] | None = None,
):
self.account_id = account_id
self.region_name = region_name
self.template = template
self.template_body = template_body
self.status = StackStatus.CREATE_IN_PROGRESS
self.status_reason = None
self.change_set_ids = change_set_ids or []
self.change_set_ids = []
self.creation_time = datetime.now(tz=timezone.utc)
self.deletion_time = None
self.change_set_id = None

self.stack_name = request_payload["StackName"]
self.change_set_name = request_payload.get("ChangeSetName")
self.parameters = request_payload.get("Parameters", [])
self.stack_id = arns.cloudformation_stack_arn(
self.stack_name,
Expand Down Expand Up @@ -159,7 +157,6 @@ def _store_event(

def describe_details(self) -> ApiStack:
result = {
"ChangeSetId": self.change_set_id,
"CreationTime": self.creation_time,
"DeletionTime": self.deletion_time,
"StackId": self.stack_id,
Expand All @@ -176,6 +173,9 @@ def describe_details(self) -> ApiStack:
"RollbackConfiguration": {},
"Tags": [],
}
if change_set_id := self.change_set_id:
result["ChangeSetId"] = change_set_id

if self.resolved_outputs:
describe_outputs = []
for key, value in self.resolved_outputs.items():
Expand All @@ -191,6 +191,11 @@ def describe_details(self) -> ApiStack:
return result


class ChangeSetRequestPayload(TypedDict, total=False):
ChangeSetName: str
ChangeSetType: NotRequired[ChangeSetType]


class ChangeSet:
change_set_name: str
change_set_id: str
Expand All @@ -203,8 +208,8 @@ class ChangeSet:
def __init__(
self,
stack: Stack,
request_payload: CreateChangeSetInput,
template: StackTemplate | None = None,
request_payload: ChangeSetRequestPayload,
template: dict | None = None,
):
self.stack = stack
self.template = template
Expand Down
Loading
Loading