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)