From 06b173abe446635c4aacc29eb524875364fad8f8 Mon Sep 17 00:00:00 2001 From: Andrew Garrett Date: Mon, 29 Feb 2016 19:50:14 +0000 Subject: [PATCH 1/6] Pin boto3 and botocore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit botocore 1.3.29 (new as of yesterday) breaks a few of the s3 and lambda tests; something about a StringIO being closed prematurely. ¯\_(ツ)_/¯ boto3 doesn't pin the botocore version; as a result the new version of botocore gets pulled in transitively, breaking today's builds. Signed-off-by: Scott Greene --- requirements-dev.txt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index d2e70ba89..a75fdcfb5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,6 +5,8 @@ sure>=1.2.24 coverage freezegun flask -boto3>=1.2.3 -botocore>=1.3.26 -six \ No newline at end of file +# botocore 1.3.29 breaks s3 in tests (lambda and s3 tests) +# so we need to pin a boto3 and botocore revision pair that we know works +boto3==1.2.4 +botocore==1.3.28 +six From ec10699c38674076e1e485fe8a5d0b027cc7f83f Mon Sep 17 00:00:00 2001 From: Andrew Garrett Date: Mon, 29 Feb 2016 19:50:23 +0000 Subject: [PATCH 2/6] Add XML support for cloudformation commands that lacked it This lets boto3's cloudformation API work with moto. fixes #444 Signed-off-by: Scott Greene --- moto/cloudformation/responses.py | 208 +++++++++------- moto/core/responses.py | 4 + moto/sns/responses.py | 4 - .../test_cloudformation_stack_crud_boto3.py | 231 ++++++++++++++++++ tests/test_cloudformation/test_server.py | 13 +- 5 files changed, 366 insertions(+), 94 deletions(-) create mode 100644 tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index c5196b2df..8f233efc6 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -47,14 +47,17 @@ class CloudFormationResponse(BaseResponse): notification_arns=stack_notification_arns, tags=tags, ) - stack_body = { - 'CreateStackResponse': { - 'CreateStackResult': { - 'StackId': stack.stack_id, + if self.request_json: + return json.dumps({ + 'CreateStackResponse': { + 'CreateStackResult': { + 'StackId': stack.stack_id, + } } - } - } - return json.dumps(stack_body) + }) + else: + template = self.response_template(CREATE_STACK_RESPONSE_TEMPLATE) + return template.render(stack=stack) def describe_stacks(self): stack_name_or_id = None @@ -87,18 +90,21 @@ class CloudFormationResponse(BaseResponse): def get_template(self): name_or_stack_id = self.querystring.get('StackName')[0] stack = self.cloudformation_backend.get_stack(name_or_stack_id) - - response = { - "GetTemplateResponse": { - "GetTemplateResult": { - "TemplateBody": stack.template, - "ResponseMetadata": { - "RequestId": "2d06e36c-ac1d-11e0-a958-f9382b6eb86bEXAMPLE" + + if self.request_json: + return json.dumps({ + "GetTemplateResponse": { + "GetTemplateResult": { + "TemplateBody": stack.template, + "ResponseMetadata": { + "RequestId": "2d06e36c-ac1d-11e0-a958-f9382b6eb86bEXAMPLE" + } + } } - } - } - } - return json.dumps(response) + }) + else: + template = self.response_template(GET_TEMPLATE_RESPONSE_TEMPLATE) + return template.render(stack=stack) def update_stack(self): stack_name = self._get_param('StackName') @@ -121,76 +127,76 @@ class CloudFormationResponse(BaseResponse): name_or_stack_id = self.querystring.get('StackName')[0] self.cloudformation_backend.delete_stack(name_or_stack_id) - return json.dumps({ - 'DeleteStackResponse': { - 'DeleteStackResult': {}, - } - }) + if self.request_json: + return json.dumps({ + 'DeleteStackResponse': { + 'DeleteStackResult': {}, + } + }) + else: + template = self.response_template(DELETE_STACK_RESPONSE_TEMPLATE) + return template.render() -DESCRIBE_STACKS_TEMPLATE = """ - - {% for stack in stacks %} - - {{ stack.name }} - {{ stack.stack_id }} - 2010-07-27T22:28:28Z - {{ stack.status }} - {% if stack.notification_arns %} - - {% for notification_arn in stack.notification_arns %} - {{ notification_arn }} - {% endfor %} - - {% else %} - - {% endif %} - false - - {% for output in stack.stack_outputs %} - - {{ output.key }} - {{ output.value }} - - {% endfor %} - - - {% for param_name, param_value in stack.stack_parameters.items() %} - - {{ param_name }} - {{ param_value }} - - {% endfor %} - - - {% for tag_key, tag_value in stack.tags.items() %} +CREATE_STACK_RESPONSE_TEMPLATE = """ + + {{ stack.stack_id }} + + + b9b4b068-3a41-11e5-94eb-example + + +""" + + +DESCRIBE_STACKS_TEMPLATE = """ + + + {% for stack in stacks %} + + {{ stack.name }} + {{ stack.stack_id }} + 2010-07-27T22:28:28Z + {{ stack.status }} + {% if stack.notification_arns %} + + {% for notification_arn in stack.notification_arns %} + {{ notification_arn }} + {% endfor %} + + {% else %} + + {% endif %} + false + + {% for output in stack.stack_outputs %} - {{ tag_key }} - {{ tag_value }} + {{ output.key }} + {{ output.value }} {% endfor %} - - - {% endfor %} - -""" - - -LIST_STACKS_RESPONSE = """ - - - {% for stack in stacks %} - - {{ stack.stack_id }} - {{ stack.status }} - {{ stack.name }} - 2011-05-23T15:47:44Z - {{ stack.description }} - - {% endfor %} - - -""" + + + {% for param_name, param_value in stack.stack_parameters.items() %} + + {{ param_name }} + {{ param_value }} + + {% endfor %} + + + {% for tag_key, tag_value in stack.tags.items() %} + + {{ tag_key }} + {{ tag_value }} + + {% endfor %} + + + {% endfor %} + + +""" DESCRIBE_STACKS_RESOURCES_RESPONSE = """ @@ -210,6 +216,23 @@ DESCRIBE_STACKS_RESOURCES_RESPONSE = """ """ +LIST_STACKS_RESPONSE = """ + + + {% for stack in stacks %} + + {{ stack.stack_id }} + {{ stack.status }} + {{ stack.name }} + 2011-05-23T15:47:44Z + {{ stack.description }} + + {% endfor %} + + +""" + + LIST_STACKS_RESOURCES_RESPONSE = """ @@ -228,3 +251,22 @@ LIST_STACKS_RESOURCES_RESPONSE = """ 2d06e36c-ac1d-11e0-a958-f9382b6eb86b """ + + +GET_TEMPLATE_RESPONSE_TEMPLATE = """ + + {{ stack.template }} + + + + b9b4b068-3a41-11e5-94eb-example + +""" + + +DELETE_STACK_RESPONSE_TEMPLATE = """ + + 5ccc7dcd-744c-11e5-be70-example + + +""" diff --git a/moto/core/responses.py b/moto/core/responses.py index 6595019fb..b924ac9e5 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -254,6 +254,10 @@ class BaseResponse(_TemplateEnvironmentMixin): param_index += 1 return results + @property + def request_json(self): + return 'JSON' in self.querystring.get('ContentType', []) + def metadata_response(request, full_url, headers): """ diff --git a/moto/sns/responses.py b/moto/sns/responses.py index 6f90586a3..96efff767 100644 --- a/moto/sns/responses.py +++ b/moto/sns/responses.py @@ -12,10 +12,6 @@ class SNSResponse(BaseResponse): def backend(self): return sns_backends[self.region] - @property - def request_json(self): - return 'JSON' in self.querystring.get('ContentType', []) - def _get_attributes(self): attributes = self._get_list_prefix('Attributes.entry') return dict( diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py new file mode 100644 index 000000000..d16a2d7b5 --- /dev/null +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -0,0 +1,231 @@ +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 + +import json +import sure # noqa +# Ensure 'assert_raises' context manager support for Python 2.6 +import tests.backport_assert_raises # noqa +from nose.tools import assert_raises + +dummy_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Stack 1", + "Resources": {}, +} + +dummy_template_json = json.dumps(dummy_template) + +@mock_cloudformation +def test_boto3_create_stack(): + cf_conn = boto3.client('cloudformation', region_name='us-east-1') + cf_conn.create_stack( + StackName="test_stack", + TemplateBody=dummy_template_json, + ) + + cf_conn.get_template(StackName="test_stack")['TemplateBody'].should.equal(dummy_template) + + +@mock_cloudformation +def test_creating_stacks_across_regions(): + west1_cf = boto3.resource('cloudformation', region_name='us-west-1') + west2_cf = boto3.resource('cloudformation', region_name='us-west-2') + west1_cf.create_stack( + StackName="test_stack", + TemplateBody=dummy_template_json, + ) + west2_cf.create_stack( + StackName="test_stack", + TemplateBody=dummy_template_json, + ) + + list(west1_cf.stacks.all()).should.have.length_of(1) + list(west2_cf.stacks.all()).should.have.length_of(1) + + +@mock_cloudformation +def test_create_stack_with_notification_arn(): + cf = boto3.resource('cloudformation', region_name='us-east-1') + cf.create_stack( + StackName="test_stack_with_notifications", + TemplateBody=dummy_template_json, + NotificationARNs=['arn:aws:sns:us-east-1:123456789012:fake-queue'], + ) + + stack = list(cf.stacks.all())[0] + stack.notification_arns.should.contain('arn:aws:sns:us-east-1:123456789012:fake-queue') + + +@mock_cloudformation +@mock_s3 +def test_create_stack_from_s3_url(): + s3_conn = boto.s3.connect_to_region('us-west-1') + bucket = s3_conn.create_bucket("foobar") + key = boto.s3.key.Key(bucket) + key.key = "template-key" + key.set_contents_from_string(dummy_template_json) + key_url = key.generate_url(expires_in=0, query_auth=False) + + cf_conn = boto3.client('cloudformation', region_name='us-west-1') + cf_conn.create_stack( + StackName='stack_from_url', + TemplateURL=key_url, + ) + + cf_conn.get_template(StackName="stack_from_url")['TemplateBody'].should.equal(dummy_template) + + +@mock_cloudformation +def test_describe_stack_by_name(): + cf_conn = boto3.client('cloudformation', region_name='us-east-1') + cf_conn.create_stack( + StackName="test_stack", + TemplateBody=dummy_template_json, + ) + + stack = cf_conn.describe_stacks(StackName="test_stack")['Stacks'][0] + stack['StackName'].should.equal('test_stack') + + +@mock_cloudformation +def test_describe_stack_by_stack_id(): + cf_conn = boto3.client('cloudformation', region_name='us-east-1') + cf_conn.create_stack( + StackName="test_stack", + TemplateBody=dummy_template_json, + ) + + stack = cf_conn.describe_stacks(StackName="test_stack")['Stacks'][0] + stack_by_id = cf_conn.describe_stacks(StackName=stack['StackId'])['Stacks'][0] + + stack_by_id['StackId'].should.equal(stack['StackId']) + stack_by_id['StackName'].should.equal("test_stack") + + +@mock_cloudformation +def test_list_stacks(): + cf = boto3.resource('cloudformation', region_name='us-east-1') + cf.create_stack( + StackName="test_stack", + TemplateBody=dummy_template_json, + ) + cf.create_stack( + StackName="test_stack2", + TemplateBody=dummy_template_json, + ) + + stacks = list(cf.stacks.all()) + stacks.should.have.length_of(2) + stack_names = [stack.stack_name for stack in stacks] + stack_names.should.contain("test_stack") + stack_names.should.contain("test_stack2") + + +@mock_cloudformation +def test_delete_stack_from_resource(): + cf = boto3.resource('cloudformation', region_name='us-east-1') + stack = cf.create_stack( + StackName="test_stack", + TemplateBody=dummy_template_json, + ) + + list(cf.stacks.all()).should.have.length_of(1) + stack.delete() + list(cf.stacks.all()).should.have.length_of(0) + + +@mock_cloudformation +def test_delete_stack_by_name(): + cf_conn = boto3.client('cloudformation', region_name='us-east-1') + cf_conn.create_stack( + StackName="test_stack", + TemplateBody=dummy_template_json, + ) + + cf_conn.describe_stacks()['Stacks'].should.have.length_of(1) + cf_conn.delete_stack(StackName="test_stack") + cf_conn.describe_stacks()['Stacks'].should.have.length_of(0) + + +@mock_cloudformation +def test_describe_deleted_stack(): + cf_conn = boto3.client('cloudformation', region_name='us-east-1') + cf_conn.create_stack( + StackName="test_stack", + TemplateBody=dummy_template_json, + ) + + stack = cf_conn.describe_stacks(StackName="test_stack")['Stacks'][0] + stack_id = stack['StackId'] + cf_conn.delete_stack(StackName=stack['StackId']) + stack_by_id = cf_conn.describe_stacks(StackName=stack_id)['Stacks'][0] + stack_by_id['StackId'].should.equal(stack['StackId']) + stack_by_id['StackName'].should.equal("test_stack") + stack_by_id['StackStatus'].should.equal("DELETE_COMPLETE") + + +@mock_cloudformation +def test_bad_describe_stack(): + cf_conn = boto3.client('cloudformation', region_name='us-east-1') + with assert_raises(ClientError): + cf_conn.describe_stacks(StackName="non_existent_stack") + + +@mock_cloudformation() +def test_cloudformation_params(): + dummy_template_with_params = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Stack 1", + "Resources": {}, + "Parameters": { + "APPNAME": { + "Default": "app-name", + "Description": "The name of the app", + "Type": "String" + } + } + } + dummy_template_with_params_json = json.dumps(dummy_template_with_params) + + cf = boto3.resource('cloudformation', region_name='us-east-1') + stack = cf.create_stack( + StackName='test_stack', + TemplateBody=dummy_template_with_params_json, + Parameters=[{ + "ParameterKey": "APPNAME", + "ParameterValue": "testing123", + }], + ) + + stack.parameters.should.have.length_of(1) + param = stack.parameters[0] + param['ParameterKey'].should.equal('APPNAME') + param['ParameterValue'].should.equal('testing123') + + +@mock_cloudformation +def test_stack_tags(): + tags = [ + { + "Key": "foo", + "Value": "bar" + }, + { + "Key": "baz", + "Value": "bleh" + } + ] + cf = boto3.resource('cloudformation', region_name='us-east-1') + stack = cf.create_stack( + StackName="test_stack", + TemplateBody=dummy_template_json, + Tags=tags, + ) + + stack.tags.should.equal(tags) diff --git a/tests/test_cloudformation/test_server.py b/tests/test_cloudformation/test_server.py index ffbc5c608..b4f50024b 100644 --- a/tests/test_cloudformation/test_server.py +++ b/tests/test_cloudformation/test_server.py @@ -19,13 +19,12 @@ def test_cloudformation_server_get(): template_body = { "Resources": {}, } - res = test_client.action_json("CreateStack", StackName=stack_name, + create_stack_resp = test_client.action_data("CreateStack", StackName=stack_name, TemplateBody=json.dumps(template_body)) - stack_id = res["CreateStackResponse"]["CreateStackResult"]["StackId"] + create_stack_resp.should.match(r".*.*.*.*.*", re.DOTALL) + stack_id_from_create_response = re.search("(.*)", create_stack_resp).groups()[0] - data = test_client.action_data("ListStacks") + list_stacks_resp = test_client.action_data("ListStacks") + stack_id_from_list_response = re.search("(.*)", list_stacks_resp).groups()[0] - stacks = re.search("(.*)", data) - - list_stack_id = stacks.groups()[0] - assert stack_id == list_stack_id + stack_id_from_create_response.should.equal(stack_id_from_list_response) From da98052b18da19826830f02c736f0a663addcbf5 Mon Sep 17 00:00:00 2001 From: Andrew Garrett Date: Mon, 29 Feb 2016 19:50:25 +0000 Subject: [PATCH 3/6] Add support for DescribeStackResource Signed-off-by: Scott Greene --- moto/cloudformation/responses.py | 35 ++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index 8f233efc6..64f4293d3 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -6,6 +6,7 @@ from six.moves.urllib.parse import urlparse from moto.core.responses import BaseResponse from moto.s3 import s3_backend from .models import cloudformation_backends +from .exceptions import ValidationError class CloudFormationResponse(BaseResponse): @@ -68,11 +69,26 @@ class CloudFormationResponse(BaseResponse): template = self.response_template(DESCRIBE_STACKS_TEMPLATE) return template.render(stacks=stacks) + def describe_stack_resource(self): + stack_name = self._get_param('StackName') + stack = self.cloudformation_backend.get_stack(stack_name) + logical_resource_id = self._get_param('LogicalResourceId') + + for stack_resource in stack.stack_resources: + if stack_resource.logical_resource_id == logical_resource_id: + resource = stack_resource + break + else: + raise ValidationError(logical_resource_id) + + template = self.response_template(DESCRIBE_STACK_RESOURCE_RESPONSE_TEMPLATE) + return template.render(stack=stack, resource=resource) + def describe_stack_resources(self): stack_name = self._get_param('StackName') stack = self.cloudformation_backend.get_stack(stack_name) - template = self.response_template(DESCRIBE_STACKS_RESOURCES_RESPONSE) + template = self.response_template(DESCRIBE_STACK_RESOURCES_RESPONSE) return template.render(stack=stack) def list_stacks(self): @@ -199,7 +215,22 @@ DESCRIBE_STACKS_TEMPLATE = """ """ -DESCRIBE_STACKS_RESOURCES_RESPONSE = """ +DESCRIBE_STACK_RESOURCE_RESPONSE_TEMPLATE = """ + + + {{ stack.stack_id }} + {{ stack.name }} + {{ resource.logical_resource_id }} + {{ resource.physical_resource_id }} + {{ resource.type }} + 2010-07-27T22:27:28Z + {{ stack.status }} + + +""" + + +DESCRIBE_STACK_RESOURCES_RESPONSE = """ {% for resource in stack.stack_resources %} From 99af8bdb79d90534400bebd1ca547dd75fcc6338 Mon Sep 17 00:00:00 2001 From: Andrew Garrett Date: Mon, 29 Feb 2016 19:50:27 +0000 Subject: [PATCH 4/6] Convert VPCZoneIdentifier list in template resource to csv Although the boto docs say to use a csv, CloudFormation templates use a list instead: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-group.html#aws-properties-as-group-prop Without this change, templates specifying VPCZoneIdentifier will break as the identifier will be the repr of the list. Signed-off-by: Scott Greene --- moto/autoscaling/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py index 812323781..d7f0db620 100644 --- a/moto/autoscaling/models.py +++ b/moto/autoscaling/models.py @@ -156,7 +156,7 @@ class FakeAutoScalingGroup(object): max_size=properties.get("MaxSize"), min_size=properties.get("MinSize"), launch_config_name=launch_config_name, - vpc_zone_identifier=properties.get("VPCZoneIdentifier"), + vpc_zone_identifier=(','.join(properties.get("VPCZoneIdentifier", [])) or None), default_cooldown=properties.get("Cooldown"), health_check_period=properties.get("HealthCheckGracePeriod"), health_check_type=properties.get("HealthCheckType"), From 993087f2bbf5a745c31e916d559c95efa2211fd9 Mon Sep 17 00:00:00 2001 From: Andrew Garrett Date: Mon, 29 Feb 2016 19:50:28 +0000 Subject: [PATCH 5/6] Fix AutoScalingGroup tags in DescribeAutoScalingGroups I'm not certain that this is the approach that's desired. It'd be nice to dynamically convert the keys one way or the other. Looking for feedback. Signed-off-by: Scott Greene --- moto/autoscaling/responses.py | 10 +++++----- .../test_cloudformation_stack_crud_boto3.py | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/moto/autoscaling/responses.py b/moto/autoscaling/responses.py index d6fc7a12b..79cb6aeee 100644 --- a/moto/autoscaling/responses.py +++ b/moto/autoscaling/responses.py @@ -241,11 +241,11 @@ DESCRIBE_AUTOSCALING_GROUPS_TEMPLATE = """ {% for tag in group.tags %} - {{ tag.resource_type }} - {{ tag.resource_id }} - {{ tag.propagate_at_launch }} - {{ tag.key }} - {{ tag.value }} + {{ tag.resource_type or tag.ResourceType }} + {{ tag.resource_id or tag.ResourceId }} + {{ tag.propagate_at_launch or tag.PropagateAtLaunch }} + {{ tag.key or tag.Key }} + {{ tag.value or tag.Value }} {% endfor %} diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py index d16a2d7b5..94d645a43 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -227,5 +227,6 @@ def test_stack_tags(): TemplateBody=dummy_template_json, Tags=tags, ) - - stack.tags.should.equal(tags) + observed_tag_items = set(item for items in [tag.items() for tag in stack.tags] for item in items) + expected_tag_items = set(item for items in [tag.items() for tag in tags] for item in items) + observed_tag_items.should.equal(expected_tag_items) From 39d9fbcd02263cbc8983d9bdfd9656e48eeaa031 Mon Sep 17 00:00:00 2001 From: Andrew Garrett Date: Mon, 29 Feb 2016 19:50:29 +0000 Subject: [PATCH 6/6] Added resource deletion upon stack deletion Only implemented for ASGs and LCs since they're all we cared about for our particular problem. It should be easy to follow this pattern for other resource types, though. Signed-off-by: Scott Greene --- moto/autoscaling/models.py | 8 ++++++++ moto/cloudformation/models.py | 11 ++++++++--- moto/cloudformation/parsing.py | 5 +++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py index d7f0db620..5a044c038 100644 --- a/moto/autoscaling/models.py +++ b/moto/autoscaling/models.py @@ -80,6 +80,10 @@ class FakeLaunchConfiguration(object): ) return config + def delete(self, region_name): + backend = autoscaling_backends[region_name] + backend.delete_launch_configuration(self.name) + @property def physical_resource_id(self): return self.name @@ -167,6 +171,10 @@ class FakeAutoScalingGroup(object): ) return group + def delete(self, region_name): + backend = autoscaling_backends[region_name] + backend.delete_autoscaling_group(self.name) + @property def physical_resource_id(self): return self.name diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index cd99856f5..d8793b8ea 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -52,6 +52,10 @@ class FakeStack(object): self.resource_map.update(json.loads(template)) self.output_map = self._create_output_map() + def delete(self): + self.resource_map.delete() + self.status = 'DELETE_COMPLETE' + class CloudFormationBackend(BaseBackend): @@ -112,13 +116,14 @@ class CloudFormationBackend(BaseBackend): if name_or_stack_id in self.stacks: # Delete by stack id stack = self.stacks.pop(name_or_stack_id, None) - stack.status = 'DELETE_COMPLETE' + stack.delete() self.deleted_stacks[stack.stack_id] = stack return self.stacks.pop(name_or_stack_id, None) else: # Delete by stack name - stack_to_delete = [stack for stack in self.stacks.values() if stack.name == name_or_stack_id][0] - self.delete_stack(stack_to_delete.stack_id) + for stack in list(self.stacks.values()): + if stack.name == name_or_stack_id: + self.delete_stack(stack.stack_id) cloudformation_backends = {} diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index ebdd83634..32b9f351f 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -374,6 +374,11 @@ class ResourceMap(collections.Mapping): changed_resource = parse_and_update_resource(resource_name, resource_json, self, self._region_name) self._parsed_resources[resource_name] = changed_resource + def delete(self): + for resource in self.resources: + parsed_resource = self._parsed_resources.pop(resource) + parsed_resource.delete(self._region_name) + class OutputMap(collections.Mapping): def __init__(self, resources, template):