From 1e4df18c42669ce43162b5ff77f62d7c88acca97 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Wed, 31 Dec 2014 14:21:47 -0500 Subject: [PATCH] Allow actual use of cloudformation input parameters. --- moto/cloudformation/exceptions.py | 30 ++++++++++++++----- moto/cloudformation/models.py | 14 ++++++--- moto/cloudformation/parsing.py | 26 ++++++++++++---- moto/cloudformation/responses.py | 17 +++++++++++ .../test_cloudformation_stack_crud.py | 30 +++++++++++++++++-- .../test_cloudformation_stack_integration.py | 13 ++++++++ .../test_cloudformation/test_stack_parsing.py | 6 +++- 7 files changed, 114 insertions(+), 22 deletions(-) diff --git a/moto/cloudformation/exceptions.py b/moto/cloudformation/exceptions.py index 8fc7dce0c..1f480e321 100644 --- a/moto/cloudformation/exceptions.py +++ b/moto/cloudformation/exceptions.py @@ -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 = """ + +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 = """ Sender - ValidationError - Stack:{{ name_or_id }} does not exist + {{ code }} + {{ message }} cf4c737e-5ae2-11e4-a7c9-ad44eEXAMPLE diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index 229380796..4e3027f8b 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -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, ) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index f29c77003..fb54ead08 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -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() diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index 4fd322ff4..1aa4c9117 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -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 = """ {% endfor %} + + {% for param_name, param_value in stack.stack_parameters.items() %} + + {{ param_name }} + {{ param_value }} + + {% endfor %} + {% endfor %} diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index ec521ee15..5bdccb434 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -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() diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index 494fb0dfe..387c69bed 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -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(): diff --git a/tests/test_cloudformation/test_stack_parsing.py b/tests/test_cloudformation/test_stack_parsing.py index 548c6fd66..77d314596 100644 --- a/tests/test_cloudformation/test_stack_parsing.py +++ b/tests/test_cloudformation/test_stack_parsing.py @@ -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)