diff --git a/moto/cloudformation/exceptions.py b/moto/cloudformation/exceptions.py index 56a95382a..6ea15c5ca 100644 --- a/moto/cloudformation/exceptions.py +++ b/moto/cloudformation/exceptions.py @@ -33,6 +33,18 @@ class MissingParameterError(BadRequest): ) +class ExportNotFound(BadRequest): + """Exception to raise if a template tries to import a non-existent export""" + + def __init__(self, export_name): + template = Template(ERROR_RESPONSE) + super(ExportNotFound, self).__init__() + self.description = template.render( + code='ExportNotFound', + message="No export named {0} found.".format(export_name) + ) + + ERROR_RESPONSE = """ Sender diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index 57f42df56..e5ab7255d 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -38,7 +38,7 @@ class FakeStack(BaseModel): resource_status_reason="User Initiated") self.description = self.template_dict.get('Description') - self.cross_stack_resources = cross_stack_resources or [] + self.cross_stack_resources = cross_stack_resources or {} self.resource_map = self._create_resource_map() self.output_map = self._create_output_map() self._add_stack_event("CREATE_COMPLETE") diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index bad9ee620..19018158d 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -28,7 +28,7 @@ from moto.s3 import models as s3_models from moto.sns import models as sns_models from moto.sqs import models as sqs_models from .utils import random_suffix -from .exceptions import MissingParameterError, UnformattedGetAttTemplateException, ValidationError +from .exceptions import ExportNotFound, MissingParameterError, UnformattedGetAttTemplateException, ValidationError from boto.cloudformation.stack import Output MODEL_MAP = { @@ -206,6 +206,8 @@ def clean_json(resource_json, resources_map): values = [x.value for x in resources_map.cross_stack_resources.values() if x.name == cleaned_val] if any(values): return values[0] + else: + raise ExportNotFound(cleaned_val) if 'Fn::GetAZs' in resource_json: region = resource_json.get('Fn::GetAZs') or DEFAULT_REGION diff --git a/tests/test_cloudformation/test_import_value.py b/tests/test_cloudformation/test_import_value.py new file mode 100644 index 000000000..04c2b5801 --- /dev/null +++ b/tests/test_cloudformation/test_import_value.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function, unicode_literals + +# Standard library modules +import unittest + +# Third-party modules +import boto3 +from botocore.exceptions import ClientError + +# Package modules +from moto import mock_cloudformation + +AWS_REGION = 'us-west-1' + +SG_STACK_NAME = 'simple-sg-stack' +SG_TEMPLATE = """ +AWSTemplateFormatVersion: 2010-09-09 +Description: Simple test CF template for moto_cloudformation + + +Resources: + SimpleSecurityGroup: + Type: AWS::EC2::SecurityGroup + Description: "A simple security group" + Properties: + GroupName: simple-security-group + GroupDescription: "A simple security group" + SecurityGroupEgress: + - + Description: "Egress to remote HTTPS servers" + CidrIp: 0.0.0.0/0 + IpProtocol: tcp + FromPort: 443 + ToPort: 443 + +Outputs: + SimpleSecurityGroupName: + Value: !GetAtt SimpleSecurityGroup.GroupId + Export: + Name: "SimpleSecurityGroup" + +""" + +EC2_STACK_NAME = 'simple-ec2-stack' +EC2_TEMPLATE = """ +--- +# The latest template format version is "2010-09-09" and as of 2018-04-09 +# is currently the only valid value. +AWSTemplateFormatVersion: 2010-09-09 +Description: Simple test CF template for moto_cloudformation + + +Resources: + SimpleInstance: + Type: AWS::EC2::Instance + Properties: + ImageId: ami-03cf127a + InstanceType: t2.micro + SecurityGroups: !Split [',', !ImportValue SimpleSecurityGroup] +""" + + +class TestSimpleInstance(unittest.TestCase): + def test_simple_instance(self): + """Test that we can create a simple CloudFormation stack that imports values from an existing CloudFormation stack""" + with mock_cloudformation(): + client = boto3.client('cloudformation', region_name=AWS_REGION) + client.create_stack(StackName=SG_STACK_NAME, TemplateBody=SG_TEMPLATE) + response = client.create_stack(StackName=EC2_STACK_NAME, TemplateBody=EC2_TEMPLATE) + self.assertIn('StackId', response) + response = client.describe_stacks(StackName=response['StackId']) + self.assertIn('Stacks', response) + stack_info = response['Stacks'] + self.assertEqual(1, len(stack_info)) + self.assertIn('StackName', stack_info[0]) + self.assertEqual(EC2_STACK_NAME, stack_info[0]['StackName']) + + def test_simple_instance_missing_export(self): + """Test that we get an exception if a CloudFormation stack tries to imports a non-existent export value""" + with mock_cloudformation(): + client = boto3.client('cloudformation', region_name=AWS_REGION) + with self.assertRaises(ClientError) as e: + client.create_stack(StackName=EC2_STACK_NAME, TemplateBody=EC2_TEMPLATE) + self.assertIn('Error', e.exception.response) + self.assertIn('Code', e.exception.response['Error']) + self.assertEqual('ExportNotFound', e.exception.response['Error']['Code'])