From 90e200f0f6e0936c0ccb59c33990831a4eb1cda7 Mon Sep 17 00:00:00 2001 From: Jeremie Tharaud <46786750+jeremietharaud@users.noreply.github.com> Date: Wed, 3 Jun 2020 07:08:35 +0200 Subject: [PATCH] Add missing changes when creating a change set (#3039) * Display changes when creating a change set * add change set id and description when describing stack * fix lint with flake8 and black --- moto/cloudformation/models.py | 19 +++++++-- moto/cloudformation/parsing.py | 6 ++- moto/cloudformation/responses.py | 4 ++ .../test_cloudformation_stack_crud_boto3.py | 41 +++++++++++++------ .../test_cloudformation_stack_integration.py | 6 --- 5 files changed, 51 insertions(+), 25 deletions(-) diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index 16ceafdb8..8c14f55b8 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -219,7 +219,12 @@ class FakeStack(BaseModel): self.stack_id = stack_id self.name = name self.template = template - self._parse_template() + if template != {}: + self._parse_template() + self.description = self.template_dict.get("Description") + else: + self.template_dict = {} + self.description = None self.parameters = parameters self.region_name = region_name self.notification_arns = notification_arns if notification_arns else [] @@ -235,7 +240,6 @@ class FakeStack(BaseModel): "CREATE_IN_PROGRESS", resource_status_reason="User Initiated" ) - self.description = self.template_dict.get("Description") self.cross_stack_resources = cross_stack_resources or {} self.resource_map = self._create_resource_map() self.output_map = self._create_output_map() @@ -331,7 +335,9 @@ class FakeStack(BaseModel): return self.output_map.exports def create_resources(self): - self.resource_map.create() + self.resource_map.create(self.template_dict) + # Set the description of the stack + self.description = self.template_dict.get("Description") self.status = "CREATE_COMPLETE" def update(self, template, role_arn=None, parameters=None, tags=None): @@ -398,6 +404,8 @@ class FakeChangeSet(FakeStack): self.change_set_id = change_set_id self.change_set_name = change_set_name self.changes = self.diff(template=template, parameters=parameters) + if self.description is None: + self.description = self.template_dict.get("Description") self.creation_time = datetime.utcnow() def diff(self, template, parameters=None): @@ -590,7 +598,7 @@ class CloudFormationBackend(BaseBackend): raise ValidationError(stack_name) else: stack_id = generate_stack_id(stack_name, region_name) - stack_template = template + stack_template = {} change_set_id = generate_changeset_id(change_set_name, region_name) new_change_set = FakeChangeSet( @@ -645,6 +653,9 @@ class CloudFormationBackend(BaseBackend): if stack is None: raise ValidationError(stack_name) if stack.events[-1].resource_status == "REVIEW_IN_PROGRESS": + stack._add_stack_event( + "CREATE_IN_PROGRESS", resource_status_reason="User Initiated" + ) stack._add_stack_event("CREATE_COMPLETE") else: stack._add_stack_event("UPDATE_IN_PROGRESS") diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index d59b21b82..81d4d1c7d 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -456,7 +456,7 @@ class ResourceMap(collections_abc.Mapping): cross_stack_resources, ): self._template = template - self._resource_json_map = template["Resources"] + self._resource_json_map = template["Resources"] if template != {} else {} self._region_name = region_name self.input_parameters = parameters self.tags = copy.deepcopy(tags) @@ -592,10 +592,12 @@ class ResourceMap(collections_abc.Mapping): self.load_parameters() self.load_conditions() - def create(self): + def create(self, template): # Since this is a lazy map, to create every object we just need to # iterate through self. # Assumes that self.load() has been called before + self._template = template + self._resource_json_map = template["Resources"] self.tags.update( { "aws:cloudformation:stack-name": self.get("AWS::StackName"), diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index c028421ca..302849481 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -662,6 +662,10 @@ DESCRIBE_STACKS_TEMPLATE = """ {{ stack.name }} {{ stack.stack_id }} + {% if stack.change_set_id %} + {{ stack.change_set_id }} + {% endif %} + {{ stack.description }} {{ stack.creation_time_iso_8601 }} {{ stack.status }} {% if stack.notification_arns %} diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py index c4fddcad0..cd76743dd 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -573,9 +573,9 @@ def test_boto3_create_stack_set_with_yaml(): def test_create_stack_set_from_s3_url(): s3 = boto3.client("s3") s3_conn = boto3.resource("s3", region_name="us-east-1") - bucket = s3_conn.create_bucket(Bucket="foobar") + s3_conn.create_bucket(Bucket="foobar") - key = s3_conn.Object("foobar", "template-key").put(Body=dummy_template_json) + s3_conn.Object("foobar", "template-key").put(Body=dummy_template_json) key_url = s3.generate_presigned_url( ClientMethod="get_object", Params={"Bucket": "foobar", "Key": "template-key"} ) @@ -716,9 +716,9 @@ def test_create_stack_with_role_arn(): def test_create_stack_from_s3_url(): s3 = boto3.client("s3") s3_conn = boto3.resource("s3", region_name="us-east-1") - bucket = s3_conn.create_bucket(Bucket="foobar") + s3_conn.create_bucket(Bucket="foobar") - key = s3_conn.Object("foobar", "template-key").put(Body=dummy_template_json) + s3_conn.Object("foobar", "template-key").put(Body=dummy_template_json) key_url = s3.generate_presigned_url( ClientMethod="get_object", Params={"Bucket": "foobar", "Key": "template-key"} ) @@ -800,9 +800,9 @@ def test_update_stack_from_s3_url(): def test_create_change_set_from_s3_url(): s3 = boto3.client("s3") s3_conn = boto3.resource("s3", region_name="us-east-1") - bucket = s3_conn.create_bucket(Bucket="foobar") + s3_conn.create_bucket(Bucket="foobar") - key = s3_conn.Object("foobar", "template-key").put(Body=dummy_template_json) + s3_conn.Object("foobar", "template-key").put(Body=dummy_template_json) key_url = s3.generate_presigned_url( ClientMethod="get_object", Params={"Bucket": "foobar", "Key": "template-key"} ) @@ -844,6 +844,25 @@ def test_describe_change_set(): assert ( two_secs_ago < stack["CreationTime"] < datetime.now(tz=pytz.UTC) ), "Change set should have been created recently" + stack["Changes"].should.have.length_of(1) + stack["Changes"][0].should.equal( + dict( + { + "Type": "Resource", + "ResourceChange": { + "Action": "Add", + "LogicalResourceId": "EC2Instance1", + "ResourceType": "AWS::EC2::Instance", + }, + } + ) + ) + + # Execute change set + cf_conn.execute_change_set(ChangeSetName="NewChangeSet") + # Verify that the changes have been applied + stack = cf_conn.describe_change_set(ChangeSetName="NewChangeSet") + stack["Changes"].should.have.length_of(1) cf_conn.create_change_set( StackName="NewStack", @@ -887,7 +906,7 @@ def test_execute_change_set_w_arn(): @mock_cloudformation def test_execute_change_set_w_name(): cf_conn = boto3.client("cloudformation", region_name="us-east-1") - change_set = cf_conn.create_change_set( + cf_conn.create_change_set( StackName="NewStack", TemplateBody=dummy_template_json, ChangeSetName="NewChangeSet", @@ -1221,9 +1240,7 @@ def test_delete_stack_with_export(): @mock_cloudformation def test_export_names_must_be_unique(): cf = boto3.resource("cloudformation", region_name="us-east-1") - first_stack = cf.create_stack( - StackName="test_stack", TemplateBody=dummy_output_template_json - ) + cf.create_stack(StackName="test_stack", TemplateBody=dummy_output_template_json) with assert_raises(ClientError): cf.create_stack(StackName="test_stack", TemplateBody=dummy_output_template_json) @@ -1237,9 +1254,7 @@ def test_stack_with_imports(): output_stack = cf.create_stack( StackName="test_stack1", TemplateBody=dummy_output_template_json ) - import_stack = cf.create_stack( - StackName="test_stack2", TemplateBody=dummy_import_template_json - ) + cf.create_stack(StackName="test_stack2", TemplateBody=dummy_import_template_json) output_stack.outputs.should.have.length_of(1) output = output_stack.outputs[0]["OutputValue"] diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index 3abb3373d..a49c4a1f4 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals import json -import base64 from decimal import Decimal import boto @@ -28,7 +27,6 @@ from moto import ( mock_dynamodb2, mock_ec2, mock_ec2_deprecated, - mock_elb, mock_elb_deprecated, mock_events, mock_iam_deprecated, @@ -37,18 +35,14 @@ from moto import ( mock_logs, mock_rds_deprecated, mock_rds2, - mock_rds2_deprecated, - mock_redshift, mock_redshift_deprecated, mock_route53_deprecated, mock_s3, mock_sns_deprecated, - mock_sqs, mock_sqs_deprecated, mock_elbv2, ) from moto.core import ACCOUNT_ID -from moto.dynamodb2.models import Table from tests.test_cloudformation.fixtures import ( ec2_classic_eip,