diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index fb54ead08..23940f081 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -95,6 +95,13 @@ def clean_json(resource_json, resources_map): join_list.append(cleaned_val if cleaned_val else '{0}'.format(val)) return resource_json['Fn::Join'][0].join(join_list) + if 'Fn::If' in resource_json: + condition_name, true_value, false_value = resource_json['Fn::If'] + if resources_map[condition_name]: + return true_value + else: + return false_value + cleaned_json = {} for key, value in resource_json.items(): cleaned_json[key] = clean_json(value, resources_map) @@ -126,6 +133,11 @@ def parse_resource(logical_id, resource_json, resources_map, region_name): if not resource_class: return None + condition = resource_json.get('Condition') + if condition and not resources_map[condition]: + # If this has a False condition, don't create the resource + return None + resource_json = clean_json(resource_json, resources_map) resource_name_property = resource_name_property_from_type(resource_type) if resource_name_property: @@ -148,6 +160,36 @@ def parse_resource(logical_id, resource_json, resources_map, region_name): return resource +def parse_condition(condition, resources_map): + if isinstance(condition, bool): + return condition + + condition_operator = condition.keys()[0] + + condition_values = [] + for value in condition.values()[0]: + # Check if we are referencing another Condition + if 'Condition' in value: + condition_values.append(resources_map[value['Condition']]) + else: + condition_values.append(clean_json(value, resources_map)) + + if condition_operator == "Fn::Equals": + return condition_values[0] == condition_values[1] + elif condition_operator == "Fn::Not": + return not parse_condition(condition_values[0], resources_map) + elif condition_operator == "Fn::And": + return all([ + parse_condition(condition_value, resources_map) + for condition_value + in condition_values]) + elif condition_operator == "Fn::Or": + return any([ + parse_condition(condition_value, resources_map) + for condition_value + in condition_values]) + + def parse_output(output_logical_id, output_json, resources_map): output_json = clean_json(output_json, resources_map) output = Output() @@ -218,8 +260,15 @@ class ResourceMap(collections.Mapping): self._parsed_resources.update(self.resolved_parameters) + def load_conditions(self): + conditions = self._template.get('Conditions', {}) + + for condition_name, condition in conditions.items(): + self._parsed_resources[condition_name] = parse_condition(condition, self._parsed_resources) + def create(self): self.load_parameters() + self.load_conditions() # Since this is a lazy map, to create every object we just need to # iterate through self. diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index 387c69bed..df9a21584 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -8,6 +8,7 @@ import boto.ec2.autoscale import boto.ec2.elb from boto.exception import BotoServerError import boto.iam +import boto.sqs import boto.vpc import sure # noqa @@ -17,6 +18,7 @@ from moto import ( mock_ec2, mock_elb, mock_iam, + mock_sqs, ) from .fixtures import ( @@ -548,3 +550,105 @@ def test_fn_join(): stack = conn.describe_stacks()[0] fn_join_output = stack.outputs[0] fn_join_output.value.should.equal('test eip:{0}'.format(eip.public_ip)) + + +@mock_cloudformation() +@mock_sqs() +def test_conditional_resources(): + sqs_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Parameters": { + "EnvType": { + "Description": "Environment type.", + "Type": "String", + } + }, + "Conditions": { + "CreateQueue": {"Fn::Equals": [{"Ref": "EnvType"}, "prod"]} + }, + "Resources": { + "QueueGroup": { + "Condition": "CreateQueue", + "Type": "AWS::SQS::Queue", + "Properties": { + "QueueName": "my-queue", + "VisibilityTimeout": 60, + } + }, + }, + } + sqs_template_json = json.dumps(sqs_template) + + conn = boto.cloudformation.connect_to_region("us-west-1") + conn.create_stack( + "test_stack_without_queue", + template_body=sqs_template_json, + parameters=[("EnvType", "staging")], + ) + sqs_conn = boto.sqs.connect_to_region("us-west-1") + list(sqs_conn.get_all_queues()).should.have.length_of(0) + + conn = boto.cloudformation.connect_to_region("us-west-1") + conn.create_stack( + "test_stack_with_queue", + template_body=sqs_template_json, + parameters=[("EnvType", "prod")], + ) + sqs_conn = boto.sqs.connect_to_region("us-west-1") + list(sqs_conn.get_all_queues()).should.have.length_of(1) + + +@mock_cloudformation() +@mock_ec2() +def test_conditional_if_handling(): + dummy_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Conditions": { + "EnvEqualsPrd": { + "Fn::Equals": [ + { + "Ref": "ENV" + }, + "prd" + ] + } + }, + "Parameters": { + "ENV": { + "Default": "dev", + "Description": "Deployment environment for the stack (dev/prd)", + "Type": "String" + }, + }, + "Description": "Stack 1", + "Resources": { + "App1": { + "Properties": { + "ImageId": { + "Fn::If": [ + "EnvEqualsPrd", + "ami-00000000", + "ami-ffffffff" + ] + }, + }, + "Type": "AWS::EC2::Instance" + }, + } + } + dummy_template_json = json.dumps(dummy_template) + + conn = boto.cloudformation.connect_to_region("us-west-1") + conn.create_stack('test_stack1', template_body=dummy_template_json) + ec2_conn = boto.ec2.connect_to_region("us-west-1") + reservation = ec2_conn.get_all_instances()[0] + ec2_instance = reservation.instances[0] + ec2_instance.image_id.should.equal("ami-ffffffff") + ec2_instance.terminate() + + conn = boto.cloudformation.connect_to_region("us-west-2") + conn.create_stack('test_stack1', template_body=dummy_template_json, parameters=[("ENV", "prd")]) + ec2_conn = boto.ec2.connect_to_region("us-west-2") + reservation = ec2_conn.get_all_instances()[0] + ec2_instance = reservation.instances[0] + ec2_instance.image_id.should.equal("ami-00000000") diff --git a/tests/test_cloudformation/test_stack_parsing.py b/tests/test_cloudformation/test_stack_parsing.py index 77d314596..13d602ebb 100644 --- a/tests/test_cloudformation/test_stack_parsing.py +++ b/tests/test_cloudformation/test_stack_parsing.py @@ -5,7 +5,7 @@ from mock import patch import sure # noqa from moto.cloudformation.models import FakeStack -from moto.cloudformation.parsing import resource_class_from_type +from moto.cloudformation.parsing import resource_class_from_type, parse_condition from moto.sqs.models import Queue from boto.cloudformation.stack import Output from boto.exception import BotoServerError @@ -145,3 +145,86 @@ 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) + + +def test_parse_equals_condition(): + parse_condition( + condition={"Fn::Equals": [{"Ref": "EnvType"}, "prod"]}, + resources_map={"EnvType": "prod"}, + ).should.equal(True) + + parse_condition( + condition={"Fn::Equals": [{"Ref": "EnvType"}, "prod"]}, + resources_map={"EnvType": "staging"}, + ).should.equal(False) + + +def test_parse_not_condition(): + parse_condition( + condition={ + "Fn::Not": [{ + "Fn::Equals": [{"Ref": "EnvType"}, "prod"] + }] + }, + resources_map={"EnvType": "prod"}, + ).should.equal(False) + + parse_condition( + condition={ + "Fn::Not": [{ + "Fn::Equals": [{"Ref": "EnvType"}, "prod"] + }] + }, + resources_map={"EnvType": "staging"}, + ).should.equal(True) + + +def test_parse_and_condition(): + parse_condition( + condition={ + "Fn::And": [ + {"Fn::Equals": [{"Ref": "EnvType"}, "prod"]}, + {"Fn::Equals": [{"Ref": "EnvType"}, "staging"]}, + ] + }, + resources_map={"EnvType": "prod"}, + ).should.equal(False) + + parse_condition( + condition={ + "Fn::And": [ + {"Fn::Equals": [{"Ref": "EnvType"}, "prod"]}, + {"Fn::Equals": [{"Ref": "EnvType"}, "prod"]}, + ] + }, + resources_map={"EnvType": "prod"}, + ).should.equal(True) + + +def test_parse_or_condition(): + parse_condition( + condition={ + "Fn::Or": [ + {"Fn::Equals": [{"Ref": "EnvType"}, "prod"]}, + {"Fn::Equals": [{"Ref": "EnvType"}, "staging"]}, + ] + }, + resources_map={"EnvType": "prod"}, + ).should.equal(True) + + parse_condition( + condition={ + "Fn::Or": [ + {"Fn::Equals": [{"Ref": "EnvType"}, "staging"]}, + {"Fn::Equals": [{"Ref": "EnvType"}, "staging"]}, + ] + }, + resources_map={"EnvType": "prod"}, + ).should.equal(False) + + +def test_reference_other_conditions(): + parse_condition( + condition={"Fn::Not": [{"Condition": "OtherCondition"}]}, + resources_map={"OtherCondition": True}, + ).should.equal(False)