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:
parent
06b173abe4
commit
ec10699c38
@ -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 = """<DescribeStacksResult>
|
||||
<Stacks>
|
||||
{% for stack in stacks %}
|
||||
<member>
|
||||
<StackName>{{ stack.name }}</StackName>
|
||||
<StackId>{{ stack.stack_id }}</StackId>
|
||||
<CreationTime>2010-07-27T22:28:28Z</CreationTime>
|
||||
<StackStatus>{{ stack.status }}</StackStatus>
|
||||
{% if stack.notification_arns %}
|
||||
<NotificationARNs>
|
||||
{% for notification_arn in stack.notification_arns %}
|
||||
<member>{{ notification_arn }}</member>
|
||||
{% endfor %}
|
||||
</NotificationARNs>
|
||||
{% else %}
|
||||
<NotificationARNs/>
|
||||
{% endif %}
|
||||
<DisableRollback>false</DisableRollback>
|
||||
<Outputs>
|
||||
{% for output in stack.stack_outputs %}
|
||||
<member>
|
||||
<OutputKey>{{ output.key }}</OutputKey>
|
||||
<OutputValue>{{ output.value }}</OutputValue>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Outputs>
|
||||
<Parameters>
|
||||
{% for param_name, param_value in stack.stack_parameters.items() %}
|
||||
<member>
|
||||
<ParameterKey>{{ param_name }}</ParameterKey>
|
||||
<ParameterValue>{{ param_value }}</ParameterValue>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Parameters>
|
||||
<Tags>
|
||||
{% for tag_key, tag_value in stack.tags.items() %}
|
||||
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>
|
||||
{% for stack in stacks %}
|
||||
<member>
|
||||
<StackName>{{ stack.name }}</StackName>
|
||||
<StackId>{{ stack.stack_id }}</StackId>
|
||||
<CreationTime>2010-07-27T22:28:28Z</CreationTime>
|
||||
<StackStatus>{{ stack.status }}</StackStatus>
|
||||
{% if stack.notification_arns %}
|
||||
<NotificationARNs>
|
||||
{% for notification_arn in stack.notification_arns %}
|
||||
<member>{{ notification_arn }}</member>
|
||||
{% endfor %}
|
||||
</NotificationARNs>
|
||||
{% else %}
|
||||
<NotificationARNs/>
|
||||
{% endif %}
|
||||
<DisableRollback>false</DisableRollback>
|
||||
<Outputs>
|
||||
{% for output in stack.stack_outputs %}
|
||||
<member>
|
||||
<Key>{{ tag_key }}</Key>
|
||||
<Value>{{ tag_value }}</Value>
|
||||
<OutputKey>{{ output.key }}</OutputKey>
|
||||
<OutputValue>{{ output.value }}</OutputValue>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Tags>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Stacks>
|
||||
</DescribeStacksResult>"""
|
||||
|
||||
|
||||
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>"""
|
||||
</Outputs>
|
||||
<Parameters>
|
||||
{% for param_name, param_value in stack.stack_parameters.items() %}
|
||||
<member>
|
||||
<ParameterKey>{{ param_name }}</ParameterKey>
|
||||
<ParameterValue>{{ param_value }}</ParameterValue>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Parameters>
|
||||
<Tags>
|
||||
{% for tag_key, tag_value in stack.tags.items() %}
|
||||
<member>
|
||||
<Key>{{ tag_key }}</Key>
|
||||
<Value>{{ tag_value }}</Value>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Tags>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Stacks>
|
||||
</DescribeStacksResult>
|
||||
</DescribeStacksResponse>"""
|
||||
|
||||
|
||||
DESCRIBE_STACKS_RESOURCES_RESPONSE = """<DescribeStackResourcesResult>
|
||||
@ -210,6 +216,23 @@ DESCRIBE_STACKS_RESOURCES_RESPONSE = """<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>
|
||||
<ListStackResourcesResult>
|
||||
<StackResourceSummaries>
|
||||
@ -228,3 +251,22 @@ LIST_STACKS_RESOURCES_RESPONSE = """<ListStackResourcesResponse>
|
||||
<RequestId>2d06e36c-ac1d-11e0-a958-f9382b6eb86b</RequestId>
|
||||
</ResponseMetadata>
|
||||
</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>
|
||||
"""
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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(
|
||||
|
@ -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)
|
@ -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"<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)
|
||||
|
||||
list_stack_id = stacks.groups()[0]
|
||||
assert stack_id == list_stack_id
|
||||
stack_id_from_create_response.should.equal(stack_id_from_list_response)
|
||||
|
Loading…
Reference in New Issue
Block a user