diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py index f6c48df5aa6f0..8b081dd35b12a 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_describer.py @@ -7,7 +7,6 @@ from localstack.services.cloudformation.engine.v2.change_set_model import ( NodeIntrinsicFunction, NodeResource, - NodeTemplate, PropertiesKey, ) from localstack.services.cloudformation.engine.v2.change_set_model_preproc import ( @@ -16,6 +15,7 @@ PreprocProperties, PreprocResource, ) +from localstack.services.cloudformation.v2.entities import ChangeSet CHANGESET_KNOWN_AFTER_APPLY: Final[str] = "{{changeSet:KNOWN_AFTER_APPLY}}" @@ -26,13 +26,10 @@ class ChangeSetModelDescriber(ChangeSetModelPreproc): def __init__( self, - node_template: NodeTemplate, - before_resolved_resources: dict, + change_set: ChangeSet, include_property_values: bool, ): - super().__init__( - node_template=node_template, before_resolved_resources=before_resolved_resources - ) + super().__init__(change_set=change_set) self._include_property_values = include_property_values self._changes = list() diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py index f78ba81259a28..8941d9e4bc1ea 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_executor.py @@ -38,18 +38,13 @@ class ChangeSetModelExecutorResult: class ChangeSetModelExecutor(ChangeSetModelPreproc): - _change_set: Final[ChangeSet] # TODO: add typing for resolved resources and parameters. resources: Final[dict] outputs: Final[dict] resolved_parameters: Final[dict] def __init__(self, change_set: ChangeSet): - super().__init__( - node_template=change_set.update_graph, - before_resolved_resources=change_set.stack.resolved_resources, - ) - self._change_set = change_set + super().__init__(change_set=change_set) self.resources = dict() self.outputs = dict() self.resolved_parameters = dict() diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py index ad19de83ebc1c..1ab31e15928df 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py @@ -28,6 +28,22 @@ from localstack.services.cloudformation.engine.v2.change_set_model_visitor import ( ChangeSetModelVisitor, ) +from localstack.services.cloudformation.v2.entities import ChangeSet +from localstack.utils.aws.arns import get_partition +from localstack.utils.urls import localstack_host + +_AWS_URL_SUFFIX = localstack_host().host # The value in AWS is "amazonaws.com" + +_PSEUDO_PARAMETERS: Final[set[str]] = { + "AWS::Partition", + "AWS::AccountId", + "AWS::Region", + "AWS::StackName", + "AWS::StackId", + "AWS::URLSuffix", + "AWS::NoValue", + "AWS::NotificationARNs", +} TBefore = TypeVar("TBefore") TAfter = TypeVar("TAfter") @@ -126,13 +142,15 @@ def __eq__(self, other): class ChangeSetModelPreproc(ChangeSetModelVisitor): + _change_set: Final[ChangeSet] _node_template: Final[NodeTemplate] _before_resolved_resources: Final[dict] _processed: dict[Scope, Any] - def __init__(self, node_template: NodeTemplate, before_resolved_resources: dict): - self._node_template = node_template - self._before_resolved_resources = before_resolved_resources + def __init__(self, change_set: ChangeSet): + self._change_set = change_set + self._node_template = change_set.update_graph + self._before_resolved_resources = change_set.stack.resolved_resources self._processed = dict() def process(self) -> None: @@ -157,11 +175,20 @@ def _get_node_property_for( return node_property return None - @staticmethod def _deployed_property_value_of( - resource_logical_id: str, property_name: str, resolved_resources: dict + self, resource_logical_id: str, property_name: str, resolved_resources: dict ) -> Any: # TODO: typing around resolved resources is needed and should be reflected here. + + # Before we can obtain deployed value for a resource, we need to first ensure to + # process the resource if this wasn't processed already. Ideally, values should only + # be accessible through delta objects, to ensure computation is always complete at + # every level. + node_resource = self._get_node_resource_for( + resource_name=resource_logical_id, node_template=self._node_template + ) + self.visit(node_resource) + resolved_resource = resolved_resources.get(resource_logical_id) if resolved_resource is None: raise RuntimeError( @@ -223,7 +250,38 @@ def _resolve_condition(self, logical_id: str) -> PreprocEntityDelta: return condition_delta raise RuntimeError(f"No condition '{logical_id}' was found.") + def _resolve_pseudo_parameter(self, pseudo_parameter_name: str) -> PreprocEntityDelta: + match pseudo_parameter_name: + case "AWS::Partition": + after = get_partition(self._change_set.region_name) + case "AWS::AccountId": + after = self._change_set.stack.account_id + case "AWS::Region": + after = self._change_set.stack.region_name + case "AWS::StackName": + after = self._change_set.stack.stack_name + case "AWS::StackId": + after = self._change_set.stack.stack_id + case "AWS::URLSuffix": + after = _AWS_URL_SUFFIX + case "AWS::NoValue": + # TODO: add support for NoValue, None cannot be used to communicate a Null value in preproc classes. + raise NotImplementedError("The use of AWS:NoValue is currently unsupported") + case "AWS::NotificationARNs": + raise NotImplementedError( + "The use of AWS::NotificationARNs is currently unsupported" + ) + case _: + raise RuntimeError(f"Unknown pseudo parameter value '{pseudo_parameter_name}'") + return PreprocEntityDelta(before=after, after=after) + def _resolve_reference(self, logical_id: str) -> PreprocEntityDelta: + if logical_id in _PSEUDO_PARAMETERS: + pseudo_parameter_delta = self._resolve_pseudo_parameter( + pseudo_parameter_name=logical_id + ) + return pseudo_parameter_delta + node_parameter = self._get_node_parameter_if_exists(parameter_name=logical_id) if isinstance(node_parameter, NodeParameter): parameter_delta = self.visit(node_parameter) diff --git a/localstack-core/localstack/services/cloudformation/v2/entities.py b/localstack-core/localstack/services/cloudformation/v2/entities.py index 31de16b69613e..da7a5e311afda 100644 --- a/localstack-core/localstack/services/cloudformation/v2/entities.py +++ b/localstack-core/localstack/services/cloudformation/v2/entities.py @@ -2,11 +2,9 @@ from typing import TypedDict from localstack.aws.api.cloudformation import ( - Changes, ChangeSetStatus, ChangeSetType, CreateChangeSetInput, - DescribeChangeSetOutput, ExecutionStatus, Output, Parameter, @@ -26,9 +24,6 @@ ChangeSetModel, NodeTemplate, ) -from localstack.services.cloudformation.engine.v2.change_set_model_describer import ( - ChangeSetModelDescriber, -) from localstack.utils.aws import arns from localstack.utils.strings import short_uid @@ -187,35 +182,3 @@ def populate_update_graph( after_parameters=after_parameters, ) self.update_graph = change_set_model.get_update_model() - - def describe_details(self, include_property_values: bool) -> DescribeChangeSetOutput: - change_set_describer = ChangeSetModelDescriber( - node_template=self.update_graph, - before_resolved_resources=self.stack.resolved_resources, - include_property_values=include_property_values, - ) - changes: Changes = change_set_describer.get_changes() - - result = { - "Status": self.status, - "ChangeSetType": self.change_set_type, - "ChangeSetId": self.change_set_id, - "ChangeSetName": self.change_set_name, - "ExecutionStatus": self.execution_status, - "RollbackConfiguration": {}, - "StackId": self.stack.stack_id, - "StackName": self.stack.stack_name, - "StackStatus": self.stack.status, - "CreationTime": self.creation_time, - "LastUpdatedTime": "", - "DisableRollback": "", - "EnableTerminationProtection": "", - "Transform": "", - # TODO: mask no echo - "Parameters": [ - Parameter(ParameterKey=key, ParameterValue=value) - for (key, value) in self.stack.resolved_parameters.items() - ], - "Changes": changes, - } - return result diff --git a/localstack-core/localstack/services/cloudformation/v2/provider.py b/localstack-core/localstack/services/cloudformation/v2/provider.py index a738adf6f29cc..98fe866b687c4 100644 --- a/localstack-core/localstack/services/cloudformation/v2/provider.py +++ b/localstack-core/localstack/services/cloudformation/v2/provider.py @@ -3,6 +3,7 @@ from localstack.aws.api import RequestContext, handler from localstack.aws.api.cloudformation import ( + Changes, ChangeSetNameOrId, ChangeSetNotFoundException, ChangeSetStatus, @@ -24,12 +25,16 @@ RetainExceptOnCreate, RetainResources, RoleARN, + RollbackConfiguration, StackName, StackNameOrId, StackStatus, ) from localstack.services.cloudformation import api_utils from localstack.services.cloudformation.engine import template_preparer +from localstack.services.cloudformation.engine.v2.change_set_model_describer import ( + ChangeSetModelDescriber, +) from localstack.services.cloudformation.engine.v2.change_set_model_executor import ( ChangeSetModelExecutor, ) @@ -296,6 +301,32 @@ def _run(*args): return ExecuteChangeSetOutput() + def _describe_change_set( + self, change_set: ChangeSet, include_property_values: bool + ) -> DescribeChangeSetOutput: + change_set_describer = ChangeSetModelDescriber( + change_set=change_set, include_property_values=include_property_values + ) + changes: Changes = change_set_describer.get_changes() + + result = DescribeChangeSetOutput( + Status=change_set.status, + ChangeSetId=change_set.change_set_id, + ChangeSetName=change_set.change_set_name, + ExecutionStatus=change_set.execution_status, + RollbackConfiguration=RollbackConfiguration(), + StackId=change_set.stack.stack_id, + StackName=change_set.stack.stack_name, + CreationTime=change_set.creation_time, + Parameters=[ + # TODO: add masking support. + Parameter(ParameterKey=key, ParameterValue=value) + for (key, value) in change_set.stack.resolved_parameters.items() + ], + Changes=changes, + ) + return result + @handler("DescribeChangeSet") def describe_change_set( self, @@ -312,9 +343,8 @@ def describe_change_set( change_set = find_change_set_v2(state, change_set_name, stack_name) if not change_set: raise ChangeSetNotFoundException(f"ChangeSet [{change_set_name}] does not exist") - - result = change_set.describe_details( - include_property_values=include_property_values or False + result = self._describe_change_set( + change_set=change_set, include_property_values=include_property_values or False ) return result diff --git a/tests/aws/services/cloudformation/v2/ported_from_v1/resources/test_apigateway.py b/tests/aws/services/cloudformation/v2/ported_from_v1/resources/test_apigateway.py index a156f1b761411..77bc440910ee6 100644 --- a/tests/aws/services/cloudformation/v2/ported_from_v1/resources/test_apigateway.py +++ b/tests/aws/services/cloudformation/v2/ported_from_v1/resources/test_apigateway.py @@ -59,8 +59,8 @@ """ +@pytest.mark.skip(reason="no support for DependsOn") # this is an `only_localstack` test because it makes use of _custom_id_ tag -@pytest.mark.skip(reason="no support for pseudo-parameters") @markers.aws.only_localstack def test_cfn_apigateway_aws_integration(deploy_cfn_template, aws_client): api_name = f"rest-api-{short_uid()}" @@ -143,7 +143,7 @@ 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 pseudo-parameters") +@pytest.mark.skip(reason="No support for DependsOn") @markers.aws.only_localstack def test_url_output(httpserver, deploy_cfn_template): httpserver.expect_request("").respond_with_data(b"", 200) @@ -396,7 +396,6 @@ def test_cfn_apigateway_rest_api(deploy_cfn_template, aws_client): # assert not apis -@pytest.mark.skip(reason="no support for pseudo-parameters") @markers.aws.validated def test_account(deploy_cfn_template, aws_client): stack = deploy_cfn_template( diff --git a/tests/aws/services/cloudformation/v2/test_change_set_ref.py b/tests/aws/services/cloudformation/v2/test_change_set_ref.py index 4ae58d9246c06..94113c52ca781 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_ref.py +++ b/tests/aws/services/cloudformation/v2/test_change_set_ref.py @@ -309,3 +309,40 @@ def test_immutable_property_update_causes_resource_replacement( } } capture_update_process(snapshot, template_1, template_2) + + @markers.aws.validated + def test_supported_pseudo_parameter( + self, + snapshot, + capture_update_process, + ): + topic_name_1 = f"topic-name-1-{long_uid()}" + snapshot.add_transformer(RegexTransformer(topic_name_1, "topic_name_1")) + topic_name_2 = f"topic-name-2-{long_uid()}" + snapshot.add_transformer(RegexTransformer(topic_name_2, "topic_name_2")) + snapshot.add_transformer(RegexTransformer("amazonaws.com", "url_suffix")) + snapshot.add_transformer(RegexTransformer("localhost.localstack.cloud", "url_suffix")) + template_1 = { + "Resources": { + "Topic1": {"Type": "AWS::SNS::Topic", "Properties": {"TopicName": topic_name_1}}, + } + } + template_2 = { + "Resources": { + "Topic2": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": topic_name_2, + "Tags": [ + {"Key": "Partition", "Value": {"Ref": "AWS::Partition"}}, + {"Key": "AccountId", "Value": {"Ref": "AWS::AccountId"}}, + {"Key": "Region", "Value": {"Ref": "AWS::Region"}}, + {"Key": "StackName", "Value": {"Ref": "AWS::StackName"}}, + {"Key": "StackId", "Value": {"Ref": "AWS::StackId"}}, + {"Key": "URLSuffix", "Value": {"Ref": "AWS::URLSuffix"}}, + ], + }, + }, + } + } + capture_update_process(snapshot, template_1, template_2) diff --git a/tests/aws/services/cloudformation/v2/test_change_set_ref.snapshot.json b/tests/aws/services/cloudformation/v2/test_change_set_ref.snapshot.json index 88caebb48be79..d6aac38ddd772 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_ref.snapshot.json +++ b/tests/aws/services/cloudformation/v2/test_change_set_ref.snapshot.json @@ -2440,5 +2440,515 @@ "Tags": [] } } + }, + "tests/aws/services/cloudformation/v2/test_change_set_ref.py::TestChangeSetRef::test_supported_pseudo_parameter": { + "recorded-date": "19-05-2025, 10:22:18", + "recorded-content": { + "create-change-set-1": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "topic_name_1" + } + }, + "Details": [], + "LogicalResourceId": "Topic1", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic1", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-1-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] + }, + "create-change-set-2": { + "Id": "arn::cloudformation::111111111111:changeSet/", + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2-prop-values": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Remove", + "BeforeContext": { + "Properties": { + "TopicName": "topic_name_1" + } + }, + "Details": [], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic_name_1", + "PolicyAction": "Delete", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Tags": [ + { + "Value": "aws", + "Key": "Partition" + }, + { + "Value": "111111111111", + "Key": "AccountId" + }, + { + "Value": "", + "Key": "Region" + }, + { + "Value": "", + "Key": "StackName" + }, + { + "Value": "arn::cloudformation::111111111111:stack//", + "Key": "StackId" + }, + { + "Value": "url_suffix", + "Key": "URLSuffix" + } + ], + "TopicName": "topic_name_2" + } + }, + "Details": [], + "LogicalResourceId": "Topic2", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Remove", + "Details": [], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic_name_1", + "PolicyAction": "Delete", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Topic2", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execute-change-set-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "post-create-2-describe": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Topic1": [ + { + "EventId": "Topic1-388a0db5-23ea-4093-b725-5ad4b7b70281", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic_name_1", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-10fea0c1-3d62-4fef-966e-6367dc235129", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic_name_1", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic_name_1", + "ResourceProperties": { + "TopicName": "topic_name_1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic_name_1", + "ResourceProperties": { + "TopicName": "topic_name_1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "", + "ResourceProperties": { + "TopicName": "topic_name_1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Topic2": [ + { + "EventId": "Topic2-CREATE_COMPLETE-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic_name_2", + "ResourceProperties": { + "Tags": [ + { + "Value": "aws", + "Key": "Partition" + }, + { + "Value": "111111111111", + "Key": "AccountId" + }, + { + "Value": "", + "Key": "Region" + }, + { + "Value": "", + "Key": "StackName" + }, + { + "Value": "arn::cloudformation::111111111111:stack//", + "Key": "StackId" + }, + { + "Value": "url_suffix", + "Key": "URLSuffix" + } + ], + "TopicName": "topic_name_2" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "arn::sns::111111111111:topic_name_2", + "ResourceProperties": { + "Tags": [ + { + "Value": "aws", + "Key": "Partition" + }, + { + "Value": "111111111111", + "Key": "AccountId" + }, + { + "Value": "", + "Key": "Region" + }, + { + "Value": "", + "Key": "StackName" + }, + { + "Value": "arn::cloudformation::111111111111:stack//", + "Key": "StackId" + }, + { + "Value": "url_suffix", + "Key": "URLSuffix" + } + ], + "TopicName": "topic_name_2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic2", + "PhysicalResourceId": "", + "ResourceProperties": { + "Tags": [ + { + "Value": "aws", + "Key": "Partition" + }, + { + "Value": "111111111111", + "Key": "AccountId" + }, + { + "Value": "", + "Key": "Region" + }, + { + "Value": "", + "Key": "StackName" + }, + { + "Value": "arn::cloudformation::111111111111:stack//", + "Key": "StackId" + }, + { + "Value": "url_suffix", + "Key": "URLSuffix" + } + ], + "TopicName": "topic_name_2" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "": [ + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "", + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceStatusReason": "User Initiated", + "ResourceType": "AWS::CloudFormation::Stack", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ] + }, + "delete-describe": { + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } } } diff --git a/tests/aws/services/cloudformation/v2/test_change_set_ref.validation.json b/tests/aws/services/cloudformation/v2/test_change_set_ref.validation.json index b211c5f80a703..1667558f83add 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_ref.validation.json +++ b/tests/aws/services/cloudformation/v2/test_change_set_ref.validation.json @@ -13,5 +13,8 @@ }, "tests/aws/services/cloudformation/v2/test_change_set_ref.py::TestChangeSetRef::test_resource_addition": { "last_validated_date": "2025-04-08T15:22:37+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_ref.py::TestChangeSetRef::test_supported_pseudo_parameter": { + "last_validated_date": "2025-05-19T10:22:18+00:00" } }