Allow actual use of cloudformation input parameters.

This commit is contained in:
Steve Pulec 2014-12-31 14:21:47 -05:00
parent 56007660d8
commit 1e4df18c42
7 changed files with 114 additions and 22 deletions

View File

@ -1,5 +1,5 @@
from __future__ import unicode_literals
from boto.exception import BotoServerError
from werkzeug.exceptions import BadRequest
from jinja2 import Template
@ -8,17 +8,31 @@ class UnformattedGetAttTemplateException(Exception):
status_code = 400
class ValidationError(BotoServerError):
class ValidationError(BadRequest):
def __init__(self, name_or_id):
template = Template(STACK_DOES_NOT_EXIST_RESPONSE)
super(ValidationError, self).__init__(status=400, reason='Bad Request',
body=template.render(name_or_id=name_or_id))
template = Template(ERROR_RESPONSE)
super(ValidationError, self).__init__()
self.description = template.render(
code="ValidationError",
messgae="Stack:{0} does not exist".format(name_or_id),
)
STACK_DOES_NOT_EXIST_RESPONSE = """<ErrorResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
class MissingParameterError(BadRequest):
def __init__(self, parameter_name):
template = Template(ERROR_RESPONSE)
super(MissingParameterError, self).__init__()
self.description = template.render(
code="Missing Parameter",
messgae="Missing parameter {0}".format(parameter_name),
)
ERROR_RESPONSE = """<ErrorResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
<Error>
<Type>Sender</Type>
<Code>ValidationError</Code>
<Message>Stack:{{ name_or_id }} does not exist</Message>
<Code>{{ code }}</Code>
<Message>{{ message }}</Message>
</Error>
<RequestId>cf4c737e-5ae2-11e4-a7c9-ad44eEXAMPLE</RequestId>
</ErrorResponse>

View File

@ -10,23 +10,28 @@ from .exceptions import ValidationError
class FakeStack(object):
def __init__(self, stack_id, name, template, region_name, notification_arns=None):
def __init__(self, stack_id, name, template, parameters, region_name, notification_arns=None):
self.stack_id = stack_id
self.name = name
self.template = template
self.parameters = parameters
self.region_name = region_name
self.notification_arns = notification_arns if notification_arns else []
self.template = template
self.status = 'CREATE_COMPLETE'
template_dict = json.loads(self.template)
self.description = template_dict.get('Description')
self.resource_map = ResourceMap(stack_id, name, region_name, template_dict)
self.resource_map = ResourceMap(stack_id, name, parameters, region_name, template_dict)
self.resource_map.create()
self.output_map = OutputMap(self.resource_map, template_dict)
self.output_map.create()
@property
def stack_parameters(self):
return self.resource_map.resolved_parameters
@property
def stack_resources(self):
return self.resource_map.values()
@ -42,12 +47,13 @@ class CloudFormationBackend(BaseBackend):
self.stacks = {}
self.deleted_stacks = {}
def create_stack(self, name, template, region_name, notification_arns=None):
def create_stack(self, name, template, parameters, region_name, notification_arns=None):
stack_id = generate_stack_id(name)
new_stack = FakeStack(
stack_id=stack_id,
name=name,
template=template,
parameters=parameters,
region_name=region_name,
notification_arns=notification_arns,
)

View File

@ -8,7 +8,7 @@ from moto.elb import models as elb_models
from moto.iam import models as iam_models
from moto.sqs import models as sqs_models
from .utils import random_suffix
from .exceptions import UnformattedGetAttTemplateException
from .exceptions import MissingParameterError, UnformattedGetAttTemplateException
from boto.cloudformation.stack import Output
from boto.exception import BotoServerError
@ -164,10 +164,12 @@ class ResourceMap(collections.Mapping):
each resources is passed this lazy map that it can grab dependencies from.
"""
def __init__(self, stack_id, stack_name, region_name, template):
def __init__(self, stack_id, stack_name, parameters, region_name, template):
self._template = template
self._resource_json_map = template['Resources']
self._region_name = region_name
self.input_parameters = parameters
self.resolved_parameters = {}
# Create the default resources
self._parsed_resources = {
@ -199,10 +201,22 @@ class ResourceMap(collections.Mapping):
return self._resource_json_map.keys()
def load_parameters(self):
parameters = self._template.get('Parameters', {})
for parameter_name, parameter in parameters.items():
# Just initialize parameters to empty string for now.
self._parsed_resources[parameter_name] = ""
parameter_slots = self._template.get('Parameters', {})
for parameter_name, parameter in parameter_slots.items():
# Set the default values.
self.resolved_parameters[parameter_name] = parameter.get('Default')
# Set any input parameters that were passed
for key, value in self.input_parameters.items():
if key in self.resolved_parameters:
self.resolved_parameters[key] = value
# Check if there are any non-default params that were not passed input params
for key, value in self.resolved_parameters.items():
if value is None:
raise MissingParameterError(key)
self._parsed_resources.update(self.resolved_parameters)
def create(self):
self.load_parameters()

View File

@ -26,6 +26,14 @@ class CloudFormationResponse(BaseResponse):
stack_name = self._get_param('StackName')
stack_body = self._get_param('TemplateBody')
template_url = self._get_param('TemplateURL')
parameters_list = self._get_list_prefix("Parameters.member")
# Hack dict-comprehension
parameters = dict([
(parameter['parameter_key'], parameter['parameter_value'])
for parameter
in parameters_list
])
if template_url:
stack_body = self._get_stack_from_s3_url(template_url)
stack_notification_arns = self._get_multi_param('NotificationARNs.member')
@ -33,6 +41,7 @@ class CloudFormationResponse(BaseResponse):
stack = self.cloudformation_backend.create_stack(
name=stack_name,
template=stack_body,
parameters=parameters,
region_name=self.region,
notification_arns=stack_notification_arns
)
@ -126,6 +135,14 @@ DESCRIBE_STACKS_TEMPLATE = """<DescribeStacksResult>
</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>
</member>
{% endfor %}
</Stacks>

View File

@ -6,13 +6,13 @@ import boto
import boto.s3
import boto.s3.key
import boto.cloudformation
from boto.exception import BotoServerError
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
from moto import mock_cloudformation, mock_s3
from moto.cloudformation.exceptions import ValidationError
dummy_template = {
"AWSTemplateFormatVersion": "2010-09-09",
@ -182,7 +182,7 @@ def test_delete_stack_by_id():
conn.list_stacks().should.have.length_of(1)
conn.delete_stack(stack_id)
conn.list_stacks().should.have.length_of(0)
with assert_raises(ValidationError):
with assert_raises(BotoServerError):
conn.describe_stacks("test_stack")
conn.describe_stacks(stack_id).should.have.length_of(1)
@ -191,10 +191,34 @@ def test_delete_stack_by_id():
@mock_cloudformation
def test_bad_describe_stack():
conn = boto.connect_cloudformation()
with assert_raises(ValidationError):
with assert_raises(BotoServerError):
conn.describe_stacks("bad_stack")
@mock_cloudformation()
def test_cloudformation_params():
dummy_template = {
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "Stack 1",
"Resources": {},
"Parameters": {
"APPNAME": {
"Default": "app-name",
"Description": "The name of the app",
"Type": "String"
}
}
}
dummy_template_json = json.dumps(dummy_template)
cfn = boto.connect_cloudformation()
cfn.create_stack('test_stack1', template_body=dummy_template_json, parameters=[('APPNAME', 'testing123')])
stack = cfn.describe_stacks('test_stack1')[0]
stack.parameters.should.have.length_of(1)
param = stack.parameters[0]
param.key.should.equal('APPNAME')
param.value.should.equal('testing123')
# @mock_cloudformation
# def test_update_stack():
# conn = boto.connect_cloudformation()

View File

@ -6,6 +6,7 @@ import boto.cloudformation
import boto.ec2
import boto.ec2.autoscale
import boto.ec2.elb
from boto.exception import BotoServerError
import boto.iam
import boto.vpc
import sure # noqa
@ -311,6 +312,7 @@ def test_vpc_single_instance_in_subnet():
conn.create_stack(
"test_stack",
template_body=template_json,
parameters=[("KeyName", "my_key")],
)
vpc_conn = boto.vpc.connect_to_region("us-west-1")
@ -474,6 +476,7 @@ def test_single_instance_with_ebs_volume():
conn.create_stack(
"test_stack",
template_body=template_json,
parameters=[("KeyName", "key_name")]
)
ec2_conn = boto.ec2.connect_to_region("us-west-1")
@ -490,6 +493,16 @@ def test_single_instance_with_ebs_volume():
ebs_volume.physical_resource_id.should.equal(volume.id)
@mock_cloudformation()
def test_create_template_without_required_param():
template_json = json.dumps(single_instance_with_ebs_volume.template)
conn = boto.cloudformation.connect_to_region("us-west-1")
conn.create_stack.when.called_with(
"test_stack",
template_body=template_json,
).should.throw(BotoServerError)
@mock_ec2()
@mock_cloudformation()
def test_classic_eip():

View File

@ -82,6 +82,7 @@ def test_parse_stack_resources():
stack_id="test_id",
name="test_stack",
template=dummy_template_json,
parameters={},
region_name='us-west-1')
stack.resource_map.should.have.length_of(1)
@ -102,6 +103,7 @@ def test_parse_stack_with_name_type_resource():
stack_id="test_id",
name="test_stack",
template=name_type_template_json,
parameters={},
region_name='us-west-1')
stack.resource_map.should.have.length_of(1)
@ -115,6 +117,7 @@ def test_parse_stack_with_outputs():
stack_id="test_id",
name="test_stack",
template=output_type_template_json,
parameters={},
region_name='us-west-1')
stack.output_map.should.have.length_of(1)
@ -129,6 +132,7 @@ def test_parse_stack_with_get_attribute_outputs():
stack_id="test_id",
name="test_stack",
template=get_attribute_outputs_template_json,
parameters={},
region_name='us-west-1')
stack.output_map.should.have.length_of(1)
@ -140,4 +144,4 @@ def test_parse_stack_with_get_attribute_outputs():
def test_parse_stack_with_bad_get_attribute_outputs():
FakeStack.when.called_with(
"test_id", "test_stack", bad_output_template_json, "us-west-1").should.throw(BotoServerError)
"test_id", "test_stack", bad_output_template_json, {}, "us-west-1").should.throw(BotoServerError)