diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index c5196b2d..8f233efc 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 6595019f..b924ac9e 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 6f90586a..96efff76 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 00000000..d16a2d7b --- /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 ffbc5c60..b4f50024 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)