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 <scott.greene@getbraintree.com>
This commit is contained in:
Andrew Garrett 2016-02-29 19:50:23 +00:00 committed by Scott Greene
parent 06b173abe4
commit ec10699c38
5 changed files with 366 additions and 94 deletions

View File

@ -47,14 +47,17 @@ class CloudFormationResponse(BaseResponse):
notification_arns=stack_notification_arns, notification_arns=stack_notification_arns,
tags=tags, tags=tags,
) )
stack_body = { if self.request_json:
return json.dumps({
'CreateStackResponse': { 'CreateStackResponse': {
'CreateStackResult': { 'CreateStackResult': {
'StackId': stack.stack_id, '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): def describe_stacks(self):
stack_name_or_id = None stack_name_or_id = None
@ -88,7 +91,8 @@ class CloudFormationResponse(BaseResponse):
name_or_stack_id = self.querystring.get('StackName')[0] name_or_stack_id = self.querystring.get('StackName')[0]
stack = self.cloudformation_backend.get_stack(name_or_stack_id) stack = self.cloudformation_backend.get_stack(name_or_stack_id)
response = { if self.request_json:
return json.dumps({
"GetTemplateResponse": { "GetTemplateResponse": {
"GetTemplateResult": { "GetTemplateResult": {
"TemplateBody": stack.template, "TemplateBody": stack.template,
@ -97,8 +101,10 @@ class CloudFormationResponse(BaseResponse):
} }
} }
} }
} })
return json.dumps(response) else:
template = self.response_template(GET_TEMPLATE_RESPONSE_TEMPLATE)
return template.render(stack=stack)
def update_stack(self): def update_stack(self):
stack_name = self._get_param('StackName') stack_name = self._get_param('StackName')
@ -121,14 +127,30 @@ class CloudFormationResponse(BaseResponse):
name_or_stack_id = self.querystring.get('StackName')[0] name_or_stack_id = self.querystring.get('StackName')[0]
self.cloudformation_backend.delete_stack(name_or_stack_id) self.cloudformation_backend.delete_stack(name_or_stack_id)
if self.request_json:
return json.dumps({ return json.dumps({
'DeleteStackResponse': { 'DeleteStackResponse': {
'DeleteStackResult': {}, 'DeleteStackResult': {},
} }
}) })
else:
template = self.response_template(DELETE_STACK_RESPONSE_TEMPLATE)
return template.render()
DESCRIBE_STACKS_TEMPLATE = """<DescribeStacksResult> CREATE_STACK_RESPONSE_TEMPLATE = """<CreateStackResponse>
<CreateStackResult>
<StackId>{{ stack.stack_id }}</StackId>
</CreateStackResult>
<ResponseMetadata>
<RequestId>b9b4b068-3a41-11e5-94eb-example</RequestId>
</ResponseMetadata>
</CreateStackResponse>
"""
DESCRIBE_STACKS_TEMPLATE = """<DescribeStacksResponse>
<DescribeStacksResult>
<Stacks> <Stacks>
{% for stack in stacks %} {% for stack in stacks %}
<member> <member>
@ -173,24 +195,8 @@ DESCRIBE_STACKS_TEMPLATE = """<DescribeStacksResult>
</member> </member>
{% endfor %} {% endfor %}
</Stacks> </Stacks>
</DescribeStacksResult>""" </DescribeStacksResult>
</DescribeStacksResponse>"""
LIST_STACKS_RESPONSE = """<ListStacksResponse>
<ListStacksResult>
<StackSummaries>
{% for stack in stacks %}
<member>
<StackId>{{ stack.stack_id }}</StackId>
<StackStatus>{{ stack.status }}</StackStatus>
<StackName>{{ stack.name }}</StackName>
<CreationTime>2011-05-23T15:47:44Z</CreationTime>
<TemplateDescription>{{ stack.description }}</TemplateDescription>
</member>
{% endfor %}
</StackSummaries>
</ListStacksResult>
</ListStacksResponse>"""
DESCRIBE_STACKS_RESOURCES_RESPONSE = """<DescribeStackResourcesResult> DESCRIBE_STACKS_RESOURCES_RESPONSE = """<DescribeStackResourcesResult>
@ -210,6 +216,23 @@ DESCRIBE_STACKS_RESOURCES_RESPONSE = """<DescribeStackResourcesResult>
</DescribeStackResourcesResult>""" </DescribeStackResourcesResult>"""
LIST_STACKS_RESPONSE = """<ListStacksResponse>
<ListStacksResult>
<StackSummaries>
{% for stack in stacks %}
<member>
<StackId>{{ stack.stack_id }}</StackId>
<StackStatus>{{ stack.status }}</StackStatus>
<StackName>{{ stack.name }}</StackName>
<CreationTime>2011-05-23T15:47:44Z</CreationTime>
<TemplateDescription>{{ stack.description }}</TemplateDescription>
</member>
{% endfor %}
</StackSummaries>
</ListStacksResult>
</ListStacksResponse>"""
LIST_STACKS_RESOURCES_RESPONSE = """<ListStackResourcesResponse> LIST_STACKS_RESOURCES_RESPONSE = """<ListStackResourcesResponse>
<ListStackResourcesResult> <ListStackResourcesResult>
<StackResourceSummaries> <StackResourceSummaries>
@ -228,3 +251,22 @@ LIST_STACKS_RESOURCES_RESPONSE = """<ListStackResourcesResponse>
<RequestId>2d06e36c-ac1d-11e0-a958-f9382b6eb86b</RequestId> <RequestId>2d06e36c-ac1d-11e0-a958-f9382b6eb86b</RequestId>
</ResponseMetadata> </ResponseMetadata>
</ListStackResourcesResponse>""" </ListStackResourcesResponse>"""
GET_TEMPLATE_RESPONSE_TEMPLATE = """<GetTemplateResponse>
<GetTemplateResult>
<TemplateBody>{{ stack.template }}
</TemplateBody>
</GetTemplateResult>
<ResponseMetadata>
<RequestId>b9b4b068-3a41-11e5-94eb-example</RequestId>
</ResponseMetadata>
</GetTemplateResponse>"""
DELETE_STACK_RESPONSE_TEMPLATE = """<DeleteStackResponse>
<ResponseMetadata>
<RequestId>5ccc7dcd-744c-11e5-be70-example</RequestId>
</ResponseMetadata>
</DeleteStackResponse>
"""

View File

@ -254,6 +254,10 @@ class BaseResponse(_TemplateEnvironmentMixin):
param_index += 1 param_index += 1
return results return results
@property
def request_json(self):
return 'JSON' in self.querystring.get('ContentType', [])
def metadata_response(request, full_url, headers): def metadata_response(request, full_url, headers):
""" """

View File

@ -12,10 +12,6 @@ class SNSResponse(BaseResponse):
def backend(self): def backend(self):
return sns_backends[self.region] return sns_backends[self.region]
@property
def request_json(self):
return 'JSON' in self.querystring.get('ContentType', [])
def _get_attributes(self): def _get_attributes(self):
attributes = self._get_list_prefix('Attributes.entry') attributes = self._get_list_prefix('Attributes.entry')
return dict( return dict(

View File

@ -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)

View File

@ -19,13 +19,12 @@ def test_cloudformation_server_get():
template_body = { template_body = {
"Resources": {}, "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)) TemplateBody=json.dumps(template_body))
stack_id = res["CreateStackResponse"]["CreateStackResult"]["StackId"] create_stack_resp.should.match(r"<CreateStackResponse>.*<CreateStackResult>.*<StackId>.*</StackId>.*</CreateStackResult>.*</CreateStackResponse>", re.DOTALL)
stack_id_from_create_response = re.search("<StackId>(.*)</StackId>", 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("<StackId>(.*)</StackId>", list_stacks_resp).groups()[0]
stacks = re.search("<StackId>(.*)</StackId>", data) stack_id_from_create_response.should.equal(stack_id_from_list_response)
list_stack_id = stacks.groups()[0]
assert stack_id == list_stack_id