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,
|
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>
|
||||||
|
"""
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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(
|
||||||
|
@ -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 = {
|
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
|
|
||||||
|
Loading…
Reference in New Issue
Block a user