diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py index 812323781..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 @@ -156,7 +160,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"), @@ -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/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/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): diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index c5196b2df..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): @@ -47,14 +48,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 @@ -65,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): @@ -87,18 +106,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,59 +143,108 @@ 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 %} - + + + {% 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_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 %} + + {{ stack.stack_id }} + {{ stack.name }} + {{ resource.logical_resource_id }} + {{ resource.physical_resource_id }} + {{ resource.type }} + 2010-07-27T22:27:28Z + {{ stack.status }} {% endfor %} - -""" + +""" LIST_STACKS_RESPONSE = """ @@ -193,23 +264,6 @@ LIST_STACKS_RESPONSE = """ """ -DESCRIBE_STACKS_RESOURCES_RESPONSE = """ - - {% for resource in stack.stack_resources %} - - {{ stack.stack_id }} - {{ stack.name }} - {{ resource.logical_resource_id }} - {{ resource.physical_resource_id }} - {{ resource.type }} - 2010-07-27T22:27:28Z - {{ stack.status }} - - {% endfor %} - -""" - - LIST_STACKS_RESOURCES_RESPONSE = """ @@ -228,3 +282,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..94d645a43 --- /dev/null +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -0,0 +1,232 @@ +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, + ) + 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) 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)