From 1834d5535cd076929342c5992116f7414491c185 Mon Sep 17 00:00:00 2001 From: Cristopher Pinzon Date: Thu, 29 May 2025 13:26:56 -0500 Subject: [PATCH 01/22] add test for fn::include in change sets --- .../v2/test_change_set_fn_transform.py | 81 ++ ...test_change_set_fn_transform.snapshot.json | 908 ++++++++++++++++++ ...st_change_set_fn_transform.validation.json | 8 + 3 files changed, 997 insertions(+) create mode 100644 tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py create mode 100644 tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json create mode 100644 tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py new file mode 100644 index 0000000000000..181dfd22a3f5e --- /dev/null +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py @@ -0,0 +1,81 @@ +import pytest +from localstack_snapshot.snapshots.transformer import RegexTransformer + +from localstack.services.cloudformation.v2.utils import is_v2_engine +from localstack.testing.aws.util import is_aws_cloud +from localstack.testing.pytest import markers +from localstack.utils.strings import long_uid + + +@pytest.mark.skipif( + condition=not is_v2_engine() and not is_aws_cloud(), reason="Requires the V2 engine" +) +@markers.snapshot.skip_snapshot_verify( + paths=[ + "per-resource-events..*", + "delete-describe..*", + # + # Before/After Context + "$..Capabilities", + "$..NotificationARNs", + "$..IncludeNestedStacks", + "$..Scope", + "$..Details", + "$..Parameters", + "$..Replacement", + "$..PolicyAction", + ] +) +class TestChangeSetFnTransform: + @markers.aws.validated + @pytest.mark.parametrize("include_format", ["yml", "json"]) + def test_embedded_fn_transform_include( + self, include_format, snapshot, capture_update_process, s3_bucket, aws_client, tmp_path + ): + name1 = f"topic-name-1-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + + bucket = s3_bucket + file = tmp_path / "bucket_definition.yml" + + if include_format == "json": + template = '{"Topic2":{"Type":"AWS::SNS::Topic","Properties":{"TopicName": "topic-2"}}}' + else: + template = """ + Topic2: + Type: AWS::SNS::Topic + Properties: + TopicName: topic-2 + """ + + file.write_text(data=template) + aws_client.s3.upload_file( + Bucket=bucket, + Key="template", + Filename=str(file.absolute()), + ) + + template_1 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1, "DisplayName": "display-value-1"}, + }, + } + } + template_2 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1, + "DisplayName": {"Fn::Sub": "The stack name is ${AWS::StackName}"}, + }, + }, + "Fn::Transform": { + "Name": "AWS::Include", + "Parameters": {"Location": f"s3://{bucket}/template"}, + }, + }, + } + capture_update_process(snapshot, template_1, template_2) diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json new file mode 100644 index 0000000000000..65c350ec93be6 --- /dev/null +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json @@ -0,0 +1,908 @@ +{ + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_fn_transform_include[yml]": { + "recorded-date": "29-05-2025, 18:20:46", + "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": { + "DisplayName": "display-value-1", + "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": "Modify", + "AfterContext": { + "Properties": { + "DisplayName": "The stack name is ", + "TopicName": "topic-name-1" + } + }, + "BeforeContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "The stack name is ", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "display-value-1", + "Name": "DisplayName", + "Path": "/Properties/DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "" + } + }, + "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": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "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-UPDATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "The stack name is ", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "The stack name is ", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_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": { + "DisplayName": "display-value-1", + "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": { + "DisplayName": "display-value-1", + "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": { + "DisplayName": "display-value-1", + "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:", + "ResourceProperties": { + "TopicName": "" + }, + "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:", + "ResourceProperties": { + "TopicName": "" + }, + "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": { + "TopicName": "" + }, + "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": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_fn_transform_include[json]": { + "recorded-date": "29-05-2025, 18: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": { + "DisplayName": "display-value-1", + "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": "Modify", + "AfterContext": { + "Properties": { + "DisplayName": "The stack name is ", + "TopicName": "topic-name-1" + } + }, + "BeforeContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "The stack name is ", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "display-value-1", + "Name": "DisplayName", + "Path": "/Properties/DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "TopicName": "" + } + }, + "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": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "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-UPDATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "The stack name is ", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "The stack name is ", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_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": { + "DisplayName": "display-value-1", + "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": { + "DisplayName": "display-value-1", + "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": { + "DisplayName": "display-value-1", + "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:", + "ResourceProperties": { + "TopicName": "" + }, + "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:", + "ResourceProperties": { + "TopicName": "" + }, + "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": { + "TopicName": "" + }, + "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_fn_transform.validation.json b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json new file mode 100644 index 0000000000000..c5f1a4b1df882 --- /dev/null +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json @@ -0,0 +1,8 @@ +{ + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_fn_transform_include[json]": { + "last_validated_date": "2025-05-29T18:22:17+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_fn_transform_include[yml]": { + "last_validated_date": "2025-05-29T18:20:45+00:00" + } +} From 4329084eadcfbbb619986d53a1305b883527b502 Mon Sep 17 00:00:00 2001 From: Cristopher Pinzon Date: Thu, 29 May 2025 13:56:37 -0500 Subject: [PATCH 02/22] add test for global transformationss --- .../v2/test_change_set_fn_transform.py | 53 ++ ...test_change_set_fn_transform.snapshot.json | 796 ++++++++++++++++++ ...st_change_set_fn_transform.validation.json | 6 + 3 files changed, 855 insertions(+) diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py index 181dfd22a3f5e..d4c7ffd8a8058 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py @@ -79,3 +79,56 @@ def test_embedded_fn_transform_include( }, } capture_update_process(snapshot, template_1, template_2) + + @markers.aws.validated + @pytest.mark.parametrize("include_format", ["yml", "json"]) + def test_global_fn_transform_include( + self, include_format, snapshot, capture_update_process, s3_bucket, aws_client, tmp_path + ): + name1 = f"topic-name-1-{long_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + + bucket = s3_bucket + file = tmp_path / "bucket_definition.yml" + + if include_format == "json": + template = '{"Outputs":{"TopicRef":{"Value":{"Ref":"Topic1"}}}} ' + else: + template = """ + Outputs: + TopicRef: + Value: + Ref: Topic1 + """ + + file.write_text(data=template) + aws_client.s3.upload_file( + Bucket=bucket, + Key="template", + Filename=str(file.absolute()), + ) + + template_1 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1, "DisplayName": "display-value-1"}, + }, + } + } + template_2 = { + "Transform": { + "Name": "AWS::Include", + "Parameters": {"Location": f"s3://{bucket}/template"}, + }, + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": name1, + "DisplayName": {"Fn::Sub": "The stack name is ${AWS::StackName}"}, + }, + }, + }, + } + capture_update_process(snapshot, template_1, template_2) diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json index 65c350ec93be6..72a8ab6485080 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json @@ -904,5 +904,801 @@ "Tags": [] } } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_fn_transform_include[json]": { + "recorded-date": "29-05-2025, 18:53:46", + "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": { + "DisplayName": "display-value-1", + "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": "Modify", + "AfterContext": { + "Properties": { + "DisplayName": "The stack name is ", + "TopicName": "topic-name-1" + } + }, + "BeforeContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "The stack name is ", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "display-value-1", + "Name": "DisplayName", + "Path": "/Properties/DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "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": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "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": [], + "Outputs": [ + { + "OutputKey": "TopicRef", + "OutputValue": "arn::sns::111111111111:topic-name-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Topic1": [ + { + "EventId": "Topic1-UPDATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "The stack name is ", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "The stack name is ", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_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": { + "DisplayName": "display-value-1", + "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": { + "DisplayName": "display-value-1", + "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": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "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": [], + "Outputs": [ + { + "OutputKey": "TopicRef", + "OutputValue": "arn::sns::111111111111:topic-name-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_fn_transform_include[yml]": { + "recorded-date": "29-05-2025, 18:52:15", + "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": { + "DisplayName": "display-value-1", + "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": "Modify", + "AfterContext": { + "Properties": { + "DisplayName": "The stack name is ", + "TopicName": "topic-name-1" + } + }, + "BeforeContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "The stack name is ", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "display-value-1", + "Name": "DisplayName", + "Path": "/Properties/DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "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": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "DisplayName", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "Replacement": "False", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "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": [], + "Outputs": [ + { + "OutputKey": "TopicRef", + "OutputValue": "arn::sns::111111111111:topic-name-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Topic1": [ + { + "EventId": "Topic1-UPDATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "The stack name is ", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "DisplayName": "The stack name is ", + "TopicName": "topic-name-1" + }, + "ResourceStatus": "UPDATE_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": { + "DisplayName": "display-value-1", + "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": { + "DisplayName": "display-value-1", + "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": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "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": [], + "Outputs": [ + { + "OutputKey": "TopicRef", + "OutputValue": "arn::sns::111111111111:topic-name-1" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } } } diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json index c5f1a4b1df882..a443ccabd1f57 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json @@ -4,5 +4,11 @@ }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_fn_transform_include[yml]": { "last_validated_date": "2025-05-29T18:20:45+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_fn_transform_include[json]": { + "last_validated_date": "2025-05-29T18:53:46+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_fn_transform_include[yml]": { + "last_validated_date": "2025-05-29T18:52:14+00:00" } } From 8bc4b04e2b9bfc44a6e11d3dd6f4c115e44d1082 Mon Sep 17 00:00:00 2001 From: Cristopher Pinzon Date: Fri, 30 May 2025 14:39:37 -0500 Subject: [PATCH 03/22] add tests for macros --- .../testing/pytest/cloudformation/fixtures.py | 10 + .../v2/test_change_set_fn_transform.py | 152 +- ...test_change_set_fn_transform.snapshot.json | 1651 ++++++++++++++++- ...st_change_set_fn_transform.validation.json | 14 +- 4 files changed, 1797 insertions(+), 30 deletions(-) diff --git a/localstack-core/localstack/testing/pytest/cloudformation/fixtures.py b/localstack-core/localstack/testing/pytest/cloudformation/fixtures.py index 745a547f078c3..912612ccdc423 100644 --- a/localstack-core/localstack/testing/pytest/cloudformation/fixtures.py +++ b/localstack-core/localstack/testing/pytest/cloudformation/fixtures.py @@ -131,6 +131,11 @@ def inner( ChangeSetName=change_set_name, TemplateBody=t1, ChangeSetType="CREATE", + Capabilities=[ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND", + ], Parameters=[{"ParameterKey": k, "ParameterValue": v} for (k, v) in p1.items()], ) snapshot.match("create-change-set-1", change_set_details) @@ -189,6 +194,11 @@ def inner( TemplateBody=t2, ChangeSetType="UPDATE", Parameters=[{"ParameterKey": k, "ParameterValue": v} for (k, v) in p2.items()], + Capabilities=[ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND", + ], ) snapshot.match("create-change-set-2", change_set_details) stack_id = change_set_details["StackId"] diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py index d4c7ffd8a8058..09853a49dbae4 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py @@ -1,10 +1,13 @@ +import os + import pytest from localstack_snapshot.snapshots.transformer import RegexTransformer +from localstack.aws.api.lambda_ import Runtime from localstack.services.cloudformation.v2.utils import is_v2_engine from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers -from localstack.utils.strings import long_uid +from localstack.utils.strings import short_uid @pytest.mark.skipif( @@ -27,25 +30,48 @@ ] ) class TestChangeSetFnTransform: + @pytest.fixture(scope="function") + def create_macro(self, aws_client, deploy_cfn_template, create_lambda_function): + def _inner(macro_name, code_path): + func_name = f"test_lambda_{short_uid()}" + create_lambda_function( + func_name=func_name, + handler_file=code_path, + runtime=Runtime.python3_12, + client=aws_client.lambda_, + ) + + deploy_cfn_template( + template_path=os.path.join( + os.path.dirname(__file__), "../../../templates/macro_resource.yml" + ), + parameters={"FunctionName": func_name, "MacroName": macro_name}, + ) + + yield _inner + @markers.aws.validated @pytest.mark.parametrize("include_format", ["yml", "json"]) def test_embedded_fn_transform_include( self, include_format, snapshot, capture_update_process, s3_bucket, aws_client, tmp_path ): - name1 = f"topic-name-1-{long_uid()}" + name1 = f"topic-name-1-{short_uid()}" + name2 = f"topic-name-2-{short_uid()}" snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) bucket = s3_bucket file = tmp_path / "bucket_definition.yml" if include_format == "json": - template = '{"Topic2":{"Type":"AWS::SNS::Topic","Properties":{"TopicName": "topic-2"}}}' + template = ( + '{"Topic2":{"Type":"AWS::SNS::Topic","Properties":{"TopicName": "%s"}}}' % name2 + ) else: - template = """ + template = f""" Topic2: Type: AWS::SNS::Topic Properties: - TopicName: topic-2 + TopicName: {name2} """ file.write_text(data=template) @@ -85,7 +111,7 @@ def test_embedded_fn_transform_include( def test_global_fn_transform_include( self, include_format, snapshot, capture_update_process, s3_bucket, aws_client, tmp_path ): - name1 = f"topic-name-1-{long_uid()}" + name1 = f"topic-name-1-{short_uid()}" snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) bucket = s3_bucket @@ -132,3 +158,117 @@ def test_global_fn_transform_include( }, } capture_update_process(snapshot, template_1, template_2) + + @markers.aws.validated + def test_serverless_fn_transform( + self, snapshot, capture_update_process, s3_bucket, aws_client, tmp_path + ): + name1 = f"topic-name-1-{short_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + + template_1 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1, "DisplayName": "display-value-1"}, + }, + } + } + template_2 = { + "Transform": "AWS::Serverless-2016-10-31", + "Resources": { + "HelloWorldFunction": { + "Type": "AWS::Serverless::Function", + "Properties": { + "Handler": "index.handler", + "Runtime": "nodejs18.x", + "InlineCode": "exports.handler = async (event) => {\n return {\n statusCode: 200,\n body: JSON.stringify({ message: 'Hello from SAM inline function!' })\n };\n};", + "Events": { + "ApiEvent": { + "Type": "Api", + "Properties": {"Path": "/hello", "Method": "get"}, + } + }, + }, + } + }, + } + capture_update_process(snapshot, template_1, template_2) + + @markers.aws.validated + def test_global_macro_fn_transform( + self, + snapshot, + capture_update_process, + create_macro, + ): + name1 = f"topic-name-1-{short_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + + macro_function_path = os.path.join( + os.path.dirname(__file__), "../../../templates/macros/replace_string.py" + ) + macro_name = "Substitution" + create_macro(macro_name, macro_function_path) + + template_1 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1, "DisplayName": "display-value-1"}, + }, + } + } + + template_2 = { + "Parameters": {"Substitution": {"Type": "String", "Default": "SubstitutionDefault"}}, + "Resources": { + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Value": "{Substitution}", "Type": "String"}, + } + }, + "Transform": {"Name": macro_name}, + } + capture_update_process(snapshot, template_1, template_2) + + @markers.aws.validated + def test_embedded_macro_fn_transform( + self, + snapshot, + capture_update_process, + create_macro, + ): + name1 = f"topic-name-1-{short_uid()}" + name2 = f"topic-name-2-{short_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + + macro_function_path = os.path.join( + os.path.dirname(__file__), "../../../templates/macros/add_standard_attributes.py" + ) + macro_name = "MakeFifo" + create_macro(macro_name, macro_function_path) + + template_2 = { + "Resources": { + "Topic1": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name1}, + }, + } + } + + template_1 = { + "Resources": { + "Topic": { + "Type": "AWS::SNS::Topic", + "Properties": {"TopicName": name2, "Fn::Transform": macro_name}, + } + } + } + capture_update_process(snapshot, template_1, template_2) + + # TODO: + # - Attribute with macro + # - Macro with parameters + # - Executing order of transformations diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json index 72a8ab6485080..007c31c38c3db 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_fn_transform_include[yml]": { - "recorded-date": "29-05-2025, 18:20:46", + "recorded-date": "30-05-2025, 19:11:21", "recorded-content": { "create-change-set-1": { "Id": "arn::cloudformation::111111111111:changeSet/", @@ -11,7 +11,11 @@ } }, "describe-change-set-1-prop-values": { - "Capabilities": [], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "ChangeSetName": "", "Changes": [ @@ -46,7 +50,11 @@ } }, "describe-change-set-1": { - "Capabilities": [], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "ChangeSetName": "", "Changes": [ @@ -81,6 +89,11 @@ } }, "post-create-1-describe": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, @@ -105,7 +118,11 @@ } }, "describe-change-set-2-prop-values": { - "Capabilities": [], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "ChangeSetName": "", "Changes": [ @@ -179,7 +196,11 @@ } }, "describe-change-set-2": { - "Capabilities": [], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "ChangeSetName": "", "Changes": [ @@ -238,6 +259,11 @@ } }, "post-create-2-describe": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, @@ -436,6 +462,11 @@ ] }, "delete-describe": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "CreationTime": "datetime", "DeletionTime": "datetime", "DisableRollback": false, @@ -453,7 +484,7 @@ } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_fn_transform_include[json]": { - "recorded-date": "29-05-2025, 18:22:18", + "recorded-date": "30-05-2025, 19:12:51", "recorded-content": { "create-change-set-1": { "Id": "arn::cloudformation::111111111111:changeSet/", @@ -464,7 +495,11 @@ } }, "describe-change-set-1-prop-values": { - "Capabilities": [], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "ChangeSetName": "", "Changes": [ @@ -499,7 +534,11 @@ } }, "describe-change-set-1": { - "Capabilities": [], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "ChangeSetName": "", "Changes": [ @@ -534,6 +573,11 @@ } }, "post-create-1-describe": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, @@ -558,7 +602,11 @@ } }, "describe-change-set-2-prop-values": { - "Capabilities": [], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "ChangeSetName": "", "Changes": [ @@ -632,7 +680,11 @@ } }, "describe-change-set-2": { - "Capabilities": [], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "ChangeSetName": "", "Changes": [ @@ -691,6 +743,11 @@ } }, "post-create-2-describe": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, @@ -889,6 +946,11 @@ ] }, "delete-describe": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "CreationTime": "datetime", "DeletionTime": "datetime", "DisableRollback": false, @@ -906,7 +968,7 @@ } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_fn_transform_include[json]": { - "recorded-date": "29-05-2025, 18:53:46", + "recorded-date": "30-05-2025, 19:15:53", "recorded-content": { "create-change-set-1": { "Id": "arn::cloudformation::111111111111:changeSet/", @@ -917,7 +979,11 @@ } }, "describe-change-set-1-prop-values": { - "Capabilities": [], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "ChangeSetName": "", "Changes": [ @@ -952,7 +1018,11 @@ } }, "describe-change-set-1": { - "Capabilities": [], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "ChangeSetName": "", "Changes": [ @@ -987,6 +1057,11 @@ } }, "post-create-1-describe": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, @@ -1011,7 +1086,11 @@ } }, "describe-change-set-2-prop-values": { - "Capabilities": [], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "ChangeSetName": "", "Changes": [ @@ -1070,7 +1149,11 @@ } }, "describe-change-set-2": { - "Capabilities": [], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "ChangeSetName": "", "Changes": [ @@ -1119,6 +1202,11 @@ } }, "post-create-2-describe": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, @@ -1281,6 +1369,11 @@ ] }, "delete-describe": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "CreationTime": "datetime", "DeletionTime": "datetime", "DisableRollback": false, @@ -1304,7 +1397,7 @@ } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_fn_transform_include[yml]": { - "recorded-date": "29-05-2025, 18:52:15", + "recorded-date": "30-05-2025, 19:14:21", "recorded-content": { "create-change-set-1": { "Id": "arn::cloudformation::111111111111:changeSet/", @@ -1315,7 +1408,11 @@ } }, "describe-change-set-1-prop-values": { - "Capabilities": [], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "ChangeSetName": "", "Changes": [ @@ -1350,7 +1447,11 @@ } }, "describe-change-set-1": { - "Capabilities": [], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "ChangeSetName": "", "Changes": [ @@ -1385,6 +1486,11 @@ } }, "post-create-1-describe": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, @@ -1409,7 +1515,11 @@ } }, "describe-change-set-2-prop-values": { - "Capabilities": [], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "ChangeSetName": "", "Changes": [ @@ -1468,7 +1578,11 @@ } }, "describe-change-set-2": { - "Capabilities": [], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "ChangeSetName": "", "Changes": [ @@ -1517,6 +1631,11 @@ } }, "post-create-2-describe": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "CreationTime": "datetime", "DisableRollback": false, @@ -1679,6 +1798,11 @@ ] }, "delete-describe": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], "CreationTime": "datetime", "DeletionTime": "datetime", "DisableRollback": false, @@ -1700,5 +1824,1492 @@ "Tags": [] } } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_serverless_fn_transform": { + "recorded-date": "30-05-2025, 19:17:58", + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-1", + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Role": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Handler": "index.handler", + "Runtime": "nodejs18.x", + "Code": { + "ZipFile": "exports.handler = async (event) => {\n return {\n statusCode: 200,\n body: JSON.stringify({ message: 'Hello from SAM inline function!' })\n };\n};" + }, + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + }, + "Details": [], + "LogicalResourceId": "HelloWorldFunction", + "ResourceType": "AWS::Lambda::Function", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "FunctionName": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Action": "lambda:InvokeFunction", + "SourceArn": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Principal": "apigateway.amazonaws.com" + } + }, + "Details": [], + "LogicalResourceId": "HelloWorldFunctionApiEventPermissionProd", + "ResourceType": "AWS::Lambda::Permission", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "ManagedPolicyArns": [ + "arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + }, + "Details": [], + "LogicalResourceId": "HelloWorldFunctionRole", + "ResourceType": "AWS::IAM::Role", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Body": { + "paths": { + "/": { + "get": { + "responses": {}, + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "arn::apigateway::lambda:path/2015-03-31/functions/{{changeSet:KNOWN_AFTER_APPLY}}/invocations" + } + } + } + }, + "swagger": "2.0", + "info": { + "title": "", + "version": "1.0" + } + } + } + }, + "Details": [], + "LogicalResourceId": "ServerlessRestApi", + "ResourceType": "AWS::ApiGateway::RestApi", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "RestApiId": "{{changeSet:KNOWN_AFTER_APPLY}}", + "Description": "RestApi deployment id: 47fc2d5f9d21ad56f83937abe2779d0e26d7095e", + "StageName": "Stage" + } + }, + "Details": [], + "LogicalResourceId": "ServerlessRestApiDeployment47fc2d5f9d", + "ResourceType": "AWS::ApiGateway::Deployment", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "RestApiId": "{{changeSet:KNOWN_AFTER_APPLY}}", + "DeploymentId": "{{changeSet:KNOWN_AFTER_APPLY}}", + "StageName": "Prod" + } + }, + "Details": [], + "LogicalResourceId": "ServerlessRestApiProdStage", + "ResourceType": "AWS::ApiGateway::Stage", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Remove", + "BeforeContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "Delete", + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "HelloWorldFunction", + "ResourceType": "AWS::Lambda::Function", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "HelloWorldFunctionApiEventPermissionProd", + "ResourceType": "AWS::Lambda::Permission", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "HelloWorldFunctionRole", + "ResourceType": "AWS::IAM::Role", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "ServerlessRestApi", + "ResourceType": "AWS::ApiGateway::RestApi", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "ServerlessRestApiDeployment47fc2d5f9d", + "ResourceType": "AWS::ApiGateway::Deployment", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "ServerlessRestApiProdStage", + "ResourceType": "AWS::ApiGateway::Stage", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Remove", + "Details": [], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "Delete", + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "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": { + "HelloWorldFunction": [ + { + "EventId": "HelloWorldFunction-CREATE_COMPLETE-date", + "LogicalResourceId": "HelloWorldFunction", + "PhysicalResourceId": "-HelloWorldFunction-rHxmhaPeJGt1", + "ResourceProperties": { + "Role": "arn::iam::111111111111:role/", + "Runtime": "nodejs18.x", + "Handler": "index.handler", + "Code": { + "ZipFile": "exports.handler = async (event) => {\n return {\n statusCode: 200,\n body: JSON.stringify({ message: 'Hello from SAM inline function!' })\n };\n};" + }, + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::Lambda::Function", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "HelloWorldFunction-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "HelloWorldFunction", + "PhysicalResourceId": "-HelloWorldFunction-rHxmhaPeJGt1", + "ResourceProperties": { + "Role": "arn::iam::111111111111:role/", + "Runtime": "nodejs18.x", + "Handler": "index.handler", + "Code": { + "ZipFile": "exports.handler = async (event) => {\n return {\n statusCode: 200,\n body: JSON.stringify({ message: 'Hello from SAM inline function!' })\n };\n};" + }, + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::Lambda::Function", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "HelloWorldFunction-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "HelloWorldFunction", + "PhysicalResourceId": "", + "ResourceProperties": { + "Role": "arn::iam::111111111111:role/", + "Runtime": "nodejs18.x", + "Handler": "index.handler", + "Code": { + "ZipFile": "exports.handler = async (event) => {\n return {\n statusCode: 200,\n body: JSON.stringify({ message: 'Hello from SAM inline function!' })\n };\n};" + }, + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::Lambda::Function", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "HelloWorldFunctionApiEventPermissionProd": [ + { + "EventId": "HelloWorldFunctionApiEventPermissionProd-CREATE_COMPLETE-date", + "LogicalResourceId": "HelloWorldFunctionApiEventPermissionProd", + "PhysicalResourceId": "-HelloWorldFunctionApiEventPermissionProd-He3072PnKtRX", + "ResourceProperties": { + "FunctionName": "-HelloWorldFunction-rHxmhaPeJGt1", + "Action": "lambda:InvokeFunction", + "SourceArn": "arn::execute-api::111111111111:yn2f9dmisl/*/GET/", + "Principal": "apigateway.amazonaws.com" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::Lambda::Permission", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "HelloWorldFunctionApiEventPermissionProd-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "HelloWorldFunctionApiEventPermissionProd", + "PhysicalResourceId": "-HelloWorldFunctionApiEventPermissionProd-He3072PnKtRX", + "ResourceProperties": { + "FunctionName": "-HelloWorldFunction-rHxmhaPeJGt1", + "Action": "lambda:InvokeFunction", + "SourceArn": "arn::execute-api::111111111111:yn2f9dmisl/*/GET/", + "Principal": "apigateway.amazonaws.com" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::Lambda::Permission", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "HelloWorldFunctionApiEventPermissionProd-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "HelloWorldFunctionApiEventPermissionProd", + "PhysicalResourceId": "", + "ResourceProperties": { + "FunctionName": "-HelloWorldFunction-rHxmhaPeJGt1", + "Action": "lambda:InvokeFunction", + "SourceArn": "arn::execute-api::111111111111:yn2f9dmisl/*/GET/", + "Principal": "apigateway.amazonaws.com" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::Lambda::Permission", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "HelloWorldFunctionRole": [ + { + "EventId": "HelloWorldFunctionRole-CREATE_COMPLETE-date", + "LogicalResourceId": "HelloWorldFunctionRole", + "PhysicalResourceId": "", + "ResourceProperties": { + "ManagedPolicyArns": [ + "arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::IAM::Role", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "HelloWorldFunctionRole-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "HelloWorldFunctionRole", + "PhysicalResourceId": "", + "ResourceProperties": { + "ManagedPolicyArns": [ + "arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::IAM::Role", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "HelloWorldFunctionRole-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "HelloWorldFunctionRole", + "PhysicalResourceId": "", + "ResourceProperties": { + "ManagedPolicyArns": [ + "arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::IAM::Role", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "ServerlessRestApi": [ + { + "EventId": "ServerlessRestApi-CREATE_COMPLETE-date", + "LogicalResourceId": "ServerlessRestApi", + "PhysicalResourceId": "yn2f9dmisl", + "ResourceProperties": { + "Body": { + "paths": { + "/": { + "get": { + "responses": {}, + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "arn::apigateway::lambda:path/2015-03-31/functions/arn::lambda::111111111111:function:-HelloWorldFunction-rHxmhaPeJGt1/invocations" + } + } + } + }, + "swagger": "2.0", + "info": { + "title": "", + "version": "1.0" + } + } + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::ApiGateway::RestApi", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "ServerlessRestApi-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "ServerlessRestApi", + "PhysicalResourceId": "yn2f9dmisl", + "ResourceProperties": { + "Body": { + "paths": { + "/": { + "get": { + "responses": {}, + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "arn::apigateway::lambda:path/2015-03-31/functions/arn::lambda::111111111111:function:-HelloWorldFunction-rHxmhaPeJGt1/invocations" + } + } + } + }, + "swagger": "2.0", + "info": { + "title": "", + "version": "1.0" + } + } + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::ApiGateway::RestApi", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "ServerlessRestApi-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "ServerlessRestApi", + "PhysicalResourceId": "", + "ResourceProperties": { + "Body": { + "paths": { + "/": { + "get": { + "responses": {}, + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "arn::apigateway::lambda:path/2015-03-31/functions/arn::lambda::111111111111:function:-HelloWorldFunction-rHxmhaPeJGt1/invocations" + } + } + } + }, + "swagger": "2.0", + "info": { + "title": "", + "version": "1.0" + } + } + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::ApiGateway::RestApi", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "ServerlessRestApiDeployment47fc2d5f9d": [ + { + "EventId": "ServerlessRestApiDeployment47fc2d5f9d-CREATE_COMPLETE-date", + "LogicalResourceId": "ServerlessRestApiDeployment47fc2d5f9d", + "PhysicalResourceId": "p4f5gj", + "ResourceProperties": { + "Description": "RestApi deployment id: 47fc2d5f9d21ad56f83937abe2779d0e26d7095e", + "StageName": "Stage", + "RestApiId": "yn2f9dmisl" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::ApiGateway::Deployment", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "ServerlessRestApiDeployment47fc2d5f9d-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "ServerlessRestApiDeployment47fc2d5f9d", + "PhysicalResourceId": "p4f5gj", + "ResourceProperties": { + "Description": "RestApi deployment id: 47fc2d5f9d21ad56f83937abe2779d0e26d7095e", + "StageName": "Stage", + "RestApiId": "yn2f9dmisl" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::ApiGateway::Deployment", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "ServerlessRestApiDeployment47fc2d5f9d-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "ServerlessRestApiDeployment47fc2d5f9d", + "PhysicalResourceId": "", + "ResourceProperties": { + "Description": "RestApi deployment id: 47fc2d5f9d21ad56f83937abe2779d0e26d7095e", + "StageName": "Stage", + "RestApiId": "yn2f9dmisl" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::ApiGateway::Deployment", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "ServerlessRestApiProdStage": [ + { + "EventId": "ServerlessRestApiProdStage-CREATE_COMPLETE-date", + "LogicalResourceId": "ServerlessRestApiProdStage", + "PhysicalResourceId": "Prod", + "ResourceProperties": { + "DeploymentId": "p4f5gj", + "StageName": "Prod", + "RestApiId": "yn2f9dmisl" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::ApiGateway::Stage", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "ServerlessRestApiProdStage-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "ServerlessRestApiProdStage", + "PhysicalResourceId": "Prod", + "ResourceProperties": { + "DeploymentId": "p4f5gj", + "StageName": "Prod", + "RestApiId": "yn2f9dmisl" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::ApiGateway::Stage", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "ServerlessRestApiProdStage-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "ServerlessRestApiProdStage", + "PhysicalResourceId": "", + "ResourceProperties": { + "DeploymentId": "p4f5gj", + "StageName": "Prod", + "RestApiId": "yn2f9dmisl" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::ApiGateway::Stage", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Topic1": [ + { + "EventId": "Topic1-bc7f9092-e33e-4b75-9ab9-0b05a1eca865", + "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-53c44796-6014-4724-bf4e-398b5792343b", + "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": { + "DisplayName": "display-value-1", + "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": { + "DisplayName": "display-value-1", + "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": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_macro_fn_transform": { + "recorded-date": "30-05-2025, 19:19:36", + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "DisplayName": "display-value-1", + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "{Substitution}", + "Type": "String" + } + }, + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Remove", + "BeforeContext": { + "Properties": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "Delete", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Substitution", + "ParameterValue": "SubstitutionDefault" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + }, + { + "ResourceChange": { + "Action": "Remove", + "Details": [], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "Delete", + "ResourceType": "AWS::SNS::Topic", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Substitution", + "ParameterValue": "SubstitutionDefault" + } + ], + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Substitution", + "ParameterValue": "SubstitutionDefault" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Parameter": [ + { + "EventId": "Parameter-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-RKPmUsCVodiF", + "ResourceProperties": { + "Type": "String", + "Value": "{Substitution}" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "CFN-Parameter-RKPmUsCVodiF", + "ResourceProperties": { + "Type": "String", + "Value": "{Substitution}" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "{Substitution}" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Topic1": [ + { + "EventId": "Topic1-d999817d-18b1-45e2-b4f2-ff6a275ad13f", + "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-ad6d008a-83b5-44f0-b20a-6f5106380a58", + "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": { + "DisplayName": "display-value-1", + "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": { + "DisplayName": "display-value-1", + "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": { + "DisplayName": "display-value-1", + "TopicName": "topic-name-1" + }, + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Substitution", + "ParameterValue": "SubstitutionDefault" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } } } diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json index a443ccabd1f57..271f9a582253a 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json @@ -1,14 +1,20 @@ { "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_fn_transform_include[json]": { - "last_validated_date": "2025-05-29T18:22:17+00:00" + "last_validated_date": "2025-05-30T19:12:50+00:00" }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_fn_transform_include[yml]": { - "last_validated_date": "2025-05-29T18:20:45+00:00" + "last_validated_date": "2025-05-30T19:11:21+00:00" }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_fn_transform_include[json]": { - "last_validated_date": "2025-05-29T18:53:46+00:00" + "last_validated_date": "2025-05-30T19:15:52+00:00" }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_fn_transform_include[yml]": { - "last_validated_date": "2025-05-29T18:52:14+00:00" + "last_validated_date": "2025-05-30T19:14:20+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_macro_fn_transform": { + "last_validated_date": "2025-05-30T19:19:31+00:00" + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_serverless_fn_transform": { + "last_validated_date": "2025-05-30T19:17:57+00:00" } } From d5ee2a7a54c4c5032fe4b58bfc6bb339a557f4b2 Mon Sep 17 00:00:00 2001 From: Cristopher Pinzon Date: Mon, 2 Jun 2025 15:37:15 -0500 Subject: [PATCH 04/22] more transformation tests --- .../v2/test_change_set_fn_transform.py | 110 +- ...test_change_set_fn_transform.snapshot.json | 1446 ++++++++++++++++- ...st_change_set_fn_transform.validation.json | 75 +- 3 files changed, 1579 insertions(+), 52 deletions(-) diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py index 09853a49dbae4..31bbbc2a26e4b 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py @@ -240,7 +240,7 @@ def test_embedded_macro_fn_transform( create_macro, ): name1 = f"topic-name-1-{short_uid()}" - name2 = f"topic-name-2-{short_uid()}" + name2 = f"topic-name-2-{short_uid()}.fifo" snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) macro_function_path = os.path.join( @@ -249,7 +249,7 @@ def test_embedded_macro_fn_transform( macro_name = "MakeFifo" create_macro(macro_name, macro_function_path) - template_2 = { + template_1 = { "Resources": { "Topic1": { "Type": "AWS::SNS::Topic", @@ -258,9 +258,9 @@ def test_embedded_macro_fn_transform( } } - template_1 = { + template_2 = { "Resources": { - "Topic": { + "Topic1": { "Type": "AWS::SNS::Topic", "Properties": {"TopicName": name2, "Fn::Transform": macro_name}, } @@ -268,7 +268,101 @@ def test_embedded_macro_fn_transform( } capture_update_process(snapshot, template_1, template_2) - # TODO: - # - Attribute with macro - # - Macro with parameters - # - Executing order of transformations + @markers.aws.validated + def test_embedded_macro_for_attribute_fn_transform( + self, + snapshot, + capture_update_process, + create_macro, + ): + name1 = f"parameter-{short_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "parameter-name")) + snapshot.add_transformer(snapshot.transform.key_value("Value", "value")) + + macro_function_path = os.path.join( + os.path.dirname(__file__), "../../../templates/macros/return_random_string.py" + ) + macro_name = "GenerateRandom" + create_macro(macro_name, macro_function_path) + + template_1 = { + "Resources": { + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": name1, "Type": "String", "Value": "foo"}, + } + } + } + + template_2 = { + "Parameters": {"Input": {"Type": "String"}}, + "Resources": { + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": name1, + "Type": "String", + "Value": { + "Fn::Transform": { + "Name": "GenerateRandom", + "Parameters": {"Prefix": {"Ref": "Input"}}, + } + }, + }, + } + }, + } + + capture_update_process(snapshot, template_1, template_2, p2={"Input": "test"}) + + @markers.aws.validated + def test_multiple_fn_transform_order( + self, + snapshot, + capture_update_process, + create_macro, + ): + name1 = f"parameter-{short_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "parameter-name")) + snapshot.add_transformer(snapshot.transform.key_value("Value", "value")) + + macro_function_path = os.path.join( + os.path.dirname(__file__), "../../../templates/macros/replace_string.py" + ) + macro_name = "ReplaceString" + create_macro(macro_name, macro_function_path) + + template_1 = { + "Resources": { + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": name1, "Type": "String", "Value": "foo"}, + } + } + } + + template_2 = { + "Resources": { + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": name1, + "Value": "", + "Type": "String", + "Fn::Transform": [ + {"Name": "ReplaceString", "Parameters": {"Input": "snippet-transform"}}, + { + "Name": "ReplaceString", + "Parameters": {"Input": "second-snippet-transform"}, + }, + ], + }, + } + }, + "Transform": [ + {"Name": "ReplaceString", "Parameters": {"Input": "global-transform"}}, + {"Name": "ReplaceString", "Parameters": {"Input": "second-global-transform"}}, + ], + } + + capture_update_process(snapshot, template_1, template_2) diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json index 007c31c38c3db..da52614a57718 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_fn_transform_include[yml]": { - "recorded-date": "30-05-2025, 19:11:21", + "recorded-date": "02-06-2025, 17:36:48", "recorded-content": { "create-change-set-1": { "Id": "arn::cloudformation::111111111111:changeSet/", @@ -484,7 +484,7 @@ } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_fn_transform_include[json]": { - "recorded-date": "30-05-2025, 19:12:51", + "recorded-date": "02-06-2025, 17:38:17", "recorded-content": { "create-change-set-1": { "Id": "arn::cloudformation::111111111111:changeSet/", @@ -968,7 +968,7 @@ } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_fn_transform_include[json]": { - "recorded-date": "30-05-2025, 19:15:53", + "recorded-date": "02-06-2025, 17:41:20", "recorded-content": { "create-change-set-1": { "Id": "arn::cloudformation::111111111111:changeSet/", @@ -1397,7 +1397,7 @@ } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_fn_transform_include[yml]": { - "recorded-date": "30-05-2025, 19:14:21", + "recorded-date": "02-06-2025, 17:39:48", "recorded-content": { "create-change-set-1": { "Id": "arn::cloudformation::111111111111:changeSet/", @@ -1826,7 +1826,7 @@ } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_serverless_fn_transform": { - "recorded-date": "30-05-2025, 19:17:58", + "recorded-date": "02-06-2025, 17:43:23", "recorded-content": { "create-change-set-1": { "Id": "arn::cloudformation::111111111111:changeSet/", @@ -2261,7 +2261,7 @@ { "EventId": "HelloWorldFunction-CREATE_COMPLETE-date", "LogicalResourceId": "HelloWorldFunction", - "PhysicalResourceId": "-HelloWorldFunction-rHxmhaPeJGt1", + "PhysicalResourceId": "-HelloWorldFunction-3tFROESvgtFK", "ResourceProperties": { "Role": "arn::iam::111111111111:role/", "Runtime": "nodejs18.x", @@ -2285,7 +2285,7 @@ { "EventId": "HelloWorldFunction-CREATE_IN_PROGRESS-date", "LogicalResourceId": "HelloWorldFunction", - "PhysicalResourceId": "-HelloWorldFunction-rHxmhaPeJGt1", + "PhysicalResourceId": "-HelloWorldFunction-3tFROESvgtFK", "ResourceProperties": { "Role": "arn::iam::111111111111:role/", "Runtime": "nodejs18.x", @@ -2336,11 +2336,11 @@ { "EventId": "HelloWorldFunctionApiEventPermissionProd-CREATE_COMPLETE-date", "LogicalResourceId": "HelloWorldFunctionApiEventPermissionProd", - "PhysicalResourceId": "-HelloWorldFunctionApiEventPermissionProd-He3072PnKtRX", + "PhysicalResourceId": "-HelloWorldFunctionApiEventPermissionProd-5fBzi1JcOwIe", "ResourceProperties": { - "FunctionName": "-HelloWorldFunction-rHxmhaPeJGt1", + "FunctionName": "-HelloWorldFunction-3tFROESvgtFK", "Action": "lambda:InvokeFunction", - "SourceArn": "arn::execute-api::111111111111:yn2f9dmisl/*/GET/", + "SourceArn": "arn::execute-api::111111111111:g59lz4kkbg/*/GET/", "Principal": "apigateway.amazonaws.com" }, "ResourceStatus": "CREATE_COMPLETE", @@ -2352,11 +2352,11 @@ { "EventId": "HelloWorldFunctionApiEventPermissionProd-CREATE_IN_PROGRESS-date", "LogicalResourceId": "HelloWorldFunctionApiEventPermissionProd", - "PhysicalResourceId": "-HelloWorldFunctionApiEventPermissionProd-He3072PnKtRX", + "PhysicalResourceId": "-HelloWorldFunctionApiEventPermissionProd-5fBzi1JcOwIe", "ResourceProperties": { - "FunctionName": "-HelloWorldFunction-rHxmhaPeJGt1", + "FunctionName": "-HelloWorldFunction-3tFROESvgtFK", "Action": "lambda:InvokeFunction", - "SourceArn": "arn::execute-api::111111111111:yn2f9dmisl/*/GET/", + "SourceArn": "arn::execute-api::111111111111:g59lz4kkbg/*/GET/", "Principal": "apigateway.amazonaws.com" }, "ResourceStatus": "CREATE_IN_PROGRESS", @@ -2371,9 +2371,9 @@ "LogicalResourceId": "HelloWorldFunctionApiEventPermissionProd", "PhysicalResourceId": "", "ResourceProperties": { - "FunctionName": "-HelloWorldFunction-rHxmhaPeJGt1", + "FunctionName": "-HelloWorldFunction-3tFROESvgtFK", "Action": "lambda:InvokeFunction", - "SourceArn": "arn::execute-api::111111111111:yn2f9dmisl/*/GET/", + "SourceArn": "arn::execute-api::111111111111:g59lz4kkbg/*/GET/", "Principal": "apigateway.amazonaws.com" }, "ResourceStatus": "CREATE_IN_PROGRESS", @@ -2501,7 +2501,7 @@ { "EventId": "ServerlessRestApi-CREATE_COMPLETE-date", "LogicalResourceId": "ServerlessRestApi", - "PhysicalResourceId": "yn2f9dmisl", + "PhysicalResourceId": "g59lz4kkbg", "ResourceProperties": { "Body": { "paths": { @@ -2511,7 +2511,7 @@ "x-amazon-apigateway-integration": { "httpMethod": "POST", "type": "aws_proxy", - "uri": "arn::apigateway::lambda:path/2015-03-31/functions/arn::lambda::111111111111:function:-HelloWorldFunction-rHxmhaPeJGt1/invocations" + "uri": "arn::apigateway::lambda:path/2015-03-31/functions/arn::lambda::111111111111:function:-HelloWorldFunction-3tFROESvgtFK/invocations" } } } @@ -2532,7 +2532,7 @@ { "EventId": "ServerlessRestApi-CREATE_IN_PROGRESS-date", "LogicalResourceId": "ServerlessRestApi", - "PhysicalResourceId": "yn2f9dmisl", + "PhysicalResourceId": "g59lz4kkbg", "ResourceProperties": { "Body": { "paths": { @@ -2542,7 +2542,7 @@ "x-amazon-apigateway-integration": { "httpMethod": "POST", "type": "aws_proxy", - "uri": "arn::apigateway::lambda:path/2015-03-31/functions/arn::lambda::111111111111:function:-HelloWorldFunction-rHxmhaPeJGt1/invocations" + "uri": "arn::apigateway::lambda:path/2015-03-31/functions/arn::lambda::111111111111:function:-HelloWorldFunction-3tFROESvgtFK/invocations" } } } @@ -2574,7 +2574,7 @@ "x-amazon-apigateway-integration": { "httpMethod": "POST", "type": "aws_proxy", - "uri": "arn::apigateway::lambda:path/2015-03-31/functions/arn::lambda::111111111111:function:-HelloWorldFunction-rHxmhaPeJGt1/invocations" + "uri": "arn::apigateway::lambda:path/2015-03-31/functions/arn::lambda::111111111111:function:-HelloWorldFunction-3tFROESvgtFK/invocations" } } } @@ -2597,11 +2597,11 @@ { "EventId": "ServerlessRestApiDeployment47fc2d5f9d-CREATE_COMPLETE-date", "LogicalResourceId": "ServerlessRestApiDeployment47fc2d5f9d", - "PhysicalResourceId": "p4f5gj", + "PhysicalResourceId": "qcrqox", "ResourceProperties": { "Description": "RestApi deployment id: 47fc2d5f9d21ad56f83937abe2779d0e26d7095e", "StageName": "Stage", - "RestApiId": "yn2f9dmisl" + "RestApiId": "g59lz4kkbg" }, "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::ApiGateway::Deployment", @@ -2612,11 +2612,11 @@ { "EventId": "ServerlessRestApiDeployment47fc2d5f9d-CREATE_IN_PROGRESS-date", "LogicalResourceId": "ServerlessRestApiDeployment47fc2d5f9d", - "PhysicalResourceId": "p4f5gj", + "PhysicalResourceId": "qcrqox", "ResourceProperties": { "Description": "RestApi deployment id: 47fc2d5f9d21ad56f83937abe2779d0e26d7095e", "StageName": "Stage", - "RestApiId": "yn2f9dmisl" + "RestApiId": "g59lz4kkbg" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", @@ -2632,7 +2632,7 @@ "ResourceProperties": { "Description": "RestApi deployment id: 47fc2d5f9d21ad56f83937abe2779d0e26d7095e", "StageName": "Stage", - "RestApiId": "yn2f9dmisl" + "RestApiId": "g59lz4kkbg" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceType": "AWS::ApiGateway::Deployment", @@ -2647,9 +2647,9 @@ "LogicalResourceId": "ServerlessRestApiProdStage", "PhysicalResourceId": "Prod", "ResourceProperties": { - "DeploymentId": "p4f5gj", + "DeploymentId": "qcrqox", "StageName": "Prod", - "RestApiId": "yn2f9dmisl" + "RestApiId": "g59lz4kkbg" }, "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::ApiGateway::Stage", @@ -2662,9 +2662,9 @@ "LogicalResourceId": "ServerlessRestApiProdStage", "PhysicalResourceId": "Prod", "ResourceProperties": { - "DeploymentId": "p4f5gj", + "DeploymentId": "qcrqox", "StageName": "Prod", - "RestApiId": "yn2f9dmisl" + "RestApiId": "g59lz4kkbg" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", @@ -2678,9 +2678,9 @@ "LogicalResourceId": "ServerlessRestApiProdStage", "PhysicalResourceId": "", "ResourceProperties": { - "DeploymentId": "p4f5gj", + "DeploymentId": "qcrqox", "StageName": "Prod", - "RestApiId": "yn2f9dmisl" + "RestApiId": "g59lz4kkbg" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceType": "AWS::ApiGateway::Stage", @@ -2691,7 +2691,7 @@ ], "Topic1": [ { - "EventId": "Topic1-bc7f9092-e33e-4b75-9ab9-0b05a1eca865", + "EventId": "Topic1-a79bb536-e5a8-4385-830c-0057881c0043", "LogicalResourceId": "Topic1", "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", "ResourceStatus": "DELETE_COMPLETE", @@ -2701,7 +2701,7 @@ "Timestamp": "timestamp" }, { - "EventId": "Topic1-53c44796-6014-4724-bf4e-398b5792343b", + "EventId": "Topic1-e811be29-1d08-47d9-a9a3-7fd72bbf09eb", "LogicalResourceId": "Topic1", "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", "ResourceStatus": "DELETE_IN_PROGRESS", @@ -2843,7 +2843,7 @@ } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_macro_fn_transform": { - "recorded-date": "30-05-2025, 19:19:36", + "recorded-date": "02-06-2025, 17:44:59", "recorded-content": { "create-change-set-1": { "Id": "arn::cloudformation::111111111111:changeSet/", @@ -3112,7 +3112,7 @@ { "EventId": "Parameter-CREATE_COMPLETE-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-RKPmUsCVodiF", + "PhysicalResourceId": "CFN-Parameter-lnSABVaxQdt8", "ResourceProperties": { "Type": "String", "Value": "{Substitution}" @@ -3126,7 +3126,7 @@ { "EventId": "Parameter-CREATE_IN_PROGRESS-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-RKPmUsCVodiF", + "PhysicalResourceId": "CFN-Parameter-lnSABVaxQdt8", "ResourceProperties": { "Type": "String", "Value": "{Substitution}" @@ -3155,7 +3155,7 @@ ], "Topic1": [ { - "EventId": "Topic1-d999817d-18b1-45e2-b4f2-ff6a275ad13f", + "EventId": "Topic1-d7cdeb51-69c0-4cb2-a5dc-c64c81b220e6", "LogicalResourceId": "Topic1", "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", "ResourceStatus": "DELETE_COMPLETE", @@ -3165,7 +3165,7 @@ "Timestamp": "timestamp" }, { - "EventId": "Topic1-ad6d008a-83b5-44f0-b20a-6f5106380a58", + "EventId": "Topic1-2dc6b6ab-bd65-47b5-ada9-d7252c9babda", "LogicalResourceId": "Topic1", "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", "ResourceStatus": "DELETE_IN_PROGRESS", @@ -3311,5 +3311,1375 @@ "Tags": [] } } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_macro_fn_transform": { + "recorded-date": "02-06-2025, 18:07:36", + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "FifoTopic": "true", + "ContentBasedDeduplication": "true", + "TopicName": "" + } + }, + "BeforeContext": { + "Properties": { + "TopicName": "topic-name-1" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "true", + "Attribute": "Properties", + "AttributeChangeType": "Add", + "Name": "FifoTopic", + "Path": "/Properties/FifoTopic", + "RequiresRecreation": "Always" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "true", + "Attribute": "Properties", + "AttributeChangeType": "Add", + "Name": "ContentBasedDeduplication", + "Path": "/Properties/ContentBasedDeduplication", + "RequiresRecreation": "Never" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "topic-name-1", + "Name": "TopicName", + "Path": "/Properties/TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "FifoTopic", + "RequiresRecreation": "Always" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "ContentBasedDeduplication", + "RequiresRecreation": "Never" + } + }, + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "TopicName", + "RequiresRecreation": "Always" + } + } + ], + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SNS::Topic", + "Scope": [ + "Properties" + ] + }, + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "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-4d2714dd-004a-47d7-b011-be0fa6fe8abb", + "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-c481534a-a8c9-4ebb-9654-de44ab49fc4a", + "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-UPDATE_COMPLETE-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:", + "ResourceProperties": { + "FifoTopic": "true", + "ContentBasedDeduplication": "true", + "TopicName": "" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:", + "ResourceProperties": { + "FifoTopic": "true", + "ContentBasedDeduplication": "true", + "TopicName": "" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SNS::Topic", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Topic1-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Topic1", + "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "ResourceProperties": { + "FifoTopic": "true", + "ContentBasedDeduplication": "true", + "TopicName": "" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", + "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" + } + ], + "": [ + { + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_macro_for_attribute_fn_transform": { + "recorded-date": "02-06-2025, 20:02:13", + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "", + "Type": "String", + "Name": "parameter-name" + } + }, + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "", + "Type": "String", + "Name": "parameter-name" + } + }, + "BeforeContext": { + "Properties": { + "Value": "", + "Type": "String", + "Name": "parameter-name" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Input", + "ParameterValue": "test" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Input", + "ParameterValue": "test" + } + ], + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Input", + "ParameterValue": "test" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Parameter": [ + { + "EventId": "Parameter-UPDATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "ResourceProperties": { + "Type": "String", + "Value": "", + "Name": "parameter-name" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "ResourceProperties": { + "Type": "String", + "Value": "", + "Name": "parameter-name" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "ResourceProperties": { + "Type": "String", + "Value": "", + "Name": "parameter-name" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "ResourceProperties": { + "Type": "String", + "Value": "", + "Name": "parameter-name" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "", + "Name": "parameter-name" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Input", + "ParameterValue": "test" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_multiple_fn_transform_order": { + "recorded-date": "02-06-2025, 20:36:50", + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "", + "Type": "String", + "Name": "parameter-name" + } + }, + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "", + "Type": "String", + "Name": "parameter-name" + } + }, + "BeforeContext": { + "Properties": { + "Value": "", + "Type": "String", + "Name": "parameter-name" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "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": { + "Parameter": [ + { + "EventId": "Parameter-UPDATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "ResourceProperties": { + "Type": "String", + "Value": "", + "Name": "parameter-name" + }, + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "ResourceProperties": { + "Type": "String", + "Value": "", + "Name": "parameter-name" + }, + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "ResourceProperties": { + "Type": "String", + "Value": "", + "Name": "parameter-name" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "ResourceProperties": { + "Type": "String", + "Value": "", + "Name": "parameter-name" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "", + "Name": "parameter-name" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "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_fn_transform.validation.json b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json index 271f9a582253a..d6683fbbcf189 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json @@ -1,20 +1,83 @@ { "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_fn_transform_include[json]": { - "last_validated_date": "2025-05-30T19:12:50+00:00" + "last_validated_date": "2025-06-02T17:38:17+00:00", + "durations_in_seconds": { + "setup": 0.39, + "call": 88.23, + "teardown": 0.87, + "total": 89.49 + } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_fn_transform_include[yml]": { - "last_validated_date": "2025-05-30T19:11:21+00:00" + "last_validated_date": "2025-06-02T17:36:48+00:00", + "durations_in_seconds": { + "setup": 0.62, + "call": 88.13, + "teardown": 0.84, + "total": 89.59 + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_macro_fn_transform": { + "last_validated_date": "2025-06-02T18:07:37+00:00", + "durations_in_seconds": { + "setup": 10.82, + "call": 130.7, + "teardown": 5.79, + "total": 147.31 + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_macro_for_attribute_fn_transform": { + "last_validated_date": "2025-06-02T20:02:13+00:00", + "durations_in_seconds": { + "setup": 10.8, + "call": 37.67, + "teardown": 5.5, + "total": 53.97 + } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_fn_transform_include[json]": { - "last_validated_date": "2025-05-30T19:15:52+00:00" + "last_validated_date": "2025-06-02T17:41:20+00:00", + "durations_in_seconds": { + "setup": 0.4, + "call": 90.2, + "teardown": 0.83, + "total": 91.43 + } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_fn_transform_include[yml]": { - "last_validated_date": "2025-05-30T19:14:20+00:00" + "last_validated_date": "2025-06-02T17:39:48+00:00", + "durations_in_seconds": { + "setup": 0.37, + "call": 90.22, + "teardown": 0.81, + "total": 91.4 + } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_macro_fn_transform": { - "last_validated_date": "2025-05-30T19:19:31+00:00" + "last_validated_date": "2025-06-02T17:44:59+00:00", + "durations_in_seconds": { + "setup": 10.52, + "call": 80.2, + "teardown": 5.08, + "total": 95.8 + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_multiple_fn_transform_order": { + "last_validated_date": "2025-06-02T20:36:50+00:00", + "durations_in_seconds": { + "setup": 10.74, + "call": 39.63, + "teardown": 5.49, + "total": 55.86 + } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_serverless_fn_transform": { - "last_validated_date": "2025-05-30T19:17:57+00:00" + "last_validated_date": "2025-06-02T17:43:23+00:00", + "durations_in_seconds": { + "setup": 0.35, + "call": 122.37, + "teardown": 0.63, + "total": 123.35 + } } } From 35eda66babe48d4a71d6d3899b21e1bde9296ea5 Mon Sep 17 00:00:00 2001 From: Cristopher Pinzon Date: Thu, 5 Jun 2025 10:20:00 -0500 Subject: [PATCH 05/22] use ssm parameters --- .../v2/test_change_set_fn_transform.py | 103 +- ...test_change_set_fn_transform.snapshot.json | 1383 ++++++----------- ...st_change_set_fn_transform.validation.json | 82 +- .../aws/templates/macros/add_standard_tags.py | 10 + 4 files changed, 616 insertions(+), 962 deletions(-) create mode 100644 tests/aws/templates/macros/add_standard_tags.py diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py index 31bbbc2a26e4b..cdd537fefa182 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py @@ -55,8 +55,8 @@ def _inner(macro_name, code_path): def test_embedded_fn_transform_include( self, include_format, snapshot, capture_update_process, s3_bucket, aws_client, tmp_path ): - name1 = f"topic-name-1-{short_uid()}" - name2 = f"topic-name-2-{short_uid()}" + name1 = f"name-1-{short_uid()}" + name2 = f"name-2-{short_uid()}" snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) bucket = s3_bucket @@ -64,14 +64,17 @@ def test_embedded_fn_transform_include( if include_format == "json": template = ( - '{"Topic2":{"Type":"AWS::SNS::Topic","Properties":{"TopicName": "%s"}}}' % name2 + '{"Parameter": { "Type": "AWS::SSM::Parameter","Properties": {"Name": "%s", "Type": "String", "Value": "foo"}}}' + % name2 ) else: template = f""" - Topic2: - Type: AWS::SNS::Topic + Parameter2: + Type: AWS::SSM::Parameter Properties: - TopicName: {name2} + Name: {name2} + Type: String + Value: foo """ file.write_text(data=template) @@ -83,20 +86,17 @@ def test_embedded_fn_transform_include( template_1 = { "Resources": { - "Topic1": { - "Type": "AWS::SNS::Topic", - "Properties": {"TopicName": name1, "DisplayName": "display-value-1"}, + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": name1, "Type": "String", "Value": "foo"}, }, } } template_2 = { "Resources": { - "Topic1": { - "Type": "AWS::SNS::Topic", - "Properties": { - "TopicName": name1, - "DisplayName": {"Fn::Sub": "The stack name is ${AWS::StackName}"}, - }, + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": name1, "Type": "String", "Value": "foo"}, }, "Fn::Transform": { "Name": "AWS::Include", @@ -111,20 +111,20 @@ def test_embedded_fn_transform_include( def test_global_fn_transform_include( self, include_format, snapshot, capture_update_process, s3_bucket, aws_client, tmp_path ): - name1 = f"topic-name-1-{short_uid()}" + name1 = f"name-1-{short_uid()}" snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) bucket = s3_bucket file = tmp_path / "bucket_definition.yml" if include_format == "json": - template = '{"Outputs":{"TopicRef":{"Value":{"Ref":"Topic1"}}}} ' + template = '{"Outputs":{"ParameterRef":{"Value":{"Ref":"Parameter"}}}} ' else: template = """ Outputs: - TopicRef: + ParameterRef: Value: - Ref: Topic1 + Ref: Parameter """ file.write_text(data=template) @@ -136,9 +136,9 @@ def test_global_fn_transform_include( template_1 = { "Resources": { - "Topic1": { - "Type": "AWS::SNS::Topic", - "Properties": {"TopicName": name1, "DisplayName": "display-value-1"}, + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": name1, "Type": "String", "Value": "foo"}, }, } } @@ -148,12 +148,9 @@ def test_global_fn_transform_include( "Parameters": {"Location": f"s3://{bucket}/template"}, }, "Resources": { - "Topic1": { - "Type": "AWS::SNS::Topic", - "Properties": { - "TopicName": name1, - "DisplayName": {"Fn::Sub": "The stack name is ${AWS::StackName}"}, - }, + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": name1, "Type": "String", "Value": "foo"}, }, }, } @@ -163,15 +160,15 @@ def test_global_fn_transform_include( def test_serverless_fn_transform( self, snapshot, capture_update_process, s3_bucket, aws_client, tmp_path ): - name1 = f"topic-name-1-{short_uid()}" + name1 = f"name-1-{short_uid()}" snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) template_1 = { "Resources": { - "Topic1": { - "Type": "AWS::SNS::Topic", - "Properties": {"TopicName": name1, "DisplayName": "display-value-1"}, - }, + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Value": "{Substitution}", "Type": "String", "Name": name1}, + } } } template_2 = { @@ -202,8 +199,8 @@ def test_global_macro_fn_transform( capture_update_process, create_macro, ): - name1 = f"topic-name-1-{short_uid()}" - snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + name1 = f"name-1-{short_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "name-1")) macro_function_path = os.path.join( os.path.dirname(__file__), "../../../templates/macros/replace_string.py" @@ -213,10 +210,10 @@ def test_global_macro_fn_transform( template_1 = { "Resources": { - "Topic1": { - "Type": "AWS::SNS::Topic", - "Properties": {"TopicName": name1, "DisplayName": "display-value-1"}, - }, + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Value": "original", "Type": "String", "Name": name1}, + } } } @@ -225,7 +222,7 @@ def test_global_macro_fn_transform( "Resources": { "Parameter": { "Type": "AWS::SSM::Parameter", - "Properties": {"Value": "{Substitution}", "Type": "String"}, + "Properties": {"Value": "{Substitution}", "Type": "String", "Name": name1}, } }, "Transform": {"Name": macro_name}, @@ -239,30 +236,34 @@ def test_embedded_macro_fn_transform( capture_update_process, create_macro, ): - name1 = f"topic-name-1-{short_uid()}" - name2 = f"topic-name-2-{short_uid()}.fifo" - snapshot.add_transformer(RegexTransformer(name1, "topic-name-1")) + name1 = f"name-1-{short_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "name-1")) macro_function_path = os.path.join( - os.path.dirname(__file__), "../../../templates/macros/add_standard_attributes.py" + os.path.dirname(__file__), "../../../templates/macros/add_standard_tags.py" ) - macro_name = "MakeFifo" + macro_name = "AddTags" create_macro(macro_name, macro_function_path) template_1 = { "Resources": { - "Topic1": { - "Type": "AWS::SNS::Topic", - "Properties": {"TopicName": name1}, + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": name1, "Type": "String", "Value": "foo"}, }, } } template_2 = { "Resources": { - "Topic1": { - "Type": "AWS::SNS::Topic", - "Properties": {"TopicName": name2, "Fn::Transform": macro_name}, + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": name1, + "Type": "String", + "Value": "foo", + "Fn::Transform": macro_name, + }, } } } diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json index da52614a57718..ab33794d73a47 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_fn_transform_include[yml]": { - "recorded-date": "02-06-2025, 17:36:48", + "recorded-date": "03-06-2025, 21:29:28", "recorded-content": { "create-change-set-1": { "Id": "arn::cloudformation::111111111111:changeSet/", @@ -24,13 +24,14 @@ "Action": "Add", "AfterContext": { "Properties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Value": "foo", + "Type": "String", + "Name": "topic-name-1" } }, "Details": [], - "LogicalResourceId": "Topic1", - "ResourceType": "AWS::SNS::Topic", + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", "Scope": [] }, "Type": "Resource" @@ -62,8 +63,8 @@ "ResourceChange": { "Action": "Add", "Details": [], - "LogicalResourceId": "Topic1", - "ResourceType": "AWS::SNS::Topic", + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", "Scope": [] }, "Type": "Resource" @@ -126,57 +127,19 @@ "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "ChangeSetName": "", "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": { - "DisplayName": "The stack name is ", - "TopicName": "topic-name-1" - } - }, - "BeforeContext": { - "Properties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" - } - }, - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Static", - "Target": { - "AfterValue": "The stack name is ", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "display-value-1", - "Name": "DisplayName", - "Path": "/Properties/DisplayName", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "Replacement": "False", - "ResourceType": "AWS::SNS::Topic", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - }, { "ResourceChange": { "Action": "Add", "AfterContext": { "Properties": { - "TopicName": "" + "Value": "foo", + "Type": "String", + "Name": "name-2-bd1e2d36" } }, "Details": [], - "LogicalResourceId": "Topic2", - "ResourceType": "AWS::SNS::Topic", + "LogicalResourceId": "Parameter2", + "ResourceType": "AWS::SSM::Parameter", "Scope": [] }, "Type": "Resource" @@ -204,36 +167,12 @@ "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "ChangeSetName": "", "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Static", - "Target": { - "Attribute": "Properties", - "Name": "DisplayName", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "Replacement": "False", - "ResourceType": "AWS::SNS::Topic", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - }, { "ResourceChange": { "Action": "Add", "Details": [], - "LogicalResourceId": "Topic2", - "ResourceType": "AWS::SNS::Topic", + "LogicalResourceId": "Parameter2", + "ResourceType": "AWS::SSM::Parameter", "Scope": [] }, "Type": "Resource" @@ -280,116 +219,97 @@ "Tags": [] }, "per-resource-events": { - "Topic1": [ - { - "EventId": "Topic1-UPDATE_COMPLETE-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "ResourceProperties": { - "DisplayName": "The stack name is ", - "TopicName": "topic-name-1" - }, - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Topic1-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "ResourceProperties": { - "DisplayName": "The stack name is ", - "TopicName": "topic-name-1" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, + "Parameter": [ { - "EventId": "Topic1-CREATE_COMPLETE-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "EventId": "Parameter-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "topic-name-1", "ResourceProperties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Type": "String", + "Value": "foo", + "Name": "topic-name-1" }, "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Topic1-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "topic-name-1", "ResourceProperties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Type": "String", + "Value": "foo", + "Name": "topic-name-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Topic1-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Topic1", + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", "PhysicalResourceId": "", "ResourceProperties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Type": "String", + "Value": "foo", + "Name": "topic-name-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" } ], - "Topic2": [ + "Parameter2": [ { - "EventId": "Topic2-CREATE_COMPLETE-date", - "LogicalResourceId": "Topic2", - "PhysicalResourceId": "arn::sns::111111111111:", + "EventId": "Parameter2-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter2", + "PhysicalResourceId": "name-2-bd1e2d36", "ResourceProperties": { - "TopicName": "" + "Type": "String", + "Value": "foo", + "Name": "name-2-bd1e2d36" }, "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Topic2-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Topic2", - "PhysicalResourceId": "arn::sns::111111111111:", + "EventId": "Parameter2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter2", + "PhysicalResourceId": "name-2-bd1e2d36", "ResourceProperties": { - "TopicName": "" + "Type": "String", + "Value": "foo", + "Name": "name-2-bd1e2d36" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Topic2-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Topic2", + "EventId": "Parameter2-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter2", "PhysicalResourceId": "", "ResourceProperties": { - "TopicName": "" + "Type": "String", + "Value": "foo", + "Name": "name-2-bd1e2d36" }, "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" @@ -484,7 +404,7 @@ } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_fn_transform_include[json]": { - "recorded-date": "02-06-2025, 17:38:17", + "recorded-date": "03-06-2025, 21:29:58", "recorded-content": { "create-change-set-1": { "Id": "arn::cloudformation::111111111111:changeSet/", @@ -508,13 +428,14 @@ "Action": "Add", "AfterContext": { "Properties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Value": "foo", + "Type": "String", + "Name": "topic-name-1" } }, "Details": [], - "LogicalResourceId": "Topic1", - "ResourceType": "AWS::SNS::Topic", + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", "Scope": [] }, "Type": "Resource" @@ -546,8 +467,8 @@ "ResourceChange": { "Action": "Add", "Details": [], - "LogicalResourceId": "Topic1", - "ResourceType": "AWS::SNS::Topic", + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", "Scope": [] }, "Type": "Resource" @@ -615,14 +536,16 @@ "Action": "Modify", "AfterContext": { "Properties": { - "DisplayName": "The stack name is ", - "TopicName": "topic-name-1" + "Value": "foo", + "Type": "String", + "Name": "name-2-3e00e97a" } }, "BeforeContext": { "Properties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Value": "foo", + "Type": "String", + "Name": "topic-name-1" } }, "Details": [ @@ -630,40 +553,26 @@ "ChangeSource": "DirectModification", "Evaluation": "Static", "Target": { - "AfterValue": "The stack name is ", + "AfterValue": "name-2-3e00e97a", "Attribute": "Properties", "AttributeChangeType": "Modify", - "BeforeValue": "display-value-1", - "Name": "DisplayName", - "Path": "/Properties/DisplayName", - "RequiresRecreation": "Never" + "BeforeValue": "topic-name-1", + "Name": "Name", + "Path": "/Properties/Name", + "RequiresRecreation": "Always" } } ], - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "Replacement": "False", - "ResourceType": "AWS::SNS::Topic", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "topic-name-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SSM::Parameter", "Scope": [ "Properties" ] }, "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Add", - "AfterContext": { - "Properties": { - "TopicName": "" - } - }, - "Details": [], - "LogicalResourceId": "Topic2", - "ResourceType": "AWS::SNS::Topic", - "Scope": [] - }, - "Type": "Resource" } ], "CreationTime": "datetime", @@ -697,30 +606,21 @@ "Evaluation": "Static", "Target": { "Attribute": "Properties", - "Name": "DisplayName", - "RequiresRecreation": "Never" + "Name": "Name", + "RequiresRecreation": "Always" } } ], - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "Replacement": "False", - "ResourceType": "AWS::SNS::Topic", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "topic-name-1", + "PolicyAction": "ReplaceAndDelete", + "Replacement": "True", + "ResourceType": "AWS::SSM::Parameter", "Scope": [ "Properties" ] }, "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "Topic2", - "ResourceType": "AWS::SNS::Topic", - "Scope": [] - }, - "Type": "Resource" } ], "CreationTime": "datetime", @@ -764,116 +664,116 @@ "Tags": [] }, "per-resource-events": { - "Topic1": [ + "Parameter": [ { - "EventId": "Topic1-UPDATE_COMPLETE-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "ResourceProperties": { - "DisplayName": "The stack name is ", - "TopicName": "topic-name-1" - }, - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", + "EventId": "Parameter-5cbf35a2-4a92-4554-a111-1f96269d3814", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "topic-name-1", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Topic1-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "ResourceProperties": { - "DisplayName": "The stack name is ", - "TopicName": "topic-name-1" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", + "EventId": "Parameter-06c1c753-8966-4dc1-9a39-cfbe492abedb", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "topic-name-1", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Topic1-CREATE_COMPLETE-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "EventId": "Parameter-UPDATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "name-2-3e00e97a", "ResourceProperties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Type": "String", + "Value": "foo", + "Name": "name-2-3e00e97a" }, - "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Topic1-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "EventId": "Parameter-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "name-2-3e00e97a", "ResourceProperties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Type": "String", + "Value": "foo", + "Name": "name-2-3e00e97a" }, - "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Topic1-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "", + "EventId": "Parameter-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "topic-name-1", "ResourceProperties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Type": "String", + "Value": "foo", + "Name": "name-2-3e00e97a" }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" - } - ], - "Topic2": [ + }, { - "EventId": "Topic2-CREATE_COMPLETE-date", - "LogicalResourceId": "Topic2", - "PhysicalResourceId": "arn::sns::111111111111:", + "EventId": "Parameter-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "topic-name-1", "ResourceProperties": { - "TopicName": "" + "Type": "String", + "Value": "foo", + "Name": "topic-name-1" }, "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Topic2-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Topic2", - "PhysicalResourceId": "arn::sns::111111111111:", + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "topic-name-1", "ResourceProperties": { - "TopicName": "" + "Type": "String", + "Value": "foo", + "Name": "topic-name-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Topic2-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Topic2", + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", "PhysicalResourceId": "", "ResourceProperties": { - "TopicName": "" + "Type": "String", + "Value": "foo", + "Name": "topic-name-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" @@ -968,7 +868,7 @@ } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_fn_transform_include[json]": { - "recorded-date": "02-06-2025, 17:41:20", + "recorded-date": "03-06-2025, 21:30:48", "recorded-content": { "create-change-set-1": { "Id": "arn::cloudformation::111111111111:changeSet/", @@ -992,13 +892,14 @@ "Action": "Add", "AfterContext": { "Properties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Value": "foo", + "Type": "String", + "Name": "topic-name-1" } }, "Details": [], - "LogicalResourceId": "Topic1", - "ResourceType": "AWS::SNS::Topic", + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", "Scope": [] }, "Type": "Resource" @@ -1030,8 +931,8 @@ "ResourceChange": { "Action": "Add", "Details": [], - "LogicalResourceId": "Topic1", - "ResourceType": "AWS::SNS::Topic", + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", "Scope": [] }, "Type": "Resource" @@ -1093,48 +994,7 @@ ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": { - "DisplayName": "The stack name is ", - "TopicName": "topic-name-1" - } - }, - "BeforeContext": { - "Properties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" - } - }, - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Static", - "Target": { - "AfterValue": "The stack name is ", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "display-value-1", - "Name": "DisplayName", - "Path": "/Properties/DisplayName", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "Replacement": "False", - "ResourceType": "AWS::SNS::Topic", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], + "Changes": [], "CreationTime": "datetime", "ExecutionStatus": "AVAILABLE", "IncludeNestedStacks": false, @@ -1156,32 +1016,7 @@ ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Static", - "Target": { - "Attribute": "Properties", - "Name": "DisplayName", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "Replacement": "False", - "ResourceType": "AWS::SNS::Topic", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], + "Changes": [], "CreationTime": "datetime", "ExecutionStatus": "AVAILABLE", "IncludeNestedStacks": false, @@ -1218,8 +1053,8 @@ "NotificationARNs": [], "Outputs": [ { - "OutputKey": "TopicRef", - "OutputValue": "arn::sns::111111111111:topic-name-1" + "OutputKey": "ParameterRef", + "OutputValue": "topic-name-1" } ], "RollbackConfiguration": {}, @@ -1229,74 +1064,49 @@ "Tags": [] }, "per-resource-events": { - "Topic1": [ - { - "EventId": "Topic1-UPDATE_COMPLETE-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "ResourceProperties": { - "DisplayName": "The stack name is ", - "TopicName": "topic-name-1" - }, - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Topic1-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "ResourceProperties": { - "DisplayName": "The stack name is ", - "TopicName": "topic-name-1" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, + "Parameter": [ { - "EventId": "Topic1-CREATE_COMPLETE-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "EventId": "Parameter-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "topic-name-1", "ResourceProperties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Type": "String", + "Value": "foo", + "Name": "topic-name-1" }, "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Topic1-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "topic-name-1", "ResourceProperties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Type": "String", + "Value": "foo", + "Name": "topic-name-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Topic1-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Topic1", + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", "PhysicalResourceId": "", "ResourceProperties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Type": "String", + "Value": "foo", + "Name": "topic-name-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" @@ -1384,8 +1194,8 @@ "NotificationARNs": [], "Outputs": [ { - "OutputKey": "TopicRef", - "OutputValue": "arn::sns::111111111111:topic-name-1" + "OutputKey": "ParameterRef", + "OutputValue": "topic-name-1" } ], "RollbackConfiguration": {}, @@ -1397,7 +1207,7 @@ } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_fn_transform_include[yml]": { - "recorded-date": "02-06-2025, 17:39:48", + "recorded-date": "03-06-2025, 21:30:23", "recorded-content": { "create-change-set-1": { "Id": "arn::cloudformation::111111111111:changeSet/", @@ -1421,13 +1231,14 @@ "Action": "Add", "AfterContext": { "Properties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Value": "foo", + "Type": "String", + "Name": "topic-name-1" } }, "Details": [], - "LogicalResourceId": "Topic1", - "ResourceType": "AWS::SNS::Topic", + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", "Scope": [] }, "Type": "Resource" @@ -1459,8 +1270,8 @@ "ResourceChange": { "Action": "Add", "Details": [], - "LogicalResourceId": "Topic1", - "ResourceType": "AWS::SNS::Topic", + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", "Scope": [] }, "Type": "Resource" @@ -1522,48 +1333,7 @@ ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "AfterContext": { - "Properties": { - "DisplayName": "The stack name is ", - "TopicName": "topic-name-1" - } - }, - "BeforeContext": { - "Properties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" - } - }, - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Static", - "Target": { - "AfterValue": "The stack name is ", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "display-value-1", - "Name": "DisplayName", - "Path": "/Properties/DisplayName", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "Replacement": "False", - "ResourceType": "AWS::SNS::Topic", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], + "Changes": [], "CreationTime": "datetime", "ExecutionStatus": "AVAILABLE", "IncludeNestedStacks": false, @@ -1585,32 +1355,7 @@ ], "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", "ChangeSetName": "", - "Changes": [ - { - "ResourceChange": { - "Action": "Modify", - "Details": [ - { - "ChangeSource": "DirectModification", - "Evaluation": "Static", - "Target": { - "Attribute": "Properties", - "Name": "DisplayName", - "RequiresRecreation": "Never" - } - } - ], - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "Replacement": "False", - "ResourceType": "AWS::SNS::Topic", - "Scope": [ - "Properties" - ] - }, - "Type": "Resource" - } - ], + "Changes": [], "CreationTime": "datetime", "ExecutionStatus": "AVAILABLE", "IncludeNestedStacks": false, @@ -1647,8 +1392,8 @@ "NotificationARNs": [], "Outputs": [ { - "OutputKey": "TopicRef", - "OutputValue": "arn::sns::111111111111:topic-name-1" + "OutputKey": "ParameterRef", + "OutputValue": "topic-name-1" } ], "RollbackConfiguration": {}, @@ -1658,74 +1403,49 @@ "Tags": [] }, "per-resource-events": { - "Topic1": [ - { - "EventId": "Topic1-UPDATE_COMPLETE-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "ResourceProperties": { - "DisplayName": "The stack name is ", - "TopicName": "topic-name-1" - }, - "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Topic1-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "ResourceProperties": { - "DisplayName": "The stack name is ", - "TopicName": "topic-name-1" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, + "Parameter": [ { - "EventId": "Topic1-CREATE_COMPLETE-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "EventId": "Parameter-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "topic-name-1", "ResourceProperties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Type": "String", + "Value": "foo", + "Name": "topic-name-1" }, "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Topic1-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "topic-name-1", "ResourceProperties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Type": "String", + "Value": "foo", + "Name": "topic-name-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Topic1-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Topic1", + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", "PhysicalResourceId": "", "ResourceProperties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Type": "String", + "Value": "foo", + "Name": "topic-name-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" @@ -1813,8 +1533,8 @@ "NotificationARNs": [], "Outputs": [ { - "OutputKey": "TopicRef", - "OutputValue": "arn::sns::111111111111:topic-name-1" + "OutputKey": "ParameterRef", + "OutputValue": "topic-name-1" } ], "RollbackConfiguration": {}, @@ -1826,7 +1546,7 @@ } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_serverless_fn_transform": { - "recorded-date": "02-06-2025, 17:43:23", + "recorded-date": "03-06-2025, 21:32:10", "recorded-content": { "create-change-set-1": { "Id": "arn::cloudformation::111111111111:changeSet/", @@ -1850,13 +1570,14 @@ "Action": "Add", "AfterContext": { "Properties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Value": "{Substitution}", + "Type": "String", + "Name": "topic-name-1" } }, "Details": [], - "LogicalResourceId": "Topic1", - "ResourceType": "AWS::SNS::Topic", + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", "Scope": [] }, "Type": "Resource" @@ -1888,8 +1609,8 @@ "ResourceChange": { "Action": "Add", "Details": [], - "LogicalResourceId": "Topic1", - "ResourceType": "AWS::SNS::Topic", + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", "Scope": [] }, "Type": "Resource" @@ -2035,6 +1756,25 @@ }, "Type": "Resource" }, + { + "ResourceChange": { + "Action": "Remove", + "BeforeContext": { + "Properties": { + "Value": "{Substitution}", + "Type": "String", + "Name": "topic-name-1" + } + }, + "Details": [], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "topic-name-1", + "PolicyAction": "Delete", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + }, { "ResourceChange": { "Action": "Add", @@ -2042,7 +1782,7 @@ "Properties": { "Body": { "paths": { - "/": { + "/": { "get": { "responses": {}, "x-amazon-apigateway-integration": { @@ -2101,24 +1841,6 @@ "Scope": [] }, "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Remove", - "BeforeContext": { - "Properties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" - } - }, - "Details": [], - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "PolicyAction": "Delete", - "ResourceType": "AWS::SNS::Topic", - "Scope": [] - }, - "Type": "Resource" } ], "CreationTime": "datetime", @@ -2175,10 +1897,12 @@ }, { "ResourceChange": { - "Action": "Add", + "Action": "Remove", "Details": [], - "LogicalResourceId": "ServerlessRestApi", - "ResourceType": "AWS::ApiGateway::RestApi", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "topic-name-1", + "PolicyAction": "Delete", + "ResourceType": "AWS::SSM::Parameter", "Scope": [] }, "Type": "Resource" @@ -2187,8 +1911,8 @@ "ResourceChange": { "Action": "Add", "Details": [], - "LogicalResourceId": "ServerlessRestApiDeployment47fc2d5f9d", - "ResourceType": "AWS::ApiGateway::Deployment", + "LogicalResourceId": "ServerlessRestApi", + "ResourceType": "AWS::ApiGateway::RestApi", "Scope": [] }, "Type": "Resource" @@ -2197,20 +1921,18 @@ "ResourceChange": { "Action": "Add", "Details": [], - "LogicalResourceId": "ServerlessRestApiProdStage", - "ResourceType": "AWS::ApiGateway::Stage", + "LogicalResourceId": "ServerlessRestApiDeployment47fc2d5f9d", + "ResourceType": "AWS::ApiGateway::Deployment", "Scope": [] }, "Type": "Resource" }, { "ResourceChange": { - "Action": "Remove", + "Action": "Add", "Details": [], - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "PolicyAction": "Delete", - "ResourceType": "AWS::SNS::Topic", + "LogicalResourceId": "ServerlessRestApiProdStage", + "ResourceType": "AWS::ApiGateway::Stage", "Scope": [] }, "Type": "Resource" @@ -2261,9 +1983,9 @@ { "EventId": "HelloWorldFunction-CREATE_COMPLETE-date", "LogicalResourceId": "HelloWorldFunction", - "PhysicalResourceId": "-HelloWorldFunction-3tFROESvgtFK", + "PhysicalResourceId": "-HelloWorldFunction-EdXB90Au1l86", "ResourceProperties": { - "Role": "arn::iam::111111111111:role/", + "Role": "arn::iam::111111111111:role/", "Runtime": "nodejs18.x", "Handler": "index.handler", "Code": { @@ -2285,9 +2007,9 @@ { "EventId": "HelloWorldFunction-CREATE_IN_PROGRESS-date", "LogicalResourceId": "HelloWorldFunction", - "PhysicalResourceId": "-HelloWorldFunction-3tFROESvgtFK", + "PhysicalResourceId": "-HelloWorldFunction-EdXB90Au1l86", "ResourceProperties": { - "Role": "arn::iam::111111111111:role/", + "Role": "arn::iam::111111111111:role/", "Runtime": "nodejs18.x", "Handler": "index.handler", "Code": { @@ -2312,7 +2034,7 @@ "LogicalResourceId": "HelloWorldFunction", "PhysicalResourceId": "", "ResourceProperties": { - "Role": "arn::iam::111111111111:role/", + "Role": "arn::iam::111111111111:role/", "Runtime": "nodejs18.x", "Handler": "index.handler", "Code": { @@ -2336,11 +2058,11 @@ { "EventId": "HelloWorldFunctionApiEventPermissionProd-CREATE_COMPLETE-date", "LogicalResourceId": "HelloWorldFunctionApiEventPermissionProd", - "PhysicalResourceId": "-HelloWorldFunctionApiEventPermissionProd-5fBzi1JcOwIe", + "PhysicalResourceId": "-HelloWorldFunctionApiEventPermissionProd-xfMFMMYChbqn", "ResourceProperties": { - "FunctionName": "-HelloWorldFunction-3tFROESvgtFK", + "FunctionName": "-HelloWorldFunction-EdXB90Au1l86", "Action": "lambda:InvokeFunction", - "SourceArn": "arn::execute-api::111111111111:g59lz4kkbg/*/GET/", + "SourceArn": "arn::execute-api::111111111111:317w2e8pc3/*/GET/", "Principal": "apigateway.amazonaws.com" }, "ResourceStatus": "CREATE_COMPLETE", @@ -2352,11 +2074,11 @@ { "EventId": "HelloWorldFunctionApiEventPermissionProd-CREATE_IN_PROGRESS-date", "LogicalResourceId": "HelloWorldFunctionApiEventPermissionProd", - "PhysicalResourceId": "-HelloWorldFunctionApiEventPermissionProd-5fBzi1JcOwIe", + "PhysicalResourceId": "-HelloWorldFunctionApiEventPermissionProd-xfMFMMYChbqn", "ResourceProperties": { - "FunctionName": "-HelloWorldFunction-3tFROESvgtFK", + "FunctionName": "-HelloWorldFunction-EdXB90Au1l86", "Action": "lambda:InvokeFunction", - "SourceArn": "arn::execute-api::111111111111:g59lz4kkbg/*/GET/", + "SourceArn": "arn::execute-api::111111111111:317w2e8pc3/*/GET/", "Principal": "apigateway.amazonaws.com" }, "ResourceStatus": "CREATE_IN_PROGRESS", @@ -2371,9 +2093,9 @@ "LogicalResourceId": "HelloWorldFunctionApiEventPermissionProd", "PhysicalResourceId": "", "ResourceProperties": { - "FunctionName": "-HelloWorldFunction-3tFROESvgtFK", + "FunctionName": "-HelloWorldFunction-EdXB90Au1l86", "Action": "lambda:InvokeFunction", - "SourceArn": "arn::execute-api::111111111111:g59lz4kkbg/*/GET/", + "SourceArn": "arn::execute-api::111111111111:317w2e8pc3/*/GET/", "Principal": "apigateway.amazonaws.com" }, "ResourceStatus": "CREATE_IN_PROGRESS", @@ -2387,7 +2109,7 @@ { "EventId": "HelloWorldFunctionRole-CREATE_COMPLETE-date", "LogicalResourceId": "HelloWorldFunctionRole", - "PhysicalResourceId": "", + "PhysicalResourceId": "", "ResourceProperties": { "ManagedPolicyArns": [ "arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" @@ -2424,7 +2146,7 @@ { "EventId": "HelloWorldFunctionRole-CREATE_IN_PROGRESS-date", "LogicalResourceId": "HelloWorldFunctionRole", - "PhysicalResourceId": "", + "PhysicalResourceId": "", "ResourceProperties": { "ManagedPolicyArns": [ "arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" @@ -2491,7 +2213,75 @@ ] }, "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::IAM::Role", + "ResourceType": "AWS::IAM::Role", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + } + ], + "Parameter": [ + { + "EventId": "Parameter-af425eba-d8e2-4ad9-9ebc-153095f8161d", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "topic-name-1", + "ResourceStatus": "DELETE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-adcb9c2b-0276-4487-9df6-5f12bf586323", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "topic-name-1", + "ResourceStatus": "DELETE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "topic-name-1", + "ResourceProperties": { + "Type": "String", + "Value": "{Substitution}", + "Name": "topic-name-1" + }, + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "topic-name-1", + "ResourceProperties": { + "Type": "String", + "Value": "{Substitution}", + "Name": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceStatusReason": "Resource creation Initiated", + "ResourceType": "AWS::SSM::Parameter", + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Timestamp": "timestamp" + }, + { + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "", + "ResourceProperties": { + "Type": "String", + "Value": "{Substitution}", + "Name": "topic-name-1" + }, + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" @@ -2501,17 +2291,17 @@ { "EventId": "ServerlessRestApi-CREATE_COMPLETE-date", "LogicalResourceId": "ServerlessRestApi", - "PhysicalResourceId": "g59lz4kkbg", + "PhysicalResourceId": "317w2e8pc3", "ResourceProperties": { "Body": { "paths": { - "/": { + "/": { "get": { "responses": {}, "x-amazon-apigateway-integration": { "httpMethod": "POST", "type": "aws_proxy", - "uri": "arn::apigateway::lambda:path/2015-03-31/functions/arn::lambda::111111111111:function:-HelloWorldFunction-3tFROESvgtFK/invocations" + "uri": "arn::apigateway::lambda:path/2015-03-31/functions/arn::lambda::111111111111:function:-HelloWorldFunction-EdXB90Au1l86/invocations" } } } @@ -2532,17 +2322,17 @@ { "EventId": "ServerlessRestApi-CREATE_IN_PROGRESS-date", "LogicalResourceId": "ServerlessRestApi", - "PhysicalResourceId": "g59lz4kkbg", + "PhysicalResourceId": "317w2e8pc3", "ResourceProperties": { "Body": { "paths": { - "/": { + "/": { "get": { "responses": {}, "x-amazon-apigateway-integration": { "httpMethod": "POST", "type": "aws_proxy", - "uri": "arn::apigateway::lambda:path/2015-03-31/functions/arn::lambda::111111111111:function:-HelloWorldFunction-3tFROESvgtFK/invocations" + "uri": "arn::apigateway::lambda:path/2015-03-31/functions/arn::lambda::111111111111:function:-HelloWorldFunction-EdXB90Au1l86/invocations" } } } @@ -2568,13 +2358,13 @@ "ResourceProperties": { "Body": { "paths": { - "/": { + "/": { "get": { "responses": {}, "x-amazon-apigateway-integration": { "httpMethod": "POST", "type": "aws_proxy", - "uri": "arn::apigateway::lambda:path/2015-03-31/functions/arn::lambda::111111111111:function:-HelloWorldFunction-3tFROESvgtFK/invocations" + "uri": "arn::apigateway::lambda:path/2015-03-31/functions/arn::lambda::111111111111:function:-HelloWorldFunction-EdXB90Au1l86/invocations" } } } @@ -2597,11 +2387,11 @@ { "EventId": "ServerlessRestApiDeployment47fc2d5f9d-CREATE_COMPLETE-date", "LogicalResourceId": "ServerlessRestApiDeployment47fc2d5f9d", - "PhysicalResourceId": "qcrqox", + "PhysicalResourceId": "5oj1e3", "ResourceProperties": { "Description": "RestApi deployment id: 47fc2d5f9d21ad56f83937abe2779d0e26d7095e", "StageName": "Stage", - "RestApiId": "g59lz4kkbg" + "RestApiId": "317w2e8pc3" }, "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::ApiGateway::Deployment", @@ -2612,11 +2402,11 @@ { "EventId": "ServerlessRestApiDeployment47fc2d5f9d-CREATE_IN_PROGRESS-date", "LogicalResourceId": "ServerlessRestApiDeployment47fc2d5f9d", - "PhysicalResourceId": "qcrqox", + "PhysicalResourceId": "5oj1e3", "ResourceProperties": { "Description": "RestApi deployment id: 47fc2d5f9d21ad56f83937abe2779d0e26d7095e", "StageName": "Stage", - "RestApiId": "g59lz4kkbg" + "RestApiId": "317w2e8pc3" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", @@ -2632,7 +2422,7 @@ "ResourceProperties": { "Description": "RestApi deployment id: 47fc2d5f9d21ad56f83937abe2779d0e26d7095e", "StageName": "Stage", - "RestApiId": "g59lz4kkbg" + "RestApiId": "317w2e8pc3" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceType": "AWS::ApiGateway::Deployment", @@ -2647,9 +2437,9 @@ "LogicalResourceId": "ServerlessRestApiProdStage", "PhysicalResourceId": "Prod", "ResourceProperties": { - "DeploymentId": "qcrqox", + "DeploymentId": "5oj1e3", "StageName": "Prod", - "RestApiId": "g59lz4kkbg" + "RestApiId": "317w2e8pc3" }, "ResourceStatus": "CREATE_COMPLETE", "ResourceType": "AWS::ApiGateway::Stage", @@ -2662,9 +2452,9 @@ "LogicalResourceId": "ServerlessRestApiProdStage", "PhysicalResourceId": "Prod", "ResourceProperties": { - "DeploymentId": "qcrqox", + "DeploymentId": "5oj1e3", "StageName": "Prod", - "RestApiId": "g59lz4kkbg" + "RestApiId": "317w2e8pc3" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", @@ -2678,9 +2468,9 @@ "LogicalResourceId": "ServerlessRestApiProdStage", "PhysicalResourceId": "", "ResourceProperties": { - "DeploymentId": "qcrqox", + "DeploymentId": "5oj1e3", "StageName": "Prod", - "RestApiId": "g59lz4kkbg" + "RestApiId": "317w2e8pc3" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceType": "AWS::ApiGateway::Stage", @@ -2689,71 +2479,6 @@ "Timestamp": "timestamp" } ], - "Topic1": [ - { - "EventId": "Topic1-a79bb536-e5a8-4385-830c-0057881c0043", - "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-e811be29-1d08-47d9-a9a3-7fd72bbf09eb", - "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": { - "DisplayName": "display-value-1", - "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": { - "DisplayName": "display-value-1", - "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": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], "": [ { "EventId": "", @@ -2843,7 +2568,7 @@ } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_macro_fn_transform": { - "recorded-date": "02-06-2025, 17:44:59", + "recorded-date": "03-06-2025, 21:42:32", "recorded-content": { "create-change-set-1": { "Id": "arn::cloudformation::111111111111:changeSet/", @@ -2867,13 +2592,14 @@ "Action": "Add", "AfterContext": { "Properties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Value": "original", + "Type": "String", + "Name": "name-1" } }, "Details": [], - "LogicalResourceId": "Topic1", - "ResourceType": "AWS::SNS::Topic", + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", "Scope": [] }, "Type": "Resource" @@ -2905,8 +2631,8 @@ "ResourceChange": { "Action": "Add", "Details": [], - "LogicalResourceId": "Topic1", - "ResourceType": "AWS::SNS::Topic", + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", "Scope": [] }, "Type": "Resource" @@ -2971,35 +2697,43 @@ "Changes": [ { "ResourceChange": { - "Action": "Add", + "Action": "Modify", "AfterContext": { "Properties": { "Value": "{Substitution}", - "Type": "String" + "Type": "String", + "Name": "name-1" } }, - "Details": [], - "LogicalResourceId": "Parameter", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Remove", "BeforeContext": { "Properties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Value": "original", + "Type": "String", + "Name": "name-1" } }, - "Details": [], - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "PolicyAction": "Delete", - "ResourceType": "AWS::SNS::Topic", - "Scope": [] + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "{Substitution}", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "original", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "name-1", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] }, "Type": "Resource" } @@ -3034,23 +2768,25 @@ "Changes": [ { "ResourceChange": { - "Action": "Add", - "Details": [], + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], "LogicalResourceId": "Parameter", + "PhysicalResourceId": "name-1", + "Replacement": "False", "ResourceType": "AWS::SSM::Parameter", - "Scope": [] - }, - "Type": "Resource" - }, - { - "ResourceChange": { - "Action": "Remove", - "Details": [], - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "PolicyAction": "Delete", - "ResourceType": "AWS::SNS::Topic", - "Scope": [] + "Scope": [ + "Properties" + ] }, "Type": "Resource" } @@ -3110,109 +2846,77 @@ "per-resource-events": { "Parameter": [ { - "EventId": "Parameter-CREATE_COMPLETE-date", + "EventId": "Parameter-UPDATE_COMPLETE-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-lnSABVaxQdt8", + "PhysicalResourceId": "name-1", "ResourceProperties": { "Type": "String", - "Value": "{Substitution}" + "Value": "{Substitution}", + "Name": "name-1" }, - "ResourceStatus": "CREATE_COMPLETE", + "ResourceStatus": "UPDATE_COMPLETE", "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "EventId": "Parameter-UPDATE_IN_PROGRESS-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "CFN-Parameter-lnSABVaxQdt8", + "PhysicalResourceId": "name-1", "ResourceProperties": { "Type": "String", - "Value": "{Substitution}" + "Value": "{Substitution}", + "Name": "name-1" }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", + "ResourceStatus": "UPDATE_IN_PROGRESS", "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "EventId": "Parameter-CREATE_COMPLETE-date", "LogicalResourceId": "Parameter", - "PhysicalResourceId": "", + "PhysicalResourceId": "name-1", "ResourceProperties": { "Type": "String", - "Value": "{Substitution}" - }, - "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SSM::Parameter", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - } - ], - "Topic1": [ - { - "EventId": "Topic1-d7cdeb51-69c0-4cb2-a5dc-c64c81b220e6", - "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-2dc6b6ab-bd65-47b5-ada9-d7252c9babda", - "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": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Value": "original", + "Name": "name-1" }, "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Topic1-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "name-1", "ResourceProperties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Type": "String", + "Value": "original", + "Name": "name-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Topic1-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Topic1", + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", "PhysicalResourceId": "", "ResourceProperties": { - "DisplayName": "display-value-1", - "TopicName": "topic-name-1" + "Type": "String", + "Value": "original", + "Name": "name-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" @@ -3313,7 +3017,7 @@ } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_macro_fn_transform": { - "recorded-date": "02-06-2025, 18:07:36", + "recorded-date": "03-06-2025, 21:51:11", "recorded-content": { "create-change-set-1": { "Id": "arn::cloudformation::111111111111:changeSet/", @@ -3337,12 +3041,14 @@ "Action": "Add", "AfterContext": { "Properties": { - "TopicName": "topic-name-1" + "Value": "foo", + "Type": "String", + "Name": "name-1" } }, "Details": [], - "LogicalResourceId": "Topic1", - "ResourceType": "AWS::SNS::Topic", + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", "Scope": [] }, "Type": "Resource" @@ -3374,8 +3080,8 @@ "ResourceChange": { "Action": "Add", "Details": [], - "LogicalResourceId": "Topic1", - "ResourceType": "AWS::SNS::Topic", + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", "Scope": [] }, "Type": "Resource" @@ -3443,14 +3149,19 @@ "Action": "Modify", "AfterContext": { "Properties": { - "FifoTopic": "true", - "ContentBasedDeduplication": "true", - "TopicName": "" + "Value": "foo", + "Type": "String", + "Tags": { + "MacroAdded": "True" + }, + "Name": "name-1" } }, "BeforeContext": { "Properties": { - "TopicName": "topic-name-1" + "Value": "foo", + "Type": "String", + "Name": "name-1" } }, "Details": [ @@ -3458,45 +3169,21 @@ "ChangeSource": "DirectModification", "Evaluation": "Static", "Target": { - "AfterValue": "true", - "Attribute": "Properties", - "AttributeChangeType": "Add", - "Name": "FifoTopic", - "Path": "/Properties/FifoTopic", - "RequiresRecreation": "Always" - } - }, - { - "ChangeSource": "DirectModification", - "Evaluation": "Static", - "Target": { - "AfterValue": "true", + "AfterValue": { + "MacroAdded": "True" + }, "Attribute": "Properties", "AttributeChangeType": "Add", - "Name": "ContentBasedDeduplication", - "Path": "/Properties/ContentBasedDeduplication", + "Name": "Tags", + "Path": "/Properties/Tags", "RequiresRecreation": "Never" } - }, - { - "ChangeSource": "DirectModification", - "Evaluation": "Static", - "Target": { - "AfterValue": "", - "Attribute": "Properties", - "AttributeChangeType": "Modify", - "BeforeValue": "topic-name-1", - "Name": "TopicName", - "Path": "/Properties/TopicName", - "RequiresRecreation": "Always" - } } ], - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "PolicyAction": "ReplaceAndDelete", - "Replacement": "True", - "ResourceType": "AWS::SNS::Topic", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "name-1", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", "Scope": [ "Properties" ] @@ -3535,34 +3222,15 @@ "Evaluation": "Static", "Target": { "Attribute": "Properties", - "Name": "FifoTopic", - "RequiresRecreation": "Always" - } - }, - { - "ChangeSource": "DirectModification", - "Evaluation": "Static", - "Target": { - "Attribute": "Properties", - "Name": "ContentBasedDeduplication", + "Name": "Tags", "RequiresRecreation": "Never" } - }, - { - "ChangeSource": "DirectModification", - "Evaluation": "Static", - "Target": { - "Attribute": "Properties", - "Name": "TopicName", - "RequiresRecreation": "Always" - } } ], - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", - "PolicyAction": "ReplaceAndDelete", - "Replacement": "True", - "ResourceType": "AWS::SNS::Topic", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "name-1", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", "Scope": [ "Properties" ] @@ -3611,110 +3279,85 @@ "Tags": [] }, "per-resource-events": { - "Topic1": [ - { - "EventId": "Topic1-4d2714dd-004a-47d7-b011-be0fa6fe8abb", - "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-c481534a-a8c9-4ebb-9654-de44ab49fc4a", - "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" - }, + "Parameter": [ { - "EventId": "Topic1-UPDATE_COMPLETE-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:", + "EventId": "Parameter-UPDATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "name-1", "ResourceProperties": { - "FifoTopic": "true", - "ContentBasedDeduplication": "true", - "TopicName": "" + "Type": "String", + "Value": "foo", + "Tags": { + "MacroAdded": "True" + }, + "Name": "name-1" }, "ResourceStatus": "UPDATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Timestamp": "timestamp" - }, - { - "EventId": "Topic1-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:", - "ResourceProperties": { - "FifoTopic": "true", - "ContentBasedDeduplication": "true", - "TopicName": "" - }, - "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Topic1-UPDATE_IN_PROGRESS-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "EventId": "Parameter-UPDATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "name-1", "ResourceProperties": { - "FifoTopic": "true", - "ContentBasedDeduplication": "true", - "TopicName": "" + "Type": "String", + "Value": "foo", + "Tags": { + "MacroAdded": "True" + }, + "Name": "name-1" }, "ResourceStatus": "UPDATE_IN_PROGRESS", - "ResourceStatusReason": "Requested update requires the creation of a new physical resource; hence creating one.", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Topic1-CREATE_COMPLETE-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "EventId": "Parameter-CREATE_COMPLETE-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "name-1", "ResourceProperties": { - "TopicName": "topic-name-1" + "Type": "String", + "Value": "foo", + "Name": "name-1" }, "ResourceStatus": "CREATE_COMPLETE", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Topic1-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Topic1", - "PhysicalResourceId": "arn::sns::111111111111:topic-name-1", + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "name-1", "ResourceProperties": { - "TopicName": "topic-name-1" + "Type": "String", + "Value": "foo", + "Name": "name-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", "ResourceStatusReason": "Resource creation Initiated", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" }, { - "EventId": "Topic1-CREATE_IN_PROGRESS-date", - "LogicalResourceId": "Topic1", + "EventId": "Parameter-CREATE_IN_PROGRESS-date", + "LogicalResourceId": "Parameter", "PhysicalResourceId": "", "ResourceProperties": { - "TopicName": "topic-name-1" + "Type": "String", + "Value": "foo", + "Name": "name-1" }, "ResourceStatus": "CREATE_IN_PROGRESS", - "ResourceType": "AWS::SNS::Topic", + "ResourceType": "AWS::SSM::Parameter", "StackId": "arn::cloudformation::111111111111:stack//", "StackName": "", "Timestamp": "timestamp" @@ -3809,7 +3452,7 @@ } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_macro_for_attribute_fn_transform": { - "recorded-date": "02-06-2025, 20:02:13", + "recorded-date": "03-06-2025, 21:34:07", "recorded-content": { "create-change-set-1": { "Id": "arn::cloudformation::111111111111:changeSet/", @@ -4258,7 +3901,7 @@ } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_multiple_fn_transform_order": { - "recorded-date": "02-06-2025, 20:36:50", + "recorded-date": "03-06-2025, 21:34:52", "recorded-content": { "create-change-set-1": { "Id": "arn::cloudformation::111111111111:changeSet/", diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json index d6683fbbcf189..142cc7a923cee 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json @@ -1,83 +1,83 @@ { "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_fn_transform_include[json]": { - "last_validated_date": "2025-06-02T17:38:17+00:00", + "last_validated_date": "2025-06-03T21:29:58+00:00", "durations_in_seconds": { "setup": 0.39, - "call": 88.23, - "teardown": 0.87, - "total": 89.49 + "call": 29.05, + "teardown": 0.82, + "total": 30.26 } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_fn_transform_include[yml]": { - "last_validated_date": "2025-06-02T17:36:48+00:00", + "last_validated_date": "2025-06-03T21:29:28+00:00", "durations_in_seconds": { "setup": 0.62, - "call": 88.13, - "teardown": 0.84, - "total": 89.59 + "call": 26.34, + "teardown": 0.8, + "total": 27.76 } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_macro_fn_transform": { - "last_validated_date": "2025-06-02T18:07:37+00:00", + "last_validated_date": "2025-06-03T21:51:12+00:00", "durations_in_seconds": { - "setup": 10.82, - "call": 130.7, - "teardown": 5.79, - "total": 147.31 + "setup": 10.68, + "call": 35.35, + "teardown": 5.52, + "total": 51.55 } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_macro_for_attribute_fn_transform": { - "last_validated_date": "2025-06-02T20:02:13+00:00", + "last_validated_date": "2025-06-03T21:34:07+00:00", "durations_in_seconds": { - "setup": 10.8, - "call": 37.67, - "teardown": 5.5, - "total": 53.97 + "setup": 0.0, + "call": 36.99, + "teardown": 5.18, + "total": 42.17 } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_fn_transform_include[json]": { - "last_validated_date": "2025-06-02T17:41:20+00:00", + "last_validated_date": "2025-06-03T21:30:48+00:00", "durations_in_seconds": { - "setup": 0.4, - "call": 90.2, - "teardown": 0.83, - "total": 91.43 + "setup": 0.39, + "call": 23.99, + "teardown": 0.8, + "total": 25.18 } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_fn_transform_include[yml]": { - "last_validated_date": "2025-06-02T17:39:48+00:00", + "last_validated_date": "2025-06-03T21:30:23+00:00", "durations_in_seconds": { "setup": 0.37, - "call": 90.22, - "teardown": 0.81, - "total": 91.4 + "call": 24.02, + "teardown": 0.89, + "total": 25.28 } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_macro_fn_transform": { - "last_validated_date": "2025-06-02T17:44:59+00:00", + "last_validated_date": "2025-06-03T21:42:32+00:00", "durations_in_seconds": { - "setup": 10.52, - "call": 80.2, - "teardown": 5.08, - "total": 95.8 + "setup": 10.81, + "call": 37.62, + "teardown": 5.34, + "total": 53.77 } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_multiple_fn_transform_order": { - "last_validated_date": "2025-06-02T20:36:50+00:00", + "last_validated_date": "2025-06-03T21:34:52+00:00", "durations_in_seconds": { - "setup": 10.74, - "call": 39.63, - "teardown": 5.49, - "total": 55.86 + "setup": 0.0, + "call": 39.28, + "teardown": 5.61, + "total": 44.89 } }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_serverless_fn_transform": { - "last_validated_date": "2025-06-02T17:43:23+00:00", + "last_validated_date": "2025-06-03T21:32:10+00:00", "durations_in_seconds": { "setup": 0.35, - "call": 122.37, - "teardown": 0.63, - "total": 123.35 + "call": 80.57, + "teardown": 0.71, + "total": 81.63 } } } diff --git a/tests/aws/templates/macros/add_standard_tags.py b/tests/aws/templates/macros/add_standard_tags.py new file mode 100644 index 0000000000000..dffe4ffb631e7 --- /dev/null +++ b/tests/aws/templates/macros/add_standard_tags.py @@ -0,0 +1,10 @@ +def handler(event, context): + fragment = add_standard_attributes(event["fragment"]) + + return {"requestId": event["requestId"], "status": "success", "fragment": fragment} + + +def add_standard_attributes(fragment): + fragment["Tags"] = {"MacroAdded": "True"} + + return fragment From 34a0fbadad1d5acc4e9719af76ed3686bffa5eea Mon Sep 17 00:00:00 2001 From: Cristopher Pinzon Date: Fri, 20 Jun 2025 16:32:16 -0500 Subject: [PATCH 06/22] test conditional transform --- .../cloudformation/test_template_engine.py | 1 - .../v2/test_change_set_fn_transform.py | 62 + ...test_change_set_fn_transform.snapshot.json | 1070 +++++++++++++++++ ...st_change_set_fn_transform.validation.json | 27 + 4 files changed, 1159 insertions(+), 1 deletion(-) diff --git a/tests/aws/services/cloudformation/test_template_engine.py b/tests/aws/services/cloudformation/test_template_engine.py index d039307ef5101..3e0bbd33c2bdf 100644 --- a/tests/aws/services/cloudformation/test_template_engine.py +++ b/tests/aws/services/cloudformation/test_template_engine.py @@ -781,7 +781,6 @@ def test_attribute_uses_macro(self, deploy_cfn_template, create_lambda_function, assert "test-" in resulting_value @markers.aws.validated - @pytest.mark.skip(reason="Fn::Transform does not support array of transformations") def test_scope_order_and_parameters( self, deploy_cfn_template, create_lambda_function, snapshot, aws_client ): diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py index cdd537fefa182..1b8fad0156617 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py @@ -367,3 +367,65 @@ def test_multiple_fn_transform_order( } capture_update_process(snapshot, template_1, template_2) + + @markers.aws.validated + @pytest.mark.parametrize("transform", ["true", "false"]) + def test_conditional_transform( + self, + transform, + snapshot, + capture_update_process, + create_macro, + ): + name1 = f"parameter-{short_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "parameter-name")) + snapshot.add_transformer(snapshot.transform.key_value("Value", "value")) + + macro_function_path = os.path.join( + os.path.dirname(__file__), "../../../templates/macros/replace_string.py" + ) + macro_name = "ReplaceString" + create_macro(macro_name, macro_function_path) + + template_1 = { + "Resources": { + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": name1, "Type": "String", "Value": "foo"}, + } + } + } + + template_2 = { + "Parameters": { + "Transform": { + "Type": "String" + } + }, + "Conditions": { + "Deploy": { + "Fn::Equals": [ + { + "Ref": "Transform" + }, + "true" + ] + } + }, + "Resources": { + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Condition": "Deploy", + "Properties": { + "Name": name1, + "Value": "", + "Type": "String", + "Fn::Transform": [ + {"Name": "ReplaceString", "Parameters": {"Input": "snippet-transform"}}, + ], + }, + } + } + } + + capture_update_process(snapshot, template_1, template_2, p2={"Transform": transform}) diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json index ab33794d73a47..931760d12cc40 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json @@ -4324,5 +4324,1075 @@ "Tags": [] } } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_conditional_transform": { + "recorded-date": "20-06-2025, 21:12:31", + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "", + "Type": "String", + "Name": "parameter-name" + } + }, + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "", + "Type": "String", + "Name": "parameter-name" + } + }, + "BeforeContext": { + "Properties": { + "Value": "", + "Type": "String", + "Name": "parameter-name" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Transform", + "ParameterValue": "true" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Transform", + "ParameterValue": "true" + } + ], + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Transform", + "ParameterValue": "true" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "delete-describe": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Transform", + "ParameterValue": "true" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Parameter": [ + { + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "Timestamp": "timestamp" + } + ], + "Stack": [ + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + } + ] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_conditional_transform[true]": { + "recorded-date": "20-06-2025, 21:15:45", + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "", + "Type": "String", + "Name": "parameter-name" + } + }, + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "", + "Type": "String", + "Name": "parameter-name" + } + }, + "BeforeContext": { + "Properties": { + "Value": "", + "Type": "String", + "Name": "parameter-name" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Transform", + "ParameterValue": "true" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Transform", + "ParameterValue": "true" + } + ], + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Transform", + "ParameterValue": "true" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "delete-describe": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Transform", + "ParameterValue": "true" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Parameter": [ + { + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "Timestamp": "timestamp" + } + ], + "Stack": [ + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + } + ] + } + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_conditional_transform[false]": { + "recorded-date": "20-06-2025, 21:16:29", + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "", + "Type": "String", + "Name": "parameter-name" + } + }, + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Remove", + "BeforeContext": { + "Properties": { + "Value": "", + "Type": "String", + "Name": "parameter-name" + } + }, + "Details": [], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "PolicyAction": "Delete", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Transform", + "ParameterValue": "false" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Remove", + "Details": [], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "PolicyAction": "Delete", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Transform", + "ParameterValue": "false" + } + ], + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Transform", + "ParameterValue": "false" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "delete-describe": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Transform", + "ParameterValue": "false" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Parameter": [ + { + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "Timestamp": "timestamp" + } + ], + "Stack": [ + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + } + ] + } + } } } diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json index 142cc7a923cee..038fd113795fc 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json @@ -1,4 +1,31 @@ { + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_conditional_transform": { + "last_validated_date": "2025-06-20T21:12:32+00:00", + "durations_in_seconds": { + "setup": 10.79, + "call": 35.71, + "teardown": 5.56, + "total": 52.06 + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_conditional_transform[false]": { + "last_validated_date": "2025-06-20T21:16:29+00:00", + "durations_in_seconds": { + "setup": 0.0, + "call": 38.41, + "teardown": 5.71, + "total": 44.12 + } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_conditional_transform[true]": { + "last_validated_date": "2025-06-20T21:15:45+00:00", + "durations_in_seconds": { + "setup": 10.81, + "call": 38.04, + "teardown": 5.12, + "total": 53.97 + } + }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_fn_transform_include[json]": { "last_validated_date": "2025-06-03T21:29:58+00:00", "durations_in_seconds": { From 244e13e11302258e30508c2baab58e23429707ec Mon Sep 17 00:00:00 2001 From: Cristopher Pinzon Date: Fri, 20 Jun 2025 17:21:28 -0500 Subject: [PATCH 07/22] test transform with fn::transform --- .../v2/test_change_set_fn_transform.py | 44 +++ ...test_change_set_fn_transform.snapshot.json | 349 ++++++++++++++++++ ...st_change_set_fn_transform.validation.json | 9 + 3 files changed, 402 insertions(+) diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py index 1b8fad0156617..0900115d521c6 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py @@ -429,3 +429,47 @@ def test_conditional_transform( } capture_update_process(snapshot, template_1, template_2, p2={"Transform": transform}) + + @markers.aws.validated + def test_macro_with_function( + self, + snapshot, + capture_update_process, + create_macro, + ): + name1 = f"parameter-{short_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "parameter-name")) + snapshot.add_transformer(snapshot.transform.key_value("Value", "value")) + + macro_function_path = os.path.join( + os.path.dirname(__file__), "../../../templates/macros/replace_string.py" + ) + macro_name = "ReplaceString" + create_macro(macro_name, macro_function_path) + + template_1 = { + "Resources": { + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": {"Name": name1, "Type": "String", "Value": "foo"}, + } + } + } + + template_2 = { + "Resources": { + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": name1, + "Value": "", + "Type": "String", + "Fn::Transform": [ + {"Name": macro_name, "Parameters": {"Input":{"Fn::Join": ["-", ["test", "string"]]}}}, + ], + }, + } + } + } + + capture_update_process(snapshot, template_1, template_2) diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json index 931760d12cc40..e9ca3f8bd137a 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json @@ -5394,5 +5394,354 @@ ] } } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_macro_with_function": { + "recorded-date": "20-06-2025, 22:19:25", + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "", + "Type": "String", + "Name": "parameter-name" + } + }, + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "", + "Type": "String", + "Name": "parameter-name" + } + }, + "BeforeContext": { + "Properties": { + "Value": "", + "Type": "String", + "Name": "parameter-name" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "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": [] + }, + "delete-describe": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Parameter": [ + { + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "Timestamp": "timestamp" + } + ], + "Stack": [ + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + } + ] + } + } } } diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json index 038fd113795fc..6b99fa98aabf8 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json @@ -89,6 +89,15 @@ "total": 53.77 } }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_macro_with_function": { + "last_validated_date": "2025-06-20T22:19:25+00:00", + "durations_in_seconds": { + "setup": 10.9, + "call": 36.99, + "teardown": 5.46, + "total": 53.35 + } + }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_multiple_fn_transform_order": { "last_validated_date": "2025-06-03T21:34:52+00:00", "durations_in_seconds": { From 6449ba364ef0cdeddcfde244ce98df70da86fa78 Mon Sep 17 00:00:00 2001 From: Cristopher Pinzon Date: Mon, 23 Jun 2025 14:05:13 -0500 Subject: [PATCH 08/22] implement global include --- .../engine/v2/change_set_model_transform.py | 39 +++++++++++++++++ .../v2/test_change_set_fn_transform.py | 42 +++++++------------ 2 files changed, 55 insertions(+), 26 deletions(-) diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py index 70981d014747c..889f19b4f839a 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py @@ -4,9 +4,12 @@ from typing import Any, Final, Optional, TypedDict import boto3 +from botocore.exceptions import ClientError from samtranslator.translator.transform import transform as transform_sam +from localstack.aws.connect import connect_to from localstack.services.cloudformation.engine.policy_loader import create_policy_loader +from localstack.services.cloudformation.engine.template_preparer import parse_template from localstack.services.cloudformation.engine.transformers import ( FailedTransformationException, execute_macro, @@ -27,12 +30,14 @@ ) from localstack.services.cloudformation.stores import get_cloudformation_store from localstack.services.cloudformation.v2.entities import ChangeSet +from localstack.utils import testutil LOG = logging.getLogger(__name__) SERVERLESS_TRANSFORM = "AWS::Serverless-2016-10-31" EXTENSIONS_TRANSFORM = "AWS::LanguageExtensions" SECRETSMANAGER_TRANSFORM = "AWS::SecretsManager-2020-07-23" +INCLUDE_TRANSFORM = "AWS::Include" _SCOPE_TRANSFORM_TEMPLATE_OUTCOME: Final[Scope] = Scope("TRANSFORM_TEMPLATE_OUTCOME") @@ -138,6 +143,32 @@ def _apply_global_serverless_transformation( if region_before is not None: os.environ["AWS_DEFAULT_REGION"] = region_before + @staticmethod + def _apply_global_include( + global_transform: GlobalTransform, template: dict, parameters: dict, account_id, region_name + ) -> dict: + location = global_transform.parameters.get("Location") + if not location or not location.startswith("s3://"): + raise FailedTransformationException( + transformation=INCLUDE_TRANSFORM, + message="Unexpected Location parameter for AWS::Include transformer: %s" % location, + ) + + s3_client = connect_to(aws_access_key_id=account_id, region_name=region_name).s3 + bucket, _, path = location.removeprefix("s3://").partition("/") + try: + content = testutil.download_s3_object(s3_client, bucket, path) + except ClientError: + raise FailedTransformationException( + transformation=INCLUDE_TRANSFORM, + message="Error downloading S3 object '%s/%s'" % (bucket, path), + ) + try: + template_to_include = parse_template(content) + except Exception as e: + raise FailedTransformationException(transformation=INCLUDE_TRANSFORM, message=str(e)) + return {**template, **template_to_include} + @staticmethod def _apply_global_macro_transformation( account_id: str, @@ -182,6 +213,14 @@ def _apply_global_transform( # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/transform-aws-secretsmanager.html LOG.warning("%s is not yet supported. Ignoring.", SECRETSMANAGER_TRANSFORM) transformed_template = template + elif transform_name == INCLUDE_TRANSFORM: + transformed_template = self._apply_global_include( + global_transform=global_transform, + region_name=self._change_set.region_name, + account_id=self._change_set.account_id, + template=template, + parameters=parameters, + ) else: transformed_template = self._apply_global_macro_transformation( account_id=self._change_set.account_id, diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py index 0900115d521c6..4f893e7e0ed8e 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py @@ -371,11 +371,11 @@ def test_multiple_fn_transform_order( @markers.aws.validated @pytest.mark.parametrize("transform", ["true", "false"]) def test_conditional_transform( - self, - transform, - snapshot, - capture_update_process, - create_macro, + self, + transform, + snapshot, + capture_update_process, + create_macro, ): name1 = f"parameter-{short_uid()}" snapshot.add_transformer(RegexTransformer(name1, "parameter-name")) @@ -397,21 +397,8 @@ def test_conditional_transform( } template_2 = { - "Parameters": { - "Transform": { - "Type": "String" - } - }, - "Conditions": { - "Deploy": { - "Fn::Equals": [ - { - "Ref": "Transform" - }, - "true" - ] - } - }, + "Parameters": {"Transform": {"Type": "String"}}, + "Conditions": {"Deploy": {"Fn::Equals": [{"Ref": "Transform"}, "true"]}}, "Resources": { "Parameter": { "Type": "AWS::SSM::Parameter", @@ -425,17 +412,17 @@ def test_conditional_transform( ], }, } - } + }, } capture_update_process(snapshot, template_1, template_2, p2={"Transform": transform}) @markers.aws.validated def test_macro_with_function( - self, - snapshot, - capture_update_process, - create_macro, + self, + snapshot, + capture_update_process, + create_macro, ): name1 = f"parameter-{short_uid()}" snapshot.add_transformer(RegexTransformer(name1, "parameter-name")) @@ -465,7 +452,10 @@ def test_macro_with_function( "Value": "", "Type": "String", "Fn::Transform": [ - {"Name": macro_name, "Parameters": {"Input":{"Fn::Join": ["-", ["test", "string"]]}}}, + { + "Name": macro_name, + "Parameters": {"Input": {"Fn::Join": ["-", ["test", "string"]]}}, + }, ], }, } From 3023d236c3eb30ad56829abfc24d2aa37155ef2c Mon Sep 17 00:00:00 2001 From: Cristopher Pinzon Date: Tue, 1 Jul 2025 17:18:56 -0500 Subject: [PATCH 09/22] partial implementation of fn::transform at attribute value level --- .../engine/v2/change_set_model_preproc.py | 50 +++++++++++++++---- .../engine/v2/change_set_model_transform.py | 22 ++++---- .../services/cloudformation/v2/provider.py | 3 ++ 3 files changed, 54 insertions(+), 21 deletions(-) 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 abaae139c741f..0b9b03b049a90 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 @@ -595,22 +595,22 @@ def _compute_fn_not(arg: bool) -> bool: ) return delta - def _compute_fn_transform(self, args: dict[str, Any]) -> Any: + def _compute_fn_transform(self, macro_definition: dict[str, Any]) -> Any: # TODO: add typing to arguments before this level. # TODO: add schema validation # TODO: add support for other transform types account_id = self._change_set.account_id region_name = self._change_set.region_name - transform_name: str = args.get("Name") + transform_name: str = macro_definition.get("Name") if not isinstance(transform_name, str): raise RuntimeError("Invalid or missing Fn::Transform 'Name' argument") - transform_parameters: dict = args.get("Parameters") + transform_parameters: dict = macro_definition.get("Parameters") if not isinstance(transform_parameters, dict): raise RuntimeError("Invalid or missing Fn::Transform 'Parameters' argument") if transform_name in transformers: - # TODO: port and refactor this 'transformers' logic to this package. + # TODO: port and refactor this 'transformers' logic to this package.https://lmstudio.ai/ builtin_transformer_class = transformers[transform_name] builtin_transformer: Transformer = builtin_transformer_class() transform_output: Any = builtin_transformer.transform( @@ -624,17 +624,36 @@ def _compute_fn_transform(self, args: dict[str, Any]) -> Any: if transform_name in macros_store: # TODO: this formatting of stack parameters is odd but required to integrate with v1 execute_macro util. # consider porting this utils and passing the plain list of parameters instead. - stack_parameters = { - parameter["ParameterKey"]: parameter - for parameter in self._change_set.stack.parameters + + resolved_parameters = { + **self._change_set.stack.resolved_parameters, + **self.visit_node_parameters( + self._change_set.update_model.node_template.parameters + ).after, + } + + template_parameters = { + **self._change_set.stack.template.get("Parameters", {}), + **(self._change_set.template or {}).get("Parameters", {}), } + + for key, value in resolved_parameters.items(): + template_parameters[key]["ParameterValue"] = value + + # -2 removes: divergence/Fn::transform, -3 returns the path until the parent + scope = macro_definition.get("Scope").split("/")[:-3] + template = self._change_set.template or {} + for key in scope: + if key: + template = template.get(key) + transform_output: Any = execute_macro( account_id=account_id, region_name=region_name, - parsed_template=dict(), # TODO: review the requirements for this argument. - macro=args, # TODO: review support for non dict bindings (v1). - stack_parameters=stack_parameters, - transformation_parameters=transform_parameters, + parsed_template=template, # TODO: review the requirements for this argument. + macro=macro_definition, # TODO: review support for non dict bindings (v1). + stack_parameters=template_parameters, + transformation_parameters=macro_definition.get("Parameters"), is_intrinsic=True, ) return transform_output @@ -647,6 +666,15 @@ def visit_node_intrinsic_function_fn_transform( self, node_intrinsic_function: NodeIntrinsicFunction ) -> PreprocEntityDelta: arguments_delta = self.visit(node_intrinsic_function.arguments) + + # TODO find better manage of this + # The scope of a transformation is important to determine the template passed to the macro + if arguments_delta.before: + arguments_delta.before["Scope"] = node_intrinsic_function.scope + + if arguments_delta.after: + arguments_delta.after["Scope"] = node_intrinsic_function.scope + delta = self._cached_apply( scope=node_intrinsic_function.scope, arguments_delta=arguments_delta, diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py index 889f19b4f839a..87ca0c9a53aa4 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py @@ -252,11 +252,12 @@ def transform(self) -> tuple[dict, dict]: if not transformed_before_template: transformed_before_template = self._before_template for before_global_transform in transform_before: - transformed_before_template = self._apply_global_transform( - global_transform=before_global_transform, - parameters=parameters_before, - template=transformed_before_template, - ) + if not is_nothing(before_global_transform.name): + transformed_before_template = self._apply_global_transform( + global_transform=before_global_transform, + parameters=parameters_before, + template=transformed_before_template, + ) self._before_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = transformed_before_template transformed_after_template = self._after_template @@ -265,11 +266,12 @@ def transform(self) -> tuple[dict, dict]: if not transformed_after_template: transformed_after_template = self._after_template for after_global_transform in transform_after: - transformed_after_template = self._apply_global_transform( - global_transform=after_global_transform, - parameters=parameters_after, - template=transformed_after_template, - ) + if not is_nothing(after_global_transform.name): + transformed_after_template = self._apply_global_transform( + global_transform=after_global_transform, + parameters=parameters_after, + template=transformed_after_template, + ) self._after_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = transformed_after_template self._save_runtime_cache() diff --git a/localstack-core/localstack/services/cloudformation/v2/provider.py b/localstack-core/localstack/services/cloudformation/v2/provider.py index 5e96a743780b0..19f15dac090be 100644 --- a/localstack-core/localstack/services/cloudformation/v2/provider.py +++ b/localstack-core/localstack/services/cloudformation/v2/provider.py @@ -812,6 +812,9 @@ def _run(*args): stack.set_stack_status(StackStatus.DELETE_COMPLETE) stack.deletion_time = datetime.now(tz=timezone.utc) except Exception as e: + import traceback + + traceback.print_exc() LOG.warning( "Failed to delete stack '%s': %s", stack.stack_name, From 429291a990160b31623b492b41e5a60e600f1010 Mon Sep 17 00:00:00 2001 From: Cristopher Pinzon Date: Wed, 2 Jul 2025 16:31:55 -0500 Subject: [PATCH 10/22] fix deletion with transform --- .../localstack/services/cloudformation/v2/provider.py | 5 ++++- .../cloudformation/v2/test_change_set_fn_transform.py | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/localstack-core/localstack/services/cloudformation/v2/provider.py b/localstack-core/localstack/services/cloudformation/v2/provider.py index 19f15dac090be..71c7bd7a6be46 100644 --- a/localstack-core/localstack/services/cloudformation/v2/provider.py +++ b/localstack-core/localstack/services/cloudformation/v2/provider.py @@ -1,4 +1,5 @@ import copy +import json import logging from collections import defaultdict from datetime import datetime, timezone @@ -380,6 +381,8 @@ def _run(*args): # which was just deployed change_set.stack.template = change_set.template except Exception as e: + import traceback + traceback.print_exc() LOG.error( "Execute change set failed: %s", e, exc_info=LOG.isEnabledFor(logging.WARNING) ) @@ -796,7 +799,7 @@ def delete_stack( change_set = ChangeSet(stack, {"ChangeSetName": f"delete-stack_{stack.stack_name}"}) # noqa self._setup_change_set_model( change_set=change_set, - before_template=stack.template, + before_template=json.loads(stack.template_body or "{}"), after_template=None, before_parameters=stack.resolved_parameters, after_parameters=None, diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py index 4f893e7e0ed8e..4ec9635b5fdf4 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py @@ -157,6 +157,12 @@ def test_global_fn_transform_include( capture_update_process(snapshot, template_1, template_2) @markers.aws.validated + @markers.snapshot.skip_snapshot_verify( + paths=[ + "$..Changes..ResourceChange.AfterContext.Properties.Body.paths", + "$..Changes..ResourceChange.AfterContext.Properties.SourceArn", + ] + ) def test_serverless_fn_transform( self, snapshot, capture_update_process, s3_bucket, aws_client, tmp_path ): From 1aa4ab58f69df3257af92c0cd9b2afd2c20fae55 Mon Sep 17 00:00:00 2001 From: Cristopher Pinzon Date: Thu, 3 Jul 2025 16:53:18 -0500 Subject: [PATCH 11/22] wip to make transformer support any location --- .../engine/v2/change_set_model_transform.py | 122 ++++++++++++++++++ .../services/cloudformation/v2/provider.py | 1 + 2 files changed, 123 insertions(+) diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py index 87ca0c9a53aa4..a884e8864dbbf 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py @@ -12,7 +12,9 @@ from localstack.services.cloudformation.engine.template_preparer import parse_template from localstack.services.cloudformation.engine.transformers import ( FailedTransformationException, + Transformer, execute_macro, + transformers, ) from localstack.services.cloudformation.engine.v2.change_set_model import ( ChangeType, @@ -276,8 +278,128 @@ def transform(self) -> tuple[dict, dict]: self._save_runtime_cache() + ### Handle Embedded Fn::Transform + transformed_before_template = self._execute_embedded_transformations( + template=transformed_before_template, resolved_parameters=parameters_before + ) + transformed_after_template = self._execute_embedded_transformations( + template=transformed_after_template, resolved_parameters=parameters_after + ) + return transformed_before_template, transformed_after_template + def _execute_embedded_transformations( + self, template, resolved_parameters + ) -> PreprocEntityDelta: + transformations = self._find_fn_transforms(template) + normalized_transformations = self._normalize_transform_definitions(transformations) + + transformed_template = copy.deepcopy(template) + for transformation in normalized_transformations: + transformed_template = self._execute_embedded_transformation( + transformation=transformation, + template=transformed_template, + resolved_parameters=resolved_parameters, + ) + return transformed_template + + @staticmethod + def _normalize_transform_definitions( + transform_definitions: list[(any, str)], + ) -> list[(dict, any)]: + def _normalize_individual_transform(transform_def: str | dict): + # TODO: validate parameters, imports, refs to resources or conditionals are not supported. + # only literals, refs to parameters and basic intrinsic functions like sub and join, posibly select + if isinstance(transform_def, str): + return {"Name": transform_def, "Parameters": {}} + + if isinstance(transform_def, dict): + return { + "Name": transform_def["Name"], + "Parameters": transform_def.get("Parameters", {}), + } + + raise FailedTransformationException("Invalid Definition of transformation") + + normalized_transforms = [] + for path, value in transform_definitions: + if isinstance(value, list): + for transform in value: + normalized_transforms.append((path, _normalize_individual_transform(transform))) + else: + normalized_transforms.append((path, _normalize_individual_transform(value))) + + return normalized_transforms + + def _execute_embedded_transformation( + self, transformation: (dict, str), template: dict, resolved_parameters: dict + ) -> dict: + macros_store = get_cloudformation_store( + account_id=self._change_set.account_id, region_name=self._change_set.region_name + ).macros + + def _apply_transform_on_template(scope, template, value): + pass + + scope = transformation[1] + transform_name = transformation[0]["Name"] + transform_parameters = transformation[0]["Parameters"] + + if transform_name in transformers: + builtin_transformer_class = transformers[transform_name] + builtin_transformer: Transformer = builtin_transformer_class() + transform_output: Any = builtin_transformer.transform( + account_id=self._change_set.account_id, + region_name=self._change_set.region_name, + parameters=transform_parameters, + ) + _apply_transform_on_template(scope, template, transform_output) + + template_parameters = template.get( + "Parameters" + ) # The complete definition is required for the macro execution + if transform_name.get("Name") in macros_store: + for key, value in resolved_parameters.items(): + template_parameters[key]["ParameterValue"] = value + + # A macro is only able to access their node parent and siblings + parent_node = template + for key in scope.split("/")[:-3]: + if key: + parent_node = template.get(key) + + transform_output: Any = execute_macro( + account_id=self._change_set.account_id, + region_name=self._change_set.region_name, + parsed_template=parent_node, + macro=transformation[0], + stack_parameters=transform_parameters, + transformation_parameters=transform_parameters, + is_intrinsic=True, + ) + return transform_output + raise + + def _find_fn_transforms(self, obj, path=None) -> list[(any, str)]: + if path is None: + path = [] + + results = [] + + if isinstance(obj, dict): + for key, value in obj.items(): + current_path = path + [key] + if key == "Fn::Transform": + results.append(("/".join(current_path), value)) + results.extend(self._find_fn_transforms(value, current_path)) + + elif isinstance(obj, list): + for idx, item in enumerate(obj): + current_path = path + [f"[{idx}]"] + results.extend(self._find_fn_transforms(item, current_path)) + + return results + def visit_node_global_transform( self, node_global_transform: NodeGlobalTransform ) -> PreprocEntityDelta[GlobalTransform, GlobalTransform]: diff --git a/localstack-core/localstack/services/cloudformation/v2/provider.py b/localstack-core/localstack/services/cloudformation/v2/provider.py index 71c7bd7a6be46..c734002b98eb6 100644 --- a/localstack-core/localstack/services/cloudformation/v2/provider.py +++ b/localstack-core/localstack/services/cloudformation/v2/provider.py @@ -382,6 +382,7 @@ def _run(*args): change_set.stack.template = change_set.template except Exception as e: import traceback + traceback.print_exc() LOG.error( "Execute change set failed: %s", e, exc_info=LOG.isEnabledFor(logging.WARNING) From 9bf7deb506650743ca63c5d54a05a221c48a999b Mon Sep 17 00:00:00 2001 From: Cristopher Pinzon Date: Fri, 4 Jul 2025 16:38:53 -0500 Subject: [PATCH 12/22] location and execution of embedded transforms work --- .../engine/v2/change_set_model_transform.py | 45 ++++++++++--------- .../v2/test_change_set_fn_transform.py | 5 ++- ...test_change_set_fn_transform.snapshot.json | 2 +- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py index a884e8864dbbf..1819ec3e3ff80 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py @@ -306,7 +306,7 @@ def _execute_embedded_transformations( @staticmethod def _normalize_transform_definitions( transform_definitions: list[(any, str)], - ) -> list[(dict, any)]: + ) -> dict: def _normalize_individual_transform(transform_def: str | dict): # TODO: validate parameters, imports, refs to resources or conditionals are not supported. # only literals, refs to parameters and basic intrinsic functions like sub and join, posibly select @@ -325,9 +325,9 @@ def _normalize_individual_transform(transform_def: str | dict): for path, value in transform_definitions: if isinstance(value, list): for transform in value: - normalized_transforms.append((path, _normalize_individual_transform(transform))) + normalized_transforms.append((_normalize_individual_transform(transform), path)) else: - normalized_transforms.append((path, _normalize_individual_transform(value))) + normalized_transforms.append((_normalize_individual_transform(value), path)) return normalized_transforms @@ -338,8 +338,20 @@ def _execute_embedded_transformation( account_id=self._change_set.account_id, region_name=self._change_set.region_name ).macros - def _apply_transform_on_template(scope, template, value): - pass + def _apply_transform_on_template(scope, template, transformation_result, include=False): + node = template + prev_node = node + for key in scope.split("/")[:-1]: + prev_node = node + node = node[key] + + if include and isinstance(prev_node, dict): + del node["Fn::Transform"] + prev_node[key].update(transformation_result) + else: + prev_node[key] = transformation_result + + return template scope = transformation[1] transform_name = transformation[0]["Name"] @@ -353,32 +365,25 @@ def _apply_transform_on_template(scope, template, value): region_name=self._change_set.region_name, parameters=transform_parameters, ) - _apply_transform_on_template(scope, template, transform_output) - - template_parameters = template.get( - "Parameters" - ) # The complete definition is required for the macro execution - if transform_name.get("Name") in macros_store: - for key, value in resolved_parameters.items(): - template_parameters[key]["ParameterValue"] = value + return _apply_transform_on_template(scope, template, transform_output, True) + if transform_name in macros_store: # A macro is only able to access their node parent and siblings parent_node = template - for key in scope.split("/")[:-3]: - if key: - parent_node = template.get(key) + for key in scope.split("/")[:-1]: + parent_node = parent_node.get(key) transform_output: Any = execute_macro( account_id=self._change_set.account_id, region_name=self._change_set.region_name, parsed_template=parent_node, macro=transformation[0], - stack_parameters=transform_parameters, + stack_parameters=resolved_parameters, transformation_parameters=transform_parameters, is_intrinsic=True, ) - return transform_output - raise + return _apply_transform_on_template(scope, template, transform_output) + raise FailedTransformationException("Macro not found") def _find_fn_transforms(self, obj, path=None) -> list[(any, str)]: if path is None: @@ -395,7 +400,7 @@ def _find_fn_transforms(self, obj, path=None) -> list[(any, str)]: elif isinstance(obj, list): for idx, item in enumerate(obj): - current_path = path + [f"[{idx}]"] + current_path = path + [f"{idx}"] results.extend(self._find_fn_transforms(item, current_path)) return results diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py index 4ec9635b5fdf4..e194608bb90c7 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py @@ -52,6 +52,9 @@ def _inner(macro_name, code_path): @markers.aws.validated @pytest.mark.parametrize("include_format", ["yml", "json"]) + @markers.snapshot.skip_snapshot_verify( + paths=["$..Changes..ResourceChange.AfterContext.Properties.Name"] + ) def test_embedded_fn_transform_include( self, include_format, snapshot, capture_update_process, s3_bucket, aws_client, tmp_path ): @@ -424,7 +427,7 @@ def test_conditional_transform( capture_update_process(snapshot, template_1, template_2, p2={"Transform": transform}) @markers.aws.validated - def test_macro_with_function( + def test_macro_with_intrisic_function( self, snapshot, capture_update_process, diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json index e9ca3f8bd137a..4f75442d6fb34 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json @@ -5395,7 +5395,7 @@ } } }, - "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_macro_with_function": { + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_macro_with_intrinsic_function": { "recorded-date": "20-06-2025, 22:19:25", "recorded-content": { "create-change-set-1": { From 3dd595d9950552f29f15c2d22e7948ef2b666a2a Mon Sep 17 00:00:00 2001 From: Cristopher Pinzon Date: Fri, 4 Jul 2025 17:45:13 -0500 Subject: [PATCH 13/22] better to use old code --- .../engine/v2/change_set_model_transform.py | 94 ++++++++++++++----- .../v2/test_change_set_fn_transform.py | 2 +- 2 files changed, 71 insertions(+), 25 deletions(-) diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py index 1819ec3e3ff80..ffd010554f47d 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py @@ -33,6 +33,7 @@ from localstack.services.cloudformation.stores import get_cloudformation_store from localstack.services.cloudformation.v2.entities import ChangeSet from localstack.utils import testutil +from localstack.utils.objects import recurse_object LOG = logging.getLogger(__name__) @@ -291,17 +292,53 @@ def transform(self) -> tuple[dict, dict]: def _execute_embedded_transformations( self, template, resolved_parameters ) -> PreprocEntityDelta: - transformations = self._find_fn_transforms(template) - normalized_transformations = self._normalize_transform_definitions(transformations) - - transformed_template = copy.deepcopy(template) - for transformation in normalized_transformations: - transformed_template = self._execute_embedded_transformation( - transformation=transformation, - template=transformed_template, - resolved_parameters=resolved_parameters, - ) - return transformed_template + account_id = self._change_set.account_id + region_name = self._change_set.region_name + stack_name = self._change_set.change_set_name + + def _visit(obj, path, **_): + if isinstance(obj, dict) and "Fn::Transform" in obj: + transform = ( + obj["Fn::Transform"] + if isinstance(obj["Fn::Transform"], dict) + else {"Name": obj["Fn::Transform"]} + ) + transform_name = transform.get("Name") + transformer_class = transformers.get(transform_name) + macro_store = get_cloudformation_store( + account_id=account_id, region_name=region_name + ).macros + parameters = transform.get("Parameters") or {} + stack_parameters = resolved_parameters + + if transformer_class: + transformer = transformer_class() + transformed = transformer.transform(account_id, region_name, parameters) + obj_copy = copy.deepcopy(obj) + obj_copy.pop("Fn::Transform") + obj_copy.update(transformed) + return obj_copy + + elif transform_name in macro_store: + obj_copy = copy.deepcopy(obj) + obj_copy.pop("Fn::Transform") + result = execute_macro( + account_id, + region_name, + obj_copy, + transform, + stack_parameters, + parameters, + True, + ) + return result + else: + LOG.warning( + "Unsupported transform function '%s' used in %s", transform_name, stack_name + ) + return obj + + return recurse_object(template, _visit) @staticmethod def _normalize_transform_definitions( @@ -339,19 +376,28 @@ def _execute_embedded_transformation( ).macros def _apply_transform_on_template(scope, template, transformation_result, include=False): - node = template - prev_node = node - for key in scope.split("/")[:-1]: - prev_node = node - node = node[key] - - if include and isinstance(prev_node, dict): - del node["Fn::Transform"] - prev_node[key].update(transformation_result) - else: - prev_node[key] = transformation_result - - return template + # node = template + # prev_node = node + # for key in scope.split("/")[:-1]: + # prev_node = node + # node = node[key] + # + # if include and isinstance(prev_node, dict): + # del node["Fn::Transform"] + # prev_node[key].update(transformation_result) + # else: + # prev_node[key] = transformation_result + # + # return template + scope = scope.replace("/", ".") + + def _visit(obj, path): + if path == scope: + return transformation_result + return obj + + t = recurse_object(template, _visit) + return t scope = transformation[1] transform_name = transformation[0]["Name"] diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py index e194608bb90c7..d67f74cc89476 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py @@ -427,7 +427,7 @@ def test_conditional_transform( capture_update_process(snapshot, template_1, template_2, p2={"Transform": transform}) @markers.aws.validated - def test_macro_with_intrisic_function( + def test_macro_with_intrinsic_function( self, snapshot, capture_update_process, From 6999639d9063f2a1fe3b0c6e5b26a1d657cf389c Mon Sep 17 00:00:00 2001 From: Cristopher Pinzon Date: Mon, 14 Jul 2025 16:28:09 -0500 Subject: [PATCH 14/22] support array of transformations --- .../engine/v2/change_set_model_transform.py | 109 ++++++++++-------- 1 file changed, 63 insertions(+), 46 deletions(-) diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py index ffd010554f47d..e4471065b16ca 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py @@ -250,6 +250,9 @@ def transform(self) -> tuple[dict, dict]: transform_after: Maybe[list[GlobalTransform]] = transform_delta.after transformed_before_template = self._before_template + transformed_before_template = self._execute_embedded_transformations( + template=transformed_before_template, resolved_parameters=parameters_before + ) if transform_before and not is_nothing(self._before_template): transformed_before_template = self._before_cache.get(_SCOPE_TRANSFORM_TEMPLATE_OUTCOME) if not transformed_before_template: @@ -264,6 +267,9 @@ def transform(self) -> tuple[dict, dict]: self._before_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = transformed_before_template transformed_after_template = self._after_template + transformed_after_template = self._execute_embedded_transformations( + template=transformed_after_template, resolved_parameters=parameters_after + ) if transform_after and not is_nothing(self._after_template): transformed_after_template = self._after_cache.get(_SCOPE_TRANSFORM_TEMPLATE_OUTCOME) if not transformed_after_template: @@ -279,14 +285,6 @@ def transform(self) -> tuple[dict, dict]: self._save_runtime_cache() - ### Handle Embedded Fn::Transform - transformed_before_template = self._execute_embedded_transformations( - template=transformed_before_template, resolved_parameters=parameters_before - ) - transformed_after_template = self._execute_embedded_transformations( - template=transformed_after_template, resolved_parameters=parameters_after - ) - return transformed_before_template, transformed_after_template def _execute_embedded_transformations( @@ -296,46 +294,65 @@ def _execute_embedded_transformations( region_name = self._change_set.region_name stack_name = self._change_set.change_set_name + def _normalize_transform(obj): + transforms = [] + + if isinstance(obj, str): + transforms.append({"Name": obj, "Parameters": {}}) + + if isinstance(obj, dict): + transforms.append(obj) + + if isinstance(obj, list): + for v in obj: + if isinstance(v, str): + transforms.append({"Name": v, "Parameters": {}}) + + if isinstance(v, dict): + transforms.append(v) + + return transforms + def _visit(obj, path, **_): if isinstance(obj, dict) and "Fn::Transform" in obj: - transform = ( - obj["Fn::Transform"] - if isinstance(obj["Fn::Transform"], dict) - else {"Name": obj["Fn::Transform"]} - ) - transform_name = transform.get("Name") - transformer_class = transformers.get(transform_name) - macro_store = get_cloudformation_store( - account_id=account_id, region_name=region_name - ).macros - parameters = transform.get("Parameters") or {} - stack_parameters = resolved_parameters - - if transformer_class: - transformer = transformer_class() - transformed = transformer.transform(account_id, region_name, parameters) - obj_copy = copy.deepcopy(obj) - obj_copy.pop("Fn::Transform") - obj_copy.update(transformed) - return obj_copy - - elif transform_name in macro_store: - obj_copy = copy.deepcopy(obj) - obj_copy.pop("Fn::Transform") - result = execute_macro( - account_id, - region_name, - obj_copy, - transform, - stack_parameters, - parameters, - True, - ) - return result - else: - LOG.warning( - "Unsupported transform function '%s' used in %s", transform_name, stack_name - ) + transforms = _normalize_transform(obj["Fn::Transform"]) + + for transform in transforms: + transform_name = transform.get("Name") + transformer_class = transformers.get(transform_name) + macro_store = get_cloudformation_store( + account_id=account_id, region_name=region_name + ).macros + parameters = transform.get("Parameters") or {} + stack_parameters = resolved_parameters + + if transformer_class: + transformer = transformer_class() + transformed = transformer.transform(account_id, region_name, parameters) + obj_copy = copy.deepcopy(obj) + obj_copy.pop("Fn::Transform", "") + obj_copy.update(transformed) + obj = obj_copy + + elif transform_name in macro_store: + obj_copy = copy.deepcopy(obj) + obj_copy.pop("Fn::Transform", "") + result = execute_macro( + account_id, + region_name, + obj_copy, + transform, + stack_parameters, + parameters, + True, + ) + obj = result + else: + LOG.warning( + "Unsupported transform function '%s' used in %s", + transform_name, + stack_name, + ) return obj return recurse_object(template, _visit) From 7234537532548c7727b1b21d6cd8bba47de29299 Mon Sep 17 00:00:00 2001 From: Cristopher Pinzon Date: Tue, 15 Jul 2025 13:26:35 -0500 Subject: [PATCH 15/22] resolving of macro parameters --- .../engine/v2/change_set_model_transform.py | 140 +++--------------- 1 file changed, 23 insertions(+), 117 deletions(-) diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py index e4471065b16ca..032aa6426797e 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py @@ -12,7 +12,7 @@ from localstack.services.cloudformation.engine.template_preparer import parse_template from localstack.services.cloudformation.engine.transformers import ( FailedTransformationException, - Transformer, + ResolveRefsRecursivelyContext, execute_macro, transformers, ) @@ -249,9 +249,8 @@ def transform(self) -> tuple[dict, dict]: transform_before: Maybe[list[GlobalTransform]] = transform_delta.before transform_after: Maybe[list[GlobalTransform]] = transform_delta.after - transformed_before_template = self._before_template transformed_before_template = self._execute_embedded_transformations( - template=transformed_before_template, resolved_parameters=parameters_before + template=self._before_template, resolved_parameters=parameters_before ) if transform_before and not is_nothing(self._before_template): transformed_before_template = self._before_cache.get(_SCOPE_TRANSFORM_TEMPLATE_OUTCOME) @@ -266,9 +265,8 @@ def transform(self) -> tuple[dict, dict]: ) self._before_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = transformed_before_template - transformed_after_template = self._after_template transformed_after_template = self._execute_embedded_transformations( - template=transformed_after_template, resolved_parameters=parameters_after + template=self._after_template, resolved_parameters=parameters_after ) if transform_after and not is_nothing(self._after_template): transformed_after_template = self._after_cache.get(_SCOPE_TRANSFORM_TEMPLATE_OUTCOME) @@ -313,6 +311,25 @@ def _normalize_transform(obj): return transforms + def _resolve_macro_parameters(macro_parameters: dict, stack_parameters): + resolved_parameters = {} + resolve_refs_recursively = ResolveRefsRecursivelyContext( + account_id=self._change_set.account_id, + region_name=self._change_set.region_name, + stack_name=self._change_set.change_set_name, + resources=self._change_set.template.get("Resources", {}), + mappings=self._change_set.template.get("Mappings", {}), + conditions=self._change_set.template.get("Conditions", {}), + parameters=stack_parameters, + ) + + for parameter_key, parameter_value in macro_parameters.items(): + if isinstance(parameter_value, dict): + parameter_value = resolve_refs_recursively.resolve(parameter_value) + resolved_parameters[parameter_key] = parameter_value + + return resolved_parameters + def _visit(obj, path, **_): if isinstance(obj, dict) and "Fn::Transform" in obj: transforms = _normalize_transform(obj["Fn::Transform"]) @@ -343,7 +360,7 @@ def _visit(obj, path, **_): obj_copy, transform, stack_parameters, - parameters, + _resolve_macro_parameters(parameters, stack_parameters), True, ) obj = result @@ -357,117 +374,6 @@ def _visit(obj, path, **_): return recurse_object(template, _visit) - @staticmethod - def _normalize_transform_definitions( - transform_definitions: list[(any, str)], - ) -> dict: - def _normalize_individual_transform(transform_def: str | dict): - # TODO: validate parameters, imports, refs to resources or conditionals are not supported. - # only literals, refs to parameters and basic intrinsic functions like sub and join, posibly select - if isinstance(transform_def, str): - return {"Name": transform_def, "Parameters": {}} - - if isinstance(transform_def, dict): - return { - "Name": transform_def["Name"], - "Parameters": transform_def.get("Parameters", {}), - } - - raise FailedTransformationException("Invalid Definition of transformation") - - normalized_transforms = [] - for path, value in transform_definitions: - if isinstance(value, list): - for transform in value: - normalized_transforms.append((_normalize_individual_transform(transform), path)) - else: - normalized_transforms.append((_normalize_individual_transform(value), path)) - - return normalized_transforms - - def _execute_embedded_transformation( - self, transformation: (dict, str), template: dict, resolved_parameters: dict - ) -> dict: - macros_store = get_cloudformation_store( - account_id=self._change_set.account_id, region_name=self._change_set.region_name - ).macros - - def _apply_transform_on_template(scope, template, transformation_result, include=False): - # node = template - # prev_node = node - # for key in scope.split("/")[:-1]: - # prev_node = node - # node = node[key] - # - # if include and isinstance(prev_node, dict): - # del node["Fn::Transform"] - # prev_node[key].update(transformation_result) - # else: - # prev_node[key] = transformation_result - # - # return template - scope = scope.replace("/", ".") - - def _visit(obj, path): - if path == scope: - return transformation_result - return obj - - t = recurse_object(template, _visit) - return t - - scope = transformation[1] - transform_name = transformation[0]["Name"] - transform_parameters = transformation[0]["Parameters"] - - if transform_name in transformers: - builtin_transformer_class = transformers[transform_name] - builtin_transformer: Transformer = builtin_transformer_class() - transform_output: Any = builtin_transformer.transform( - account_id=self._change_set.account_id, - region_name=self._change_set.region_name, - parameters=transform_parameters, - ) - return _apply_transform_on_template(scope, template, transform_output, True) - - if transform_name in macros_store: - # A macro is only able to access their node parent and siblings - parent_node = template - for key in scope.split("/")[:-1]: - parent_node = parent_node.get(key) - - transform_output: Any = execute_macro( - account_id=self._change_set.account_id, - region_name=self._change_set.region_name, - parsed_template=parent_node, - macro=transformation[0], - stack_parameters=resolved_parameters, - transformation_parameters=transform_parameters, - is_intrinsic=True, - ) - return _apply_transform_on_template(scope, template, transform_output) - raise FailedTransformationException("Macro not found") - - def _find_fn_transforms(self, obj, path=None) -> list[(any, str)]: - if path is None: - path = [] - - results = [] - - if isinstance(obj, dict): - for key, value in obj.items(): - current_path = path + [key] - if key == "Fn::Transform": - results.append(("/".join(current_path), value)) - results.extend(self._find_fn_transforms(value, current_path)) - - elif isinstance(obj, list): - for idx, item in enumerate(obj): - current_path = path + [f"{idx}"] - results.extend(self._find_fn_transforms(item, current_path)) - - return results - def visit_node_global_transform( self, node_global_transform: NodeGlobalTransform ) -> PreprocEntityDelta[GlobalTransform, GlobalTransform]: From 3178df921f9ecad2d75e223d4cf0305adb154bf0 Mon Sep 17 00:00:00 2001 From: Cristopher Pinzon Date: Wed, 16 Jul 2025 09:24:00 -0500 Subject: [PATCH 16/22] fix deletion --- .../localstack/services/cloudformation/v2/provider.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/localstack-core/localstack/services/cloudformation/v2/provider.py b/localstack-core/localstack/services/cloudformation/v2/provider.py index c734002b98eb6..c7d042fb28f61 100644 --- a/localstack-core/localstack/services/cloudformation/v2/provider.py +++ b/localstack-core/localstack/services/cloudformation/v2/provider.py @@ -1,5 +1,4 @@ import copy -import json import logging from collections import defaultdict from datetime import datetime, timezone @@ -800,7 +799,7 @@ def delete_stack( change_set = ChangeSet(stack, {"ChangeSetName": f"delete-stack_{stack.stack_name}"}) # noqa self._setup_change_set_model( change_set=change_set, - before_template=json.loads(stack.template_body or "{}"), + before_template=template_preparer.parse_template(template=stack.template_body), after_template=None, before_parameters=stack.resolved_parameters, after_parameters=None, From 16652031fd1515e63d3be20aa2aa468a6e262bdf Mon Sep 17 00:00:00 2001 From: Cristopher Pinzon Date: Wed, 16 Jul 2025 10:43:35 -0500 Subject: [PATCH 17/22] resolve parameters for AWS::Include --- .../engine/v2/change_set_model_transform.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py index 032aa6426797e..9632dd5490a1a 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py @@ -324,9 +324,9 @@ def _resolve_macro_parameters(macro_parameters: dict, stack_parameters): ) for parameter_key, parameter_value in macro_parameters.items(): - if isinstance(parameter_value, dict): - parameter_value = resolve_refs_recursively.resolve(parameter_value) - resolved_parameters[parameter_key] = parameter_value + resolved_parameters[parameter_key] = resolve_refs_recursively.resolve( + parameter_value + ) return resolved_parameters @@ -345,7 +345,13 @@ def _visit(obj, path, **_): if transformer_class: transformer = transformer_class() - transformed = transformer.transform(account_id, region_name, parameters) + transformed = transformer.transform( + account_id, + region_name, + _resolve_macro_parameters( + macro_parameters=parameters, stack_parameters=stack_parameters + ), + ) obj_copy = copy.deepcopy(obj) obj_copy.pop("Fn::Transform", "") obj_copy.update(transformed) From 9520de5f0db7aefe09b2291b0c5b8d0df0d23729 Mon Sep 17 00:00:00 2001 From: Cristopher Pinzon Date: Wed, 16 Jul 2025 13:25:33 -0500 Subject: [PATCH 18/22] restore skip --- tests/aws/services/cloudformation/test_template_engine.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/aws/services/cloudformation/test_template_engine.py b/tests/aws/services/cloudformation/test_template_engine.py index 3e0bbd33c2bdf..d039307ef5101 100644 --- a/tests/aws/services/cloudformation/test_template_engine.py +++ b/tests/aws/services/cloudformation/test_template_engine.py @@ -781,6 +781,7 @@ def test_attribute_uses_macro(self, deploy_cfn_template, create_lambda_function, assert "test-" in resulting_value @markers.aws.validated + @pytest.mark.skip(reason="Fn::Transform does not support array of transformations") def test_scope_order_and_parameters( self, deploy_cfn_template, create_lambda_function, snapshot, aws_client ): From 4b41da32092ff1bfd022950c5e7a3872aff4225c Mon Sep 17 00:00:00 2001 From: Cristopher Pinzon Date: Wed, 16 Jul 2025 15:40:57 -0500 Subject: [PATCH 19/22] more cleaning --- .../cloudformation/engine/v2/change_set_model_preproc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0b9b03b049a90..bebc7cdce158b 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 @@ -610,7 +610,7 @@ def _compute_fn_transform(self, macro_definition: dict[str, Any]) -> Any: raise RuntimeError("Invalid or missing Fn::Transform 'Parameters' argument") if transform_name in transformers: - # TODO: port and refactor this 'transformers' logic to this package.https://lmstudio.ai/ + # TODO: port and refactor this 'transformers' logic to this package. builtin_transformer_class = transformers[transform_name] builtin_transformer: Transformer = builtin_transformer_class() transform_output: Any = builtin_transformer.transform( From b8f42ca41785573b8263042e70f73bc6f775ae5c Mon Sep 17 00:00:00 2001 From: Simon Walker Date: Tue, 22 Jul 2025 17:15:31 +0100 Subject: [PATCH 20/22] Paired changes --- .../engine/v2/change_set_model.py | 89 +++- .../engine/v2/change_set_model_preproc.py | 152 ++++--- .../engine/v2/change_set_model_transform.py | 10 + .../v2/test_change_set_fn_transform.py | 40 ++ ...test_change_set_fn_transform.snapshot.json | 391 ++++++++++++++++++ ...st_change_set_fn_transform.validation.json | 9 + 6 files changed, 604 insertions(+), 87 deletions(-) diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py index ce0cd63f00912..c74aadcd6d898 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py @@ -401,6 +401,25 @@ def __init__( self.arguments = arguments +class NodeIntrinsicFunctionTransform(NodeIntrinsicFunction): + siblings: Final[list[ChangeSetEntity]] + + def __init__( + self, + scope: Scope, + change_type: ChangeType, + arguments: ChangeSetEntity, + siblings: list[ChangeSetEntity], + ): + super().__init__( + scope=scope, + change_type=change_type, + intrinsic_function=FnTransform, + arguments=arguments, + ) + self.siblings = siblings + + class NodeObject(ChangeSetNode): bindings: Final[dict[str, ChangeSetEntity]] @@ -580,12 +599,20 @@ def _visit_intrinsic_function( change_type = resolve_function(arguments) else: change_type = arguments.change_type - node_intrinsic_function = NodeIntrinsicFunction( - scope=scope, - change_type=change_type, - intrinsic_function=intrinsic_function, - arguments=arguments, - ) + if intrinsic_function == FnTransform: + node_intrinsic_function = NodeIntrinsicFunctionTransform( + scope=scope, + change_type=change_type, + arguments=arguments, + siblings=[], + ) + else: + node_intrinsic_function = NodeIntrinsicFunction( + scope=scope, + change_type=change_type, + intrinsic_function=intrinsic_function, + arguments=arguments, + ) self._visited_scopes[scope] = node_intrinsic_function return node_intrinsic_function @@ -834,17 +861,33 @@ def _visit_properties( return node_properties property_names: list[str] = self._safe_keys_of(before_properties, after_properties) properties: list[NodeProperty] = list() + transform_node: NodeIntrinsicFunctionTransform | None = None + for property_name in property_names: property_scope, (before_property, after_property) = self._safe_access_in( scope, property_name, before_properties, after_properties ) - property_ = self._visit_property( - scope=property_scope, - property_name=property_name, - before_property=before_property, - after_property=after_property, - ) + if property_name == FnTransform: + transform_node = self._visit_intrinsic_function( + scope=property_scope, + intrinsic_function=property_name, + before_arguments=before_property, + after_arguments=after_property, + ) + continue + else: + property_ = self._visit_property( + scope=property_scope, + property_name=property_name, + before_property=before_property, + after_property=after_property, + ) properties.append(property_) + + if transform_node: + transform_node.siblings = properties + properties.append(transform_node) + node_properties = NodeProperties(scope=scope, properties=properties) self._visited_scopes[scope] = node_properties return node_properties @@ -920,18 +963,26 @@ def _visit_resources( self, scope: Scope, before_resources: Maybe[dict], after_resources: Maybe[dict] ) -> NodeResources: # TODO: investigate type changes behavior. - resources: list[NodeResource] = list() + resources: list[NodeResource | NodeIntrinsicFunction] = list() resource_names = self._safe_keys_of(before_resources, after_resources) for resource_name in resource_names: resource_scope, (before_resource, after_resource) = self._safe_access_in( scope, resource_name, before_resources, after_resources ) - resource = self._visit_resource( - scope=resource_scope, - resource_name=resource_name, - before_resource=before_resource, - after_resource=after_resource, - ) + if resource_name == FnTransform: + resource = self._visit_intrinsic_function( + scope=resource_scope, + intrinsic_function=resource_name, + before_arguments=before_resource, + after_arguments=after_resource, + ) + else: + resource = self._visit_resource( + scope=resource_scope, + resource_name=resource_name, + before_resource=before_resource, + after_resource=after_resource, + ) resources.append(resource) return NodeResources(scope=scope, resources=resources) 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 bebc7cdce158b..ceeb7eb3215bb 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 @@ -23,6 +23,7 @@ NodeDependsOn, NodeDivergence, NodeIntrinsicFunction, + NodeIntrinsicFunctionTransform, NodeMapping, NodeObject, NodeOutput, @@ -595,86 +596,88 @@ def _compute_fn_not(arg: bool) -> bool: ) return delta - def _compute_fn_transform(self, macro_definition: dict[str, Any]) -> Any: + def _compute_fn_transform(self, macro_definition: list[Any] | dict[str, Any]) -> Any: # TODO: add typing to arguments before this level. # TODO: add schema validation # TODO: add support for other transform types account_id = self._change_set.account_id region_name = self._change_set.region_name - transform_name: str = macro_definition.get("Name") - if not isinstance(transform_name, str): - raise RuntimeError("Invalid or missing Fn::Transform 'Name' argument") - transform_parameters: dict = macro_definition.get("Parameters") - if not isinstance(transform_parameters, dict): - raise RuntimeError("Invalid or missing Fn::Transform 'Parameters' argument") - - if transform_name in transformers: - # TODO: port and refactor this 'transformers' logic to this package. - builtin_transformer_class = transformers[transform_name] - builtin_transformer: Transformer = builtin_transformer_class() - transform_output: Any = builtin_transformer.transform( - account_id=account_id, region_name=region_name, parameters=transform_parameters - ) - return transform_output - - macros_store = get_cloudformation_store( - account_id=account_id, region_name=region_name - ).macros - if transform_name in macros_store: - # TODO: this formatting of stack parameters is odd but required to integrate with v1 execute_macro util. - # consider porting this utils and passing the plain list of parameters instead. - - resolved_parameters = { - **self._change_set.stack.resolved_parameters, - **self.visit_node_parameters( - self._change_set.update_model.node_template.parameters - ).after, - } - - template_parameters = { - **self._change_set.stack.template.get("Parameters", {}), - **(self._change_set.template or {}).get("Parameters", {}), - } - - for key, value in resolved_parameters.items(): - template_parameters[key]["ParameterValue"] = value - - # -2 removes: divergence/Fn::transform, -3 returns the path until the parent - scope = macro_definition.get("Scope").split("/")[:-3] - template = self._change_set.template or {} - for key in scope: - if key: - template = template.get(key) - - transform_output: Any = execute_macro( - account_id=account_id, - region_name=region_name, - parsed_template=template, # TODO: review the requirements for this argument. - macro=macro_definition, # TODO: review support for non dict bindings (v1). - stack_parameters=template_parameters, - transformation_parameters=macro_definition.get("Parameters"), - is_intrinsic=True, + + def _invoke_macro(defn: dict) -> Any: + transform_name: str = defn.get("Name") + if not isinstance(transform_name, str): + raise RuntimeError("Invalid or missing Fn::Transform 'Name' argument") + transform_parameters: dict = defn.get("Parameters") + if not isinstance(transform_parameters, dict): + raise RuntimeError("Invalid or missing Fn::Transform 'Parameters' argument") + + if transform_name in transformers: + # TODO: port and refactor this 'transformers' logic to this package. + builtin_transformer_class = transformers[transform_name] + builtin_transformer: Transformer = builtin_transformer_class() + transform_output: Any = builtin_transformer.transform( + account_id=account_id, region_name=region_name, parameters=transform_parameters + ) + return transform_output + + macros_store = get_cloudformation_store( + account_id=account_id, region_name=region_name + ).macros + if transform_name in macros_store: + # TODO: this formatting of stack parameters is odd but required to integrate with v1 execute_macro util. + # consider porting this utils and passing the plain list of parameters instead. + + resolved_parameters = { + **self._change_set.stack.resolved_parameters, + **self.visit_node_parameters( + self._change_set.update_model.node_template.parameters + ).after, + } + + template_parameters = { + **self._change_set.stack.template.get("Parameters", {}), + **(self._change_set.template or {}).get("Parameters", {}), + } + + for key, value in resolved_parameters.items(): + template_parameters[key]["ParameterValue"] = value + + # -2 removes: divergence/Fn::transform, -3 returns the path until the parent + scope = defn.get("Scope").split("/")[:-3] + template = self._change_set.template or {} + for key in scope: + if key: + template = template.get(key) + + transform_output: Any = execute_macro( + account_id=account_id, + region_name=region_name, + parsed_template=template, # TODO: review the requirements for this argument. + macro=defn, # TODO: review support for non dict bindings (v1). + stack_parameters=template_parameters, + transformation_parameters=defn.get("Parameters"), + is_intrinsic=True, + ) + return transform_output + + raise RuntimeError( + f"Unsupported transform function '{transform_name}' in '{self._change_set.stack.stack_name}'" ) - return transform_output - raise RuntimeError( - f"Unsupported transform function '{transform_name}' in '{self._change_set.stack.stack_name}'" - ) + if isinstance(macro_definition, list): + output = {} + for macro in macro_definition: + output.update(**_invoke_macro(macro)) + return output + else: + return _invoke_macro(macro_definition) def visit_node_intrinsic_function_fn_transform( - self, node_intrinsic_function: NodeIntrinsicFunction + self, node_intrinsic_function: NodeIntrinsicFunctionTransform ) -> PreprocEntityDelta: arguments_delta = self.visit(node_intrinsic_function.arguments) - # TODO find better manage of this - # The scope of a transformation is important to determine the template passed to the macro - if arguments_delta.before: - arguments_delta.before["Scope"] = node_intrinsic_function.scope - - if arguments_delta.after: - arguments_delta.after["Scope"] = node_intrinsic_function.scope - delta = self._cached_apply( scope=node_intrinsic_function.scope, arguments_delta=arguments_delta, @@ -1052,9 +1055,22 @@ def visit_node_properties( node_change_type = node_properties.change_type before_bindings = dict() if node_change_type != ChangeType.CREATED else Nothing after_bindings = dict() if node_change_type != ChangeType.REMOVED else Nothing + + # TODO: handle transforms first + transform_node = next( + ( + property + for property in node_properties.properties + if isinstance(property, NodeIntrinsicFunctionTransform) + ), + None, + ) + if transform_node: + delta = self.visit_node_intrinsic_function_fn_transform(transform_node) + for node_property in node_properties.properties: - property_name = node_property.name delta = self.visit(node_property) + property_name = node_property.name delta_before = delta.before delta_after = delta.after if ( diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py index 9632dd5490a1a..a1380368dbc0d 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py @@ -20,6 +20,7 @@ ChangeType, Maybe, NodeGlobalTransform, + NodeIntrinsicFunction, NodeParameter, NodeTransform, Nothing, @@ -243,6 +244,10 @@ def transform(self) -> tuple[dict, dict]: parameters_before = parameters_delta.before parameters_after = parameters_delta.after + # TODO: using Fn::Transform in the top level scope + # visit the resources to resolve local transforms + self.visit(node_template.resources) + transform_delta: PreprocEntityDelta[list[GlobalTransform], list[GlobalTransform]] = ( self.visit_node_transform(node_template.transform) ) @@ -285,6 +290,11 @@ def transform(self) -> tuple[dict, dict]: return transformed_before_template, transformed_after_template + def visit_node_intrinsic_function_fn_transform( + self, node_intrinsic_function: NodeIntrinsicFunction + ) -> PreprocEntityDelta: + return super().visit_node_intrinsic_function_fn_transform(node_intrinsic_function) + def _execute_embedded_transformations( self, template, resolved_parameters ) -> PreprocEntityDelta: diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py index d67f74cc89476..80eb99915052a 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py @@ -325,6 +325,46 @@ def test_embedded_macro_for_attribute_fn_transform( capture_update_process(snapshot, template_1, template_2, p2={"Input": "test"}) + @markers.aws.validated + def test_embedded_macro_for_attribute_fn_transform_with_updates( + self, + snapshot, + capture_update_process, + create_macro, + ): + name1 = f"parameter-{short_uid()}" + snapshot.add_transformer(RegexTransformer(name1, "parameter-name")) + snapshot.add_transformer(snapshot.transform.key_value("Value", "value")) + + macro_function_path = os.path.join( + os.path.dirname(__file__), "../../../templates/macros/return_random_string.py" + ) + macro_name = "GenerateRandom" + create_macro(macro_name, macro_function_path) + + template_1 = { + "Parameters": {"Input": {"Type": "String"}}, + "Resources": { + "Parameter": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Name": name1, + "Type": "String", + "Value": { + "Fn::Transform": { + "Name": "GenerateRandom", + "Parameters": {"Prefix": {"Ref": "Input"}}, + }, + }, + }, + } + }, + } + + capture_update_process( + snapshot, template_1, template_1.copy(), p1={"Input": "test"}, p2={"Input": "test"} + ) + @markers.aws.validated def test_multiple_fn_transform_order( self, diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json index 4f75442d6fb34..ec10b9e323c37 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.snapshot.json @@ -5743,5 +5743,396 @@ ] } } + }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_macro_for_attribute_fn_transform_with_updates": { + "recorded-date": "22-07-2025, 14:50:56", + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "AfterContext": { + "Properties": { + "Value": "", + "Type": "String", + "Name": "parameter-name" + } + }, + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Input", + "ParameterValue": "test" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-1": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "Parameter", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Input", + "ParameterValue": "test" + } + ], + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Input", + "ParameterValue": "test" + } + ], + "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": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "AfterContext": { + "Properties": { + "Value": "", + "Type": "String", + "Name": "parameter-name" + } + }, + "BeforeContext": { + "Properties": { + "Value": "", + "Type": "String", + "Name": "parameter-name" + } + }, + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "AfterValue": "", + "Attribute": "Properties", + "AttributeChangeType": "Modify", + "BeforeValue": "", + "Name": "Value", + "Path": "/Properties/Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Input", + "ParameterValue": "test" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-change-set-2": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Modify", + "Details": [ + { + "ChangeSource": "DirectModification", + "Evaluation": "Static", + "Target": { + "Attribute": "Properties", + "Name": "Value", + "RequiresRecreation": "Never" + } + } + ], + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "Replacement": "False", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [ + "Properties" + ] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Input", + "ParameterValue": "test" + } + ], + "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": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Input", + "ParameterValue": "test" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "UPDATE_COMPLETE", + "Tags": [] + }, + "delete-describe": { + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND" + ], + "CreationTime": "datetime", + "DeletionTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "Parameters": [ + { + "ParameterKey": "Input", + "ParameterValue": "test" + } + ], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "DELETE_COMPLETE", + "Tags": [] + }, + "per-resource-events": { + "Parameter": [ + { + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::SSM::Parameter", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "Parameter", + "PhysicalResourceId": "parameter-name", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::SSM::Parameter", + "Timestamp": "timestamp" + } + ], + "Stack": [ + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "REVIEW_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_IN_PROGRESS", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + }, + { + "LogicalResourceId": "", + "PhysicalResourceId": "arn::cloudformation::111111111111:stack//", + "ResourceStatus": "UPDATE_COMPLETE", + "ResourceType": "AWS::CloudFormation::Stack", + "Timestamp": "timestamp" + } + ] + } + } } } diff --git a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json index 6b99fa98aabf8..5b3c277621524 100644 --- a/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json +++ b/tests/aws/services/cloudformation/v2/test_change_set_fn_transform.validation.json @@ -62,6 +62,15 @@ "total": 42.17 } }, + "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_embedded_macro_for_attribute_fn_transform_with_updates": { + "last_validated_date": "2025-07-22T14:50:57+00:00", + "durations_in_seconds": { + "setup": 12.29, + "call": 93.66, + "teardown": 6.07, + "total": 112.02 + } + }, "tests/aws/services/cloudformation/v2/test_change_set_fn_transform.py::TestChangeSetFnTransform::test_global_fn_transform_include[json]": { "last_validated_date": "2025-06-03T21:30:48+00:00", "durations_in_seconds": { From 522858daa22afcdc144b9c6a52956b9c4cbe8b52 Mon Sep 17 00:00:00 2001 From: Simon Walker Date: Wed, 23 Jul 2025 17:38:26 +0100 Subject: [PATCH 21/22] WIP --- .../engine/v2/change_set_model.py | 47 +++++++--- .../engine/v2/change_set_model_preproc.py | 93 ++++++++++++++----- .../engine/v2/change_set_model_transform.py | 47 +++++++--- 3 files changed, 141 insertions(+), 46 deletions(-) diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py index c74aadcd6d898..900871e8ff72c 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py @@ -7,6 +7,7 @@ from typing_extensions import TypeVar +from localstack.utils.json import extract_jsonpath from localstack.utils.strings import camel_to_snake_case T = TypeVar("T") @@ -99,6 +100,9 @@ def open_index(self, index: int) -> Scope: def unwrap(self) -> list[str]: return self.split(self._SEPARATOR) + def to_jsonpath(self) -> str: + pass + class ChangeType(enum.Enum): UNCHANGED = "Unchanged" @@ -274,7 +278,7 @@ def __init__( class NodeOutputs(ChangeSetNode): - outputs: Final[list[NodeOutput]] + outputs: Final[list[NodeOutput | NodeIntrinsicFunctionTransform]] def __init__(self, scope: Scope, outputs: list[NodeOutput]): change_type = parent_change_type_of(outputs) @@ -402,14 +406,13 @@ def __init__( class NodeIntrinsicFunctionTransform(NodeIntrinsicFunction): - siblings: Final[list[ChangeSetEntity]] - def __init__( self, scope: Scope, change_type: ChangeType, arguments: ChangeSetEntity, - siblings: list[ChangeSetEntity], + before_siblings: list[Any], + after_siblings: list[Any], ): super().__init__( scope=scope, @@ -417,7 +420,8 @@ def __init__( intrinsic_function=FnTransform, arguments=arguments, ) - self.siblings = siblings + self.before_siblings = before_siblings + self.after_siblings = after_siblings class NodeObject(ChangeSetNode): @@ -599,12 +603,21 @@ def _visit_intrinsic_function( change_type = resolve_function(arguments) else: change_type = arguments.change_type + if intrinsic_function == FnTransform: + if scope_has_component_component("Fn::Transform"): + raise RuntimeError("Nested Fn::Transforms are bad") + + path = "$" + ".".join(scope.split("/")[:-1]) + before_siblings = extract_jsonpath(self._before_template, path) + after_siblings = extract_jsonpath(self._after_template, path) + node_intrinsic_function = NodeIntrinsicFunctionTransform( scope=scope, change_type=change_type, arguments=arguments, - siblings=[], + before_siblings=before_siblings, + after_siblings=after_siblings, ) else: node_intrinsic_function = NodeIntrinsicFunction( @@ -1184,18 +1197,26 @@ def _visit_output( def _visit_outputs( self, scope: Scope, before_outputs: Maybe[dict], after_outputs: Maybe[dict] ) -> NodeOutputs: - outputs: list[NodeOutput] = list() + outputs: list[NodeOutput | NodeIntrinsicFunctionTransform] = list() output_names: list[str] = self._safe_keys_of(before_outputs, after_outputs) for output_name in output_names: scope_output, (before_output, after_output) = self._safe_access_in( scope, output_name, before_outputs, after_outputs ) - output = self._visit_output( - scope=scope_output, - name=output_name, - before_output=before_output, - after_output=after_output, - ) + if output_name == FnTransform: + output = self._visit_intrinsic_function( + scope=scope_output, + intrinsic_function=output_name, + before_arguments=before_output, + after_arguments=after_output, + ) + else: + output = self._visit_output( + scope=scope_output, + name=output_name, + before_output=before_output, + after_output=after_output, + ) outputs.append(output) return NodeOutputs(scope=scope, outputs=outputs) 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 ceeb7eb3215bb..6e6e252e41019 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 @@ -596,7 +596,9 @@ def _compute_fn_not(arg: bool) -> bool: ) return delta - def _compute_fn_transform(self, macro_definition: list[Any] | dict[str, Any]) -> Any: + def _compute_fn_transform( + self, macro_definition: Any, siblings: Any, resolved_parameters: Any + ) -> Any: # TODO: add typing to arguments before this level. # TODO: add schema validation # TODO: add support for other transform types @@ -604,6 +606,74 @@ def _compute_fn_transform(self, macro_definition: list[Any] | dict[str, Any]) -> account_id = self._change_set.account_id region_name = self._change_set.region_name + def _normalize_transform(obj): + transforms = [] + + if isinstance(obj, str): + transforms.append({"Name": obj, "Parameters": {}}) + + if isinstance(obj, dict): + transforms.append(obj) + + if isinstance(obj, list): + for v in obj: + if isinstance(v, str): + transforms.append({"Name": v, "Parameters": {}}) + + if isinstance(v, dict): + transforms.append(v) + + return transforms + + transforms = _normalize_transform(macro_definition) + for transform in transforms: + transform_name = transform["Name"] + if transform_name in transforms: + builtin_transformer_class = transformers[transform_name] + builtin_transformer: Transformer = builtin_transformer_class() + transform_output: Any = builtin_transformer.transform( + account_id=account_id, region_name=region_name, parameters=transform_parameters + ) + raise RuntimeError("TODO") + else: + macros_store = get_cloudformation_store( + account_id=account_id, region_name=region_name + ).macros + + resolved_parameters = { + **self._change_set.stack.resolved_parameters, + **self.visit_node_parameters( + self._change_set.update_model.node_template.parameters + ).after, + } + + template_parameters = { + **self._change_set.stack.template.get("Parameters", {}), + **(self._change_set.template or {}).get("Parameters", {}), + } + + for key, value in resolved_parameters.items(): + template_parameters[key]["ParameterValue"] = value + + # -2 removes: divergence/Fn::transform, -3 returns the path until the parent + scope = macro_definition.get("Scope").split("/")[:-3] + template = self._change_set.template or {} + for key in scope: + if key: + template = template.get(key) + + transform_output: Any = execute_macro( + account_id=account_id, + region_name=region_name, + parsed_template=template, # TODO: review the requirements for this argument. + macro=macro_definition, # TODO: review support for non dict bindings (v1). + stack_parameters=template_parameters, + transformation_parameters=macro_definition.get("Parameters"), + is_intrinsic=True, + ) + + return transform_output + def _invoke_macro(defn: dict) -> Any: transform_name: str = defn.get("Name") if not isinstance(transform_name, str): @@ -676,14 +746,7 @@ def _invoke_macro(defn: dict) -> Any: def visit_node_intrinsic_function_fn_transform( self, node_intrinsic_function: NodeIntrinsicFunctionTransform ) -> PreprocEntityDelta: - arguments_delta = self.visit(node_intrinsic_function.arguments) - - delta = self._cached_apply( - scope=node_intrinsic_function.scope, - arguments_delta=arguments_delta, - resolver=self._compute_fn_transform, - ) - return delta + raise RuntimeError("Transforms should have been resolved before preprocessing the template") def visit_node_intrinsic_function_fn_sub( self, node_intrinsic_function: NodeIntrinsicFunction @@ -1056,18 +1119,6 @@ def visit_node_properties( before_bindings = dict() if node_change_type != ChangeType.CREATED else Nothing after_bindings = dict() if node_change_type != ChangeType.REMOVED else Nothing - # TODO: handle transforms first - transform_node = next( - ( - property - for property in node_properties.properties - if isinstance(property, NodeIntrinsicFunctionTransform) - ), - None, - ) - if transform_node: - delta = self.visit_node_intrinsic_function_fn_transform(transform_node) - for node_property in node_properties.properties: delta = self.visit(node_property) property_name = node_property.name diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py index a1380368dbc0d..f69e149a747a6 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py @@ -20,7 +20,7 @@ ChangeType, Maybe, NodeGlobalTransform, - NodeIntrinsicFunction, + NodeIntrinsicFunctionTransform, NodeParameter, NodeTransform, Nothing, @@ -91,6 +91,7 @@ def visit_node_parameter( # Enable compatability with v1 util. # TODO: port v1's SSM parameter resolution + # TODO: port parameter_value_delta = super().visit_node_parameter(node_parameter=node_parameter) parameter_value_before = parameter_value_delta.before parameter_value_after = parameter_value_delta.after @@ -244,9 +245,11 @@ def transform(self) -> tuple[dict, dict]: parameters_before = parameters_delta.before parameters_after = parameters_delta.after - # TODO: using Fn::Transform in the top level scope - # visit the resources to resolve local transforms + # Visit any nodes that can contain transforms self.visit(node_template.resources) + self.visit(node_template.outputs) + + # now visit global transforms transform_delta: PreprocEntityDelta[list[GlobalTransform], list[GlobalTransform]] = ( self.visit_node_transform(node_template.transform) @@ -254,9 +257,6 @@ def transform(self) -> tuple[dict, dict]: transform_before: Maybe[list[GlobalTransform]] = transform_delta.before transform_after: Maybe[list[GlobalTransform]] = transform_delta.after - transformed_before_template = self._execute_embedded_transformations( - template=self._before_template, resolved_parameters=parameters_before - ) if transform_before and not is_nothing(self._before_template): transformed_before_template = self._before_cache.get(_SCOPE_TRANSFORM_TEMPLATE_OUTCOME) if not transformed_before_template: @@ -270,9 +270,6 @@ def transform(self) -> tuple[dict, dict]: ) self._before_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = transformed_before_template - transformed_after_template = self._execute_embedded_transformations( - template=self._after_template, resolved_parameters=parameters_after - ) if transform_after and not is_nothing(self._after_template): transformed_after_template = self._after_cache.get(_SCOPE_TRANSFORM_TEMPLATE_OUTCOME) if not transformed_after_template: @@ -290,10 +287,36 @@ def transform(self) -> tuple[dict, dict]: return transformed_before_template, transformed_after_template - def visit_node_intrinsic_function_fn_transform( - self, node_intrinsic_function: NodeIntrinsicFunction + def visit_node_intrinsic_function_transform( + self, node_intrinsic_function: NodeIntrinsicFunctionTransform ) -> PreprocEntityDelta: - return super().visit_node_intrinsic_function_fn_transform(node_intrinsic_function) + arguments_delta = self.visit(node_intrinsic_function.arguments) + parameters_delta = self.visit_node_parameters( + self._change_set.update_model.node_template.parameters + ) + + # TODO: caching + + if not is_nothing(arguments_delta.before): + before = self._compute_fn_transform( + arguments_delta.before, + node_intrinsic_function.before_siblings, + parameters_delta.before, + ) + replace_at_jsonpath(self._before_template, node_intrinsic_function, before) + else: + before = Nothing + if not is_nothing(arguments_delta.after): + after = self._compute_fn_transform( + arguments_delta.after, + node_intrinsic_function.after_siblings, + parameters_delta.after, + ) + replace_at_jsonpath(self._before_template, node_intrinsic_function, before) + else: + after = Nothing + + return PreprocEntityDelta(before=before, after=after) def _execute_embedded_transformations( self, template, resolved_parameters From 90f305e83a025a9cc938c6bf16cef56346ed0dbe Mon Sep 17 00:00:00 2001 From: Cristopher Pinzon Date: Wed, 23 Jul 2025 16:45:17 -0500 Subject: [PATCH 22/22] replacement of trasformation --- .../engine/v2/change_set_model.py | 16 ++- .../engine/v2/change_set_model_preproc.py | 102 +++------------ .../engine/v2/change_set_model_transform.py | 123 ++++-------------- .../cloudformation/test_template_engine.py | 4 +- .../test_template_engine.snapshot.json | 4 +- .../test_template_engine.validation.json | 8 +- ...ransformation_multiple_scope_parameter.yml | 4 + 7 files changed, 70 insertions(+), 191 deletions(-) diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py index 900871e8ff72c..f9a0e420e448f 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py @@ -101,7 +101,19 @@ def unwrap(self) -> list[str]: return self.split(self._SEPARATOR) def to_jsonpath(self) -> str: - pass + parts = self.split("/") + json_parts = [] + + for part in parts: + if not part: # Skip empty strings from leading/trailing slashes + continue + # Wrap keys with special characters (e.g., colon) in quotes + if ":" in part: + json_parts.append(f'"{part}"') + else: + json_parts.append(part) + + return f"$.{'.'.join(json_parts)}" class ChangeType(enum.Enum): @@ -605,7 +617,7 @@ def _visit_intrinsic_function( change_type = arguments.change_type if intrinsic_function == FnTransform: - if scope_has_component_component("Fn::Transform"): + if scope.count(FnTransform) > 1: raise RuntimeError("Nested Fn::Transforms are bad") path = "$" + ".".join(scope.split("/")[:-1]) 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 6e6e252e41019..c057dea4b71ce 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 @@ -626,77 +626,27 @@ def _normalize_transform(obj): return transforms transforms = _normalize_transform(macro_definition) + transform_output = copy.deepcopy(siblings) + transform_output.pop("Fn::Transform", "") for transform in transforms: transform_name = transform["Name"] if transform_name in transforms: builtin_transformer_class = transformers[transform_name] builtin_transformer: Transformer = builtin_transformer_class() - transform_output: Any = builtin_transformer.transform( - account_id=account_id, region_name=region_name, parameters=transform_parameters + transform_output.update( + builtin_transformer.transform( + account_id=account_id, + region_name=region_name, + parameters=transform["Parameters"], + ) ) - raise RuntimeError("TODO") else: macros_store = get_cloudformation_store( account_id=account_id, region_name=region_name ).macros - resolved_parameters = { - **self._change_set.stack.resolved_parameters, - **self.visit_node_parameters( - self._change_set.update_model.node_template.parameters - ).after, - } - - template_parameters = { - **self._change_set.stack.template.get("Parameters", {}), - **(self._change_set.template or {}).get("Parameters", {}), - } - - for key, value in resolved_parameters.items(): - template_parameters[key]["ParameterValue"] = value - - # -2 removes: divergence/Fn::transform, -3 returns the path until the parent - scope = macro_definition.get("Scope").split("/")[:-3] - template = self._change_set.template or {} - for key in scope: - if key: - template = template.get(key) - - transform_output: Any = execute_macro( - account_id=account_id, - region_name=region_name, - parsed_template=template, # TODO: review the requirements for this argument. - macro=macro_definition, # TODO: review support for non dict bindings (v1). - stack_parameters=template_parameters, - transformation_parameters=macro_definition.get("Parameters"), - is_intrinsic=True, - ) - - return transform_output - - def _invoke_macro(defn: dict) -> Any: - transform_name: str = defn.get("Name") - if not isinstance(transform_name, str): - raise RuntimeError("Invalid or missing Fn::Transform 'Name' argument") - transform_parameters: dict = defn.get("Parameters") - if not isinstance(transform_parameters, dict): - raise RuntimeError("Invalid or missing Fn::Transform 'Parameters' argument") - - if transform_name in transformers: - # TODO: port and refactor this 'transformers' logic to this package. - builtin_transformer_class = transformers[transform_name] - builtin_transformer: Transformer = builtin_transformer_class() - transform_output: Any = builtin_transformer.transform( - account_id=account_id, region_name=region_name, parameters=transform_parameters - ) - return transform_output - - macros_store = get_cloudformation_store( - account_id=account_id, region_name=region_name - ).macros - if transform_name in macros_store: - # TODO: this formatting of stack parameters is odd but required to integrate with v1 execute_macro util. - # consider porting this utils and passing the plain list of parameters instead. + if transform["Name"] not in macros_store: + raise RuntimeError("Unsupported transform ") resolved_parameters = { **self._change_set.stack.resolved_parameters, @@ -713,35 +663,17 @@ def _invoke_macro(defn: dict) -> Any: for key, value in resolved_parameters.items(): template_parameters[key]["ParameterValue"] = value - # -2 removes: divergence/Fn::transform, -3 returns the path until the parent - scope = defn.get("Scope").split("/")[:-3] - template = self._change_set.template or {} - for key in scope: - if key: - template = template.get(key) - transform_output: Any = execute_macro( account_id=account_id, region_name=region_name, - parsed_template=template, # TODO: review the requirements for this argument. - macro=defn, # TODO: review support for non dict bindings (v1). + parsed_template=transform_output, # TODO: review the requirements for this argument. + macro=transform, # TODO: review support for non dict bindings (v1). stack_parameters=template_parameters, - transformation_parameters=defn.get("Parameters"), + transformation_parameters=transform.get("Parameters"), is_intrinsic=True, ) - return transform_output - raise RuntimeError( - f"Unsupported transform function '{transform_name}' in '{self._change_set.stack.stack_name}'" - ) - - if isinstance(macro_definition, list): - output = {} - for macro in macro_definition: - output.update(**_invoke_macro(macro)) - return output - else: - return _invoke_macro(macro_definition) + return transform_output def visit_node_intrinsic_function_fn_transform( self, node_intrinsic_function: NodeIntrinsicFunctionTransform @@ -1121,7 +1053,11 @@ def visit_node_properties( for node_property in node_properties.properties: delta = self.visit(node_property) - property_name = node_property.name + property_name = ( + node_property.name + if not isinstance(node_property, NodeIntrinsicFunctionTransform) + else "Fn::Transform" + ) delta_before = delta.before delta_after = delta.after if ( diff --git a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py index f69e149a747a6..ba9e896e75aaf 100644 --- a/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py +++ b/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py @@ -12,9 +12,7 @@ from localstack.services.cloudformation.engine.template_preparer import parse_template from localstack.services.cloudformation.engine.transformers import ( FailedTransformationException, - ResolveRefsRecursivelyContext, execute_macro, - transformers, ) from localstack.services.cloudformation.engine.v2.change_set_model import ( ChangeType, @@ -34,7 +32,6 @@ from localstack.services.cloudformation.stores import get_cloudformation_store from localstack.services.cloudformation.v2.entities import ChangeSet from localstack.utils import testutil -from localstack.utils.objects import recurse_object LOG = logging.getLogger(__name__) @@ -250,13 +247,13 @@ def transform(self) -> tuple[dict, dict]: self.visit(node_template.outputs) # now visit global transforms - transform_delta: PreprocEntityDelta[list[GlobalTransform], list[GlobalTransform]] = ( self.visit_node_transform(node_template.transform) ) transform_before: Maybe[list[GlobalTransform]] = transform_delta.before transform_after: Maybe[list[GlobalTransform]] = transform_delta.after + transformed_before_template = self._before_template if transform_before and not is_nothing(self._before_template): transformed_before_template = self._before_cache.get(_SCOPE_TRANSFORM_TEMPLATE_OUTCOME) if not transformed_before_template: @@ -270,6 +267,7 @@ def transform(self) -> tuple[dict, dict]: ) self._before_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = transformed_before_template + transformed_after_template = self._after_template if transform_after and not is_nothing(self._after_template): transformed_after_template = self._after_cache.get(_SCOPE_TRANSFORM_TEMPLATE_OUTCOME) if not transformed_after_template: @@ -297,122 +295,45 @@ def visit_node_intrinsic_function_transform( # TODO: caching + def _replace_at_jsonpath(template, node, result): + path = node.scope.to_jsonpath() + parent_path = ".".join(path.split(".")[:-1]) + import jsonpath_ng + + pattern = jsonpath_ng.parse(parent_path) + result_template = pattern.update(template, result) + + return result_template + if not is_nothing(arguments_delta.before): before = self._compute_fn_transform( arguments_delta.before, node_intrinsic_function.before_siblings, parameters_delta.before, ) - replace_at_jsonpath(self._before_template, node_intrinsic_function, before) + updated_before_template = _replace_at_jsonpath( + self._before_template, node_intrinsic_function, before + ) + self._after_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = updated_before_template else: before = Nothing + if not is_nothing(arguments_delta.after): after = self._compute_fn_transform( arguments_delta.after, node_intrinsic_function.after_siblings, parameters_delta.after, ) - replace_at_jsonpath(self._before_template, node_intrinsic_function, before) + updated_after_template = _replace_at_jsonpath( + self._after_template, node_intrinsic_function, after + ) + self._after_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = updated_after_template else: after = Nothing + self._save_runtime_cache() return PreprocEntityDelta(before=before, after=after) - def _execute_embedded_transformations( - self, template, resolved_parameters - ) -> PreprocEntityDelta: - account_id = self._change_set.account_id - region_name = self._change_set.region_name - stack_name = self._change_set.change_set_name - - def _normalize_transform(obj): - transforms = [] - - if isinstance(obj, str): - transforms.append({"Name": obj, "Parameters": {}}) - - if isinstance(obj, dict): - transforms.append(obj) - - if isinstance(obj, list): - for v in obj: - if isinstance(v, str): - transforms.append({"Name": v, "Parameters": {}}) - - if isinstance(v, dict): - transforms.append(v) - - return transforms - - def _resolve_macro_parameters(macro_parameters: dict, stack_parameters): - resolved_parameters = {} - resolve_refs_recursively = ResolveRefsRecursivelyContext( - account_id=self._change_set.account_id, - region_name=self._change_set.region_name, - stack_name=self._change_set.change_set_name, - resources=self._change_set.template.get("Resources", {}), - mappings=self._change_set.template.get("Mappings", {}), - conditions=self._change_set.template.get("Conditions", {}), - parameters=stack_parameters, - ) - - for parameter_key, parameter_value in macro_parameters.items(): - resolved_parameters[parameter_key] = resolve_refs_recursively.resolve( - parameter_value - ) - - return resolved_parameters - - def _visit(obj, path, **_): - if isinstance(obj, dict) and "Fn::Transform" in obj: - transforms = _normalize_transform(obj["Fn::Transform"]) - - for transform in transforms: - transform_name = transform.get("Name") - transformer_class = transformers.get(transform_name) - macro_store = get_cloudformation_store( - account_id=account_id, region_name=region_name - ).macros - parameters = transform.get("Parameters") or {} - stack_parameters = resolved_parameters - - if transformer_class: - transformer = transformer_class() - transformed = transformer.transform( - account_id, - region_name, - _resolve_macro_parameters( - macro_parameters=parameters, stack_parameters=stack_parameters - ), - ) - obj_copy = copy.deepcopy(obj) - obj_copy.pop("Fn::Transform", "") - obj_copy.update(transformed) - obj = obj_copy - - elif transform_name in macro_store: - obj_copy = copy.deepcopy(obj) - obj_copy.pop("Fn::Transform", "") - result = execute_macro( - account_id, - region_name, - obj_copy, - transform, - stack_parameters, - _resolve_macro_parameters(parameters, stack_parameters), - True, - ) - obj = result - else: - LOG.warning( - "Unsupported transform function '%s' used in %s", - transform_name, - stack_name, - ) - return obj - - return recurse_object(template, _visit) - def visit_node_global_transform( self, node_global_transform: NodeGlobalTransform ) -> PreprocEntityDelta[GlobalTransform, GlobalTransform]: diff --git a/tests/aws/services/cloudformation/test_template_engine.py b/tests/aws/services/cloudformation/test_template_engine.py index d039307ef5101..90dc7d40272e6 100644 --- a/tests/aws/services/cloudformation/test_template_engine.py +++ b/tests/aws/services/cloudformation/test_template_engine.py @@ -781,7 +781,7 @@ def test_attribute_uses_macro(self, deploy_cfn_template, create_lambda_function, assert "test-" in resulting_value @markers.aws.validated - @pytest.mark.skip(reason="Fn::Transform does not support array of transformations") + # @pytest.mark.skip(reason="Fn::Transform does not support array of transformations") def test_scope_order_and_parameters( self, deploy_cfn_template, create_lambda_function, snapshot, aws_client ): @@ -814,7 +814,7 @@ def test_scope_order_and_parameters( template_path=os.path.join( os.path.dirname(__file__), "../../templates/transformation_multiple_scope_parameter.yml", - ), + ) ) processed_template = aws_client.cloudformation.get_template( diff --git a/tests/aws/services/cloudformation/test_template_engine.snapshot.json b/tests/aws/services/cloudformation/test_template_engine.snapshot.json index da52914bdd544..8d6c96aa2ea09 100644 --- a/tests/aws/services/cloudformation/test_template_engine.snapshot.json +++ b/tests/aws/services/cloudformation/test_template_engine.snapshot.json @@ -112,7 +112,7 @@ } }, "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_scope_order_and_parameters": { - "recorded-date": "07-12-2022, 09:08:26", + "recorded-date": "23-07-2025, 18:18:33", "recorded-content": { "processed_template": { "StagesAvailable": [ @@ -124,7 +124,7 @@ "Parameter": { "Properties": { "Type": "String", - "Value": "snippet-transform second-snippet-transform global-transform second-global-transform " + "Value": "snippet-transform second-snippet-transform snippet-transform-upper global-transform second-global-transform " }, "Type": "AWS::SSM::Parameter" } diff --git a/tests/aws/services/cloudformation/test_template_engine.validation.json b/tests/aws/services/cloudformation/test_template_engine.validation.json index e0bbb0be7e342..b72ee0330c7d0 100644 --- a/tests/aws/services/cloudformation/test_template_engine.validation.json +++ b/tests/aws/services/cloudformation/test_template_engine.validation.json @@ -72,7 +72,13 @@ "last_validated_date": "2024-05-17T06:19:03+00:00" }, "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_scope_order_and_parameters": { - "last_validated_date": "2022-12-07T08:08:26+00:00" + "last_validated_date": "2025-07-23T18:18:42+00:00", + "durations_in_seconds": { + "setup": 10.95, + "call": 24.95, + "teardown": 9.81, + "total": 45.71 + } }, "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_snipped_scope[transformation_snippet_topic.json]": { "last_validated_date": "2022-12-08T15:25:43+00:00" diff --git a/tests/aws/templates/transformation_multiple_scope_parameter.yml b/tests/aws/templates/transformation_multiple_scope_parameter.yml index f977039b8a7e1..851192269b411 100644 --- a/tests/aws/templates/transformation_multiple_scope_parameter.yml +++ b/tests/aws/templates/transformation_multiple_scope_parameter.yml @@ -1,5 +1,9 @@ Resources: Parameter: + Fn::Transform: + - Name: ReplaceString + Parameters: + Input: "snippet-transform-upper" Type: AWS::SSM::Parameter Properties: Value: ""