From 0816a2f5e507a5f809dfe7592138c0bd7c13cc17 Mon Sep 17 00:00:00 2001 From: Mateusz Kiersnowski Date: Wed, 22 Oct 2025 20:47:54 +0200 Subject: [PATCH 1/5] Add test for creating a CloudFormation stack from a template --- .../cloudformation/api/test_stacks.py | 23 +++++ .../api/test_stacks.snapshot.json | 93 ++++++------------- tests/aws/templates/ec2_cfn_init_template.yml | 67 +++++++++++++ 3 files changed, 116 insertions(+), 67 deletions(-) create mode 100644 tests/aws/templates/ec2_cfn_init_template.yml diff --git a/tests/aws/services/cloudformation/api/test_stacks.py b/tests/aws/services/cloudformation/api/test_stacks.py index dc70b77aafca9..cb9969e8f89bd 100644 --- a/tests/aws/services/cloudformation/api/test_stacks.py +++ b/tests/aws/services/cloudformation/api/test_stacks.py @@ -183,6 +183,29 @@ def test_stack_name_creation(self, deploy_cfn_template, snapshot, aws_client): snapshot.match("stack_response", e.value.response) + @markers.aws.only_localstack + def test_create_stack_from_template(self, snapshot, aws_client): + snapshot.add_transformer(snapshot.transform.cloudformation_api()) + + stack_name = f"stack-{short_uid()}" + + result = aws_client.cloudformation.create_stack( + StackName=stack_name, + TemplateBody=load_file( + os.path.join( + os.path.dirname(__file__), "../../../templates/ec2_cfn_init_template.yml" + ) + ), + ) + snapshot.match("stack_create_ec2", result) + + aws_client.cloudformation.get_waiter("stack_create_complete").wait(StackName=stack_name) + + describe_result = aws_client.cloudformation.describe_stacks(StackName=stack_name) + snapshot.match("stack_description", describe_result) + + aws_client.cloudformation.delete_stack(StackName=stack_name) + @markers.aws.validated @pytest.mark.parametrize("fileformat", ["yaml", "json"]) def test_get_template_using_create_stack(self, snapshot, fileformat, aws_client): diff --git a/tests/aws/services/cloudformation/api/test_stacks.snapshot.json b/tests/aws/services/cloudformation/api/test_stacks.snapshot.json index fde3beada5d45..9218ae6c3d4ab 100644 --- a/tests/aws/services/cloudformation/api/test_stacks.snapshot.json +++ b/tests/aws/services/cloudformation/api/test_stacks.snapshot.json @@ -4205,8 +4205,8 @@ ] } }, - "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_stack_description_lifecycle[no-tags]": { - "recorded-date": "09-09-2025, 09:37:20", + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_stack_description_lifecycle": { + "recorded-date": "08-08-2025, 15:30:34", "recorded-content": { "change-set-pre-execute": { "Capabilities": [], @@ -4271,80 +4271,39 @@ } } }, - "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_stack_description_lifecycle[with-tags]": { - "recorded-date": "09-09-2025, 09:37:28", + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_create_stack_from_template": { + "recorded-date": "22-10-2025, 18:44:36", "recorded-content": { - "change-set-pre-execute": { - "Capabilities": [], - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "ChangeSetName": "", - "Changes": [ + "stack_create_ec2": { + "StackId": "arn::cloudformation::111111111111:stack//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "stack_description": { + "Stacks": [ { - "ResourceChange": { - "Action": "Add", - "Details": [], - "LogicalResourceId": "TestResource", - "ResourceType": "AWS::SSM::Parameter", - "Scope": [] + "CreationTime": "datetime", + "Description": "AWS CloudFormation sample template. \nCreate an Amazon EC2 instance with cfn-init and cfn-signal.\n", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" }, - "Type": "Resource" - } - ], - "CreationTime": "datetime", - "ExecutionStatus": "AVAILABLE", - "IncludeNestedStacks": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "Status": "CREATE_COMPLETE", - "Tags": [ - { - "Key": "foo", - "Value": "bar" + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [] } ], "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } - }, - "stack-pre-execute": { - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "REVIEW_IN_PROGRESS", - "StackStatusReason": "User Initiated", - "Tags": [] - }, - "stack-post-execute": { - "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", - "CreationTime": "datetime", - "Description": "test .test.net", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "CREATE_COMPLETE", - "Tags": [ - { - "Key": "foo", - "Value": "bar" - } - ] } } } diff --git a/tests/aws/templates/ec2_cfn_init_template.yml b/tests/aws/templates/ec2_cfn_init_template.yml new file mode 100644 index 0000000000000..dce052b4ec7e7 --- /dev/null +++ b/tests/aws/templates/ec2_cfn_init_template.yml @@ -0,0 +1,67 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Description: "AWS CloudFormation sample template. \nCreate an Amazon EC2 instance with cfn-init and cfn-signal.\n" + +Resources: + Instance: + CreationPolicy: + ResourceSignal: + Timeout: PT5M + Type: AWS::EC2::Instance + Metadata: + guard: + SuppressedRules: + - EC2_INSTANCES_IN_VPC + AWS::CloudFormation::Init: + config: + packages: + yum: + httpd: [] + files: + /var/www/html/index.html: + content: | + +

Congratulations, you have successfully launched the AWS CloudFormation sample.

+ + mode: "000644" + owner: root + group: root + /etc/cfn/cfn-hup.conf: + content: !Sub | + [main] + stack=${AWS::StackId} + region=${AWS::Region} + mode: "000400" + owner: root + group: root + /etc/cfn/hooks.d/cfn-auto-reloader.conf: + content: !Sub |- + [cfn-auto-reloader-hook] + triggers=post.update + path=Resources.LaunchConfig.Metadata.AWS::CloudFormation::Init + action=/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource Instance --region ${AWS::Region} + runas=root + services: + sysvinit: + httpd: + enabled: true + ensureRunning: true + cfn-hup: + enabled: true + ensureRunning: true + files: + - /etc/cfn/cfn-hup.conf + - /etc/cfn/hooks.d/cfn-auto-reloader.conf + Properties: + ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-arm64}}' + InstanceType: t4g.nano + KeyName: sample + BlockDeviceMappings: + - DeviceName: /dev/sda1 + Ebs: + VolumeSize: 32 + UserData: !Base64 + Fn::Sub: |- + #!/bin/bash + /opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource Instance --region ${AWS::Region} + /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource Instance --region ${AWS::Region} From 0f237376a1f9fc1262b4470087895f2cd196219d Mon Sep 17 00:00:00 2001 From: Mateusz Kiersnowski Date: Thu, 23 Oct 2025 00:06:38 +0200 Subject: [PATCH 2/5] Fix snapshot after rebase --- .../api/test_stacks.snapshot.json | 84 ++++++++++++++++++- 1 file changed, 80 insertions(+), 4 deletions(-) diff --git a/tests/aws/services/cloudformation/api/test_stacks.snapshot.json b/tests/aws/services/cloudformation/api/test_stacks.snapshot.json index 9218ae6c3d4ab..689e7bb61bf0c 100644 --- a/tests/aws/services/cloudformation/api/test_stacks.snapshot.json +++ b/tests/aws/services/cloudformation/api/test_stacks.snapshot.json @@ -4205,8 +4205,8 @@ ] } }, - "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_stack_description_lifecycle": { - "recorded-date": "08-08-2025, 15:30:34", + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_stack_description_lifecycle[no-tags]": { + "recorded-date": "09-09-2025, 09:37:20", "recorded-content": { "change-set-pre-execute": { "Capabilities": [], @@ -4271,8 +4271,85 @@ } } }, + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_stack_description_lifecycle[with-tags]": { + "recorded-date": "09-09-2025, 09:37:28", + "recorded-content": { + "change-set-pre-execute": { + "Capabilities": [], + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "ChangeSetName": "", + "Changes": [ + { + "ResourceChange": { + "Action": "Add", + "Details": [], + "LogicalResourceId": "TestResource", + "ResourceType": "AWS::SSM::Parameter", + "Scope": [] + }, + "Type": "Resource" + } + ], + "CreationTime": "datetime", + "ExecutionStatus": "AVAILABLE", + "IncludeNestedStacks": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "Status": "CREATE_COMPLETE", + "Tags": [ + { + "Key": "foo", + "Value": "bar" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "stack-pre-execute": { + "CreationTime": "datetime", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "REVIEW_IN_PROGRESS", + "StackStatusReason": "User Initiated", + "Tags": [] + }, + "stack-post-execute": { + "ChangeSetId": "arn::cloudformation::111111111111:changeSet/", + "CreationTime": "datetime", + "Description": "test .test.net", + "DisableRollback": false, + "DriftInformation": { + "StackDriftStatus": "NOT_CHECKED" + }, + "EnableTerminationProtection": false, + "LastUpdatedTime": "datetime", + "NotificationARNs": [], + "RollbackConfiguration": {}, + "StackId": "arn::cloudformation::111111111111:stack//", + "StackName": "", + "StackStatus": "CREATE_COMPLETE", + "Tags": [ + { + "Key": "foo", + "Value": "bar" + } + ] + } + } + }, "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_create_stack_from_template": { - "recorded-date": "22-10-2025, 18:44:36", + "recorded-date": "22-10-2025, 22:04:58", "recorded-content": { "stack_create_ec2": { "StackId": "arn::cloudformation::111111111111:stack//", @@ -4285,7 +4362,6 @@ "Stacks": [ { "CreationTime": "datetime", - "Description": "AWS CloudFormation sample template. \nCreate an Amazon EC2 instance with cfn-init and cfn-signal.\n", "DisableRollback": false, "DriftInformation": { "StackDriftStatus": "NOT_CHECKED" From 7d039037f64f1aa3594020d604353f09c9a3081d Mon Sep 17 00:00:00 2001 From: Mateusz Kiersnowski Date: Thu, 23 Oct 2025 21:15:22 +0200 Subject: [PATCH 3/5] handle invalid TemplateBody in create_stack cfn --- .../cloudformation/engine/yaml_parser.py | 11 ++++-- .../cloudformation/api/test_stacks.py | 23 ++++-------- .../api/test_stacks.snapshot.json | 35 ++----------------- 3 files changed, 18 insertions(+), 51 deletions(-) diff --git a/localstack-core/localstack/services/cloudformation/engine/yaml_parser.py b/localstack-core/localstack/services/cloudformation/engine/yaml_parser.py index c0b72ead58f8f..3d12004f42423 100644 --- a/localstack-core/localstack/services/cloudformation/engine/yaml_parser.py +++ b/localstack-core/localstack/services/cloudformation/engine/yaml_parser.py @@ -1,5 +1,7 @@ import yaml +from localstack.services.cloudformation.engine.validations import ValidationError + def construct_raw(_, node): return node.value @@ -60,5 +62,10 @@ def shorthand_constructor(loader: yaml.Loader, tag_suffix: str, node: yaml.Node) yaml.add_multi_constructor("!", shorthand_constructor, customloader) -def parse_yaml(input_data: str): - return yaml.load(input_data, customloader) +def parse_yaml(input_data: str) -> dict: + parsed = yaml.load(input_data, Loader=customloader) + + if not isinstance(parsed, dict): + raise ValidationError("Template format error: unsupported structure.") + + return parsed diff --git a/tests/aws/services/cloudformation/api/test_stacks.py b/tests/aws/services/cloudformation/api/test_stacks.py index cb9969e8f89bd..0189a32a9f607 100644 --- a/tests/aws/services/cloudformation/api/test_stacks.py +++ b/tests/aws/services/cloudformation/api/test_stacks.py @@ -183,28 +183,17 @@ def test_stack_name_creation(self, deploy_cfn_template, snapshot, aws_client): snapshot.match("stack_response", e.value.response) - @markers.aws.only_localstack - def test_create_stack_from_template(self, snapshot, aws_client): + @markers.aws.unknown + def test_create_stack_url_as_template(self, snapshot, aws_client): snapshot.add_transformer(snapshot.transform.cloudformation_api()) stack_name = f"stack-{short_uid()}" - result = aws_client.cloudformation.create_stack( - StackName=stack_name, - TemplateBody=load_file( - os.path.join( - os.path.dirname(__file__), "../../../templates/ec2_cfn_init_template.yml" - ) - ), - ) - snapshot.match("stack_create_ec2", result) - - aws_client.cloudformation.get_waiter("stack_create_complete").wait(StackName=stack_name) + template_url = "https://raw.githubusercontent.com/aws-cloudformation/aws-cloudformation-templates/refs/heads/main/EC2/InstanceWithCfnInit.yaml" - describe_result = aws_client.cloudformation.describe_stacks(StackName=stack_name) - snapshot.match("stack_description", describe_result) - - aws_client.cloudformation.delete_stack(StackName=stack_name) + with pytest.raises(ClientError) as e: + aws_client.cloudformation.create_stack(StackName=stack_name, TemplateBody=template_url) + snapshot.match("stack_create_ec2", e.value) @markers.aws.validated @pytest.mark.parametrize("fileformat", ["yaml", "json"]) diff --git a/tests/aws/services/cloudformation/api/test_stacks.snapshot.json b/tests/aws/services/cloudformation/api/test_stacks.snapshot.json index 689e7bb61bf0c..8bf66e2269025 100644 --- a/tests/aws/services/cloudformation/api/test_stacks.snapshot.json +++ b/tests/aws/services/cloudformation/api/test_stacks.snapshot.json @@ -4348,39 +4348,10 @@ } } }, - "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_create_stack_from_template": { - "recorded-date": "22-10-2025, 22:04:58", + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_create_stack_url_as_template": { + "recorded-date": "23-10-2025, 18:51:49", "recorded-content": { - "stack_create_ec2": { - "StackId": "arn::cloudformation::111111111111:stack//", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "stack_description": { - "Stacks": [ - { - "CreationTime": "datetime", - "DisableRollback": false, - "DriftInformation": { - "StackDriftStatus": "NOT_CHECKED" - }, - "EnableTerminationProtection": false, - "LastUpdatedTime": "datetime", - "NotificationARNs": [], - "RollbackConfiguration": {}, - "StackId": "arn::cloudformation::111111111111:stack//", - "StackName": "", - "StackStatus": "CREATE_COMPLETE", - "Tags": [] - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } + "stack_create_ec2": "An error occurred (ValidationError) when calling the CreateStack operation: Template format error: unsupported structure." } } } From 400858ee58f39c6db790d9f06eb1e8404241cfdf Mon Sep 17 00:00:00 2001 From: Mateusz Kiersnowski Date: Thu, 23 Oct 2025 21:17:57 +0200 Subject: [PATCH 4/5] remove unused template --- tests/aws/templates/ec2_cfn_init_template.yml | 67 ------------------- 1 file changed, 67 deletions(-) delete mode 100644 tests/aws/templates/ec2_cfn_init_template.yml diff --git a/tests/aws/templates/ec2_cfn_init_template.yml b/tests/aws/templates/ec2_cfn_init_template.yml deleted file mode 100644 index dce052b4ec7e7..0000000000000 --- a/tests/aws/templates/ec2_cfn_init_template.yml +++ /dev/null @@ -1,67 +0,0 @@ -AWSTemplateFormatVersion: "2010-09-09" - -Description: "AWS CloudFormation sample template. \nCreate an Amazon EC2 instance with cfn-init and cfn-signal.\n" - -Resources: - Instance: - CreationPolicy: - ResourceSignal: - Timeout: PT5M - Type: AWS::EC2::Instance - Metadata: - guard: - SuppressedRules: - - EC2_INSTANCES_IN_VPC - AWS::CloudFormation::Init: - config: - packages: - yum: - httpd: [] - files: - /var/www/html/index.html: - content: | - -

Congratulations, you have successfully launched the AWS CloudFormation sample.

- - mode: "000644" - owner: root - group: root - /etc/cfn/cfn-hup.conf: - content: !Sub | - [main] - stack=${AWS::StackId} - region=${AWS::Region} - mode: "000400" - owner: root - group: root - /etc/cfn/hooks.d/cfn-auto-reloader.conf: - content: !Sub |- - [cfn-auto-reloader-hook] - triggers=post.update - path=Resources.LaunchConfig.Metadata.AWS::CloudFormation::Init - action=/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource Instance --region ${AWS::Region} - runas=root - services: - sysvinit: - httpd: - enabled: true - ensureRunning: true - cfn-hup: - enabled: true - ensureRunning: true - files: - - /etc/cfn/cfn-hup.conf - - /etc/cfn/hooks.d/cfn-auto-reloader.conf - Properties: - ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-arm64}}' - InstanceType: t4g.nano - KeyName: sample - BlockDeviceMappings: - - DeviceName: /dev/sda1 - Ebs: - VolumeSize: 32 - UserData: !Base64 - Fn::Sub: |- - #!/bin/bash - /opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource Instance --region ${AWS::Region} - /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource Instance --region ${AWS::Region} From 8b40b541e001bf6c8cc7b27fa229850c4caa9f8d Mon Sep 17 00:00:00 2001 From: Mateusz Kiersnowski Date: Thu, 23 Oct 2025 21:26:40 +0200 Subject: [PATCH 5/5] validate against AWS --- tests/aws/services/cloudformation/api/test_stacks.py | 2 +- .../cloudformation/api/test_stacks.snapshot.json | 2 +- .../cloudformation/api/test_stacks.validation.json | 9 +++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/aws/services/cloudformation/api/test_stacks.py b/tests/aws/services/cloudformation/api/test_stacks.py index 0189a32a9f607..8939ecb816fb2 100644 --- a/tests/aws/services/cloudformation/api/test_stacks.py +++ b/tests/aws/services/cloudformation/api/test_stacks.py @@ -183,7 +183,7 @@ def test_stack_name_creation(self, deploy_cfn_template, snapshot, aws_client): snapshot.match("stack_response", e.value.response) - @markers.aws.unknown + @markers.aws.validated def test_create_stack_url_as_template(self, snapshot, aws_client): snapshot.add_transformer(snapshot.transform.cloudformation_api()) diff --git a/tests/aws/services/cloudformation/api/test_stacks.snapshot.json b/tests/aws/services/cloudformation/api/test_stacks.snapshot.json index 8bf66e2269025..0a6415fd994ee 100644 --- a/tests/aws/services/cloudformation/api/test_stacks.snapshot.json +++ b/tests/aws/services/cloudformation/api/test_stacks.snapshot.json @@ -4349,7 +4349,7 @@ } }, "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_create_stack_url_as_template": { - "recorded-date": "23-10-2025, 18:51:49", + "recorded-date": "23-10-2025, 19:25:10", "recorded-content": { "stack_create_ec2": "An error occurred (ValidationError) when calling the CreateStack operation: Template format error: unsupported structure." } diff --git a/tests/aws/services/cloudformation/api/test_stacks.validation.json b/tests/aws/services/cloudformation/api/test_stacks.validation.json index 9dd42d7bd0ce4..adab0cbddb4a3 100644 --- a/tests/aws/services/cloudformation/api/test_stacks.validation.json +++ b/tests/aws/services/cloudformation/api/test_stacks.validation.json @@ -1,4 +1,13 @@ { + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_create_stack_url_as_template": { + "last_validated_date": "2025-10-23T19:25:10+00:00", + "durations_in_seconds": { + "setup": 0.82, + "call": 0.82, + "teardown": 0.02, + "total": 1.66 + } + }, "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_failure_options_for_stack_update[False-2]": { "last_validated_date": "2024-06-25T17:21:51+00:00" },