diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 1c13c5058..e617fa9f8 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -453,8 +453,8 @@ class ResourceMap(collections.Mapping): resource_name, resource_json, self, self._region_name) self._parsed_resources[resource_name] = new_resource - removed_resource_nams = set(old_template) - set(new_template) - for resource_name in removed_resource_nams: + removed_resource_names = set(old_template) - set(new_template) + for resource_name in removed_resource_names: resource_json = old_template[resource_name] parse_and_delete_resource( resource_name, resource_json, self, self._region_name) diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index a5b251b89..3023fa114 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -161,11 +161,15 @@ class CloudFormationResponse(BaseResponse): def update_stack(self): stack_name = self._get_param('StackName') role_arn = self._get_param('RoleARN') + template_url = self._get_param('TemplateURL') if self._get_param('UsePreviousTemplate') == "true": stack_body = self.cloudformation_backend.get_stack( stack_name).template + elif template_url: + stack_body = self._get_stack_from_s3_url(template_url) else: stack_body = self._get_param('TemplateBody') + parameters = dict([ (parameter['parameter_key'], parameter['parameter_value']) for parameter diff --git a/moto/ec2/models.py b/moto/ec2/models.py index a50125b09..89c616e51 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -509,6 +509,22 @@ class Instance(TaggedEC2Resource, BotoInstance): instance.add_tag(tag["Key"], tag["Value"]) return instance + @classmethod + def delete_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + ec2_backend = ec2_backends[region_name] + all_instances = ec2_backend.all_instances() + + # the resource_name for instances is the stack name, logical id, and random suffix separated + # by hyphens. So to lookup the instances using the 'aws:cloudformation:logical-id' tag, we need to + # extract the logical-id from the resource_name + logical_id = resource_name.split('-')[1] + + for instance in all_instances: + instance_tags = instance.get_tags() + for tag in instance_tags: + if tag['key'] == 'aws:cloudformation:logical-id' and tag['value'] == logical_id: + instance.delete(region_name) + @property def physical_resource_id(self): return self.id diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py index ed2ee8337..182603e2c 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -1,18 +1,13 @@ from __future__ import unicode_literals -import boto3 -import boto -import boto.s3 -import boto.s3.key -from botocore.exceptions import ClientError -from moto import mock_cloudformation, mock_s3, mock_sqs - import json -import sure # noqa + +import boto3 +from botocore.exceptions import ClientError # Ensure 'assert_raises' context manager support for Python 2.6 -import tests.backport_assert_raises # noqa from nose.tools import assert_raises -import random + +from moto import mock_cloudformation, mock_s3, mock_sqs, mock_ec2 dummy_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -39,7 +34,6 @@ dummy_template = { } } - dummy_template_yaml = """--- AWSTemplateFormatVersion: 2010-09-09 Description: Stack1 with yaml template @@ -57,7 +51,6 @@ Resources: Value: Name tag for tests """ - dummy_template_yaml_with_short_form_func = """--- AWSTemplateFormatVersion: 2010-09-09 Description: Stack1 with yaml template @@ -75,7 +68,6 @@ Resources: Value: Name tag for tests """ - dummy_template_yaml_with_ref = """--- AWSTemplateFormatVersion: 2010-09-09 Description: Stack1 with yaml template @@ -100,7 +92,6 @@ Resources: Value: !Ref TagName """ - dummy_update_template = { "AWSTemplateFormatVersion": "2010-09-09", "Parameters": { @@ -131,12 +122,12 @@ dummy_output_template = { } } }, - "Outputs" : { - "StackVPC" : { - "Description" : "The ID of the VPC", - "Value" : "VPCID", - "Export" : { - "Name" : "My VPC ID" + "Outputs": { + "StackVPC": { + "Description": "The ID of the VPC", + "Value": "VPCID", + "Export": { + "Name": "My VPC ID" } } } @@ -156,7 +147,7 @@ dummy_import_template = { } dummy_template_json = json.dumps(dummy_template) -dummy_update_template_json = json.dumps(dummy_template) +dummy_update_template_json = json.dumps(dummy_update_template) dummy_output_template_json = json.dumps(dummy_output_template) dummy_import_template_json = json.dumps(dummy_import_template) @@ -172,6 +163,7 @@ def test_boto3_create_stack(): cf_conn.get_template(StackName="test_stack")['TemplateBody'].should.equal( dummy_template) + @mock_cloudformation def test_boto3_create_stack_with_yaml(): cf_conn = boto3.client('cloudformation', region_name='us-east-1') @@ -283,6 +275,41 @@ def test_create_stack_from_s3_url(): 'TemplateBody'].should.equal(dummy_template) +@mock_cloudformation +@mock_s3 +@mock_ec2 +def test_update_stack_from_s3_url(): + s3 = boto3.client('s3') + s3_conn = boto3.resource('s3') + + cf_conn = boto3.client('cloudformation', region_name='us-east-1') + cf_conn.create_stack( + StackName="update_stack_from_url", + TemplateBody=dummy_template_json, + Tags=[{'Key': 'foo', 'Value': 'bar'}], + ) + + s3_conn.create_bucket(Bucket="foobar") + + s3_conn.Object( + 'foobar', 'template-key').put(Body=dummy_update_template_json) + key_url = s3.generate_presigned_url( + ClientMethod='get_object', + Params={ + 'Bucket': 'foobar', + 'Key': 'template-key' + } + ) + + cf_conn.update_stack( + StackName="update_stack_from_url", + TemplateURL=key_url, + ) + + cf_conn.get_template(StackName="update_stack_from_url")[ + 'TemplateBody'].should.equal(dummy_update_template) + + @mock_cloudformation def test_describe_stack_pagination(): conn = boto3.client('cloudformation', region_name='us-east-1') @@ -382,6 +409,7 @@ def test_delete_stack_from_resource(): @mock_cloudformation +@mock_ec2 def test_delete_stack_by_name(): cf_conn = boto3.client('cloudformation', region_name='us-east-1') cf_conn.create_stack( @@ -412,6 +440,7 @@ def test_describe_deleted_stack(): @mock_cloudformation +@mock_ec2 def test_describe_updated_stack(): cf_conn = boto3.client('cloudformation', region_name='us-east-1') cf_conn.create_stack( @@ -502,6 +531,7 @@ def test_stack_tags(): @mock_cloudformation +@mock_ec2 def test_stack_events(): cf = boto3.resource('cloudformation', region_name='us-east-1') stack = cf.create_stack( @@ -617,6 +647,7 @@ def test_export_names_must_be_unique(): TemplateBody=dummy_output_template_json, ) + @mock_sqs @mock_cloudformation def test_stack_with_imports():