From 2f6f42a183c561ce8836433fb4d3349b0d6306be Mon Sep 17 00:00:00 2001 From: Toshiya Kawasaki Date: Fri, 8 Sep 2017 03:28:15 +0900 Subject: [PATCH] handle short form function in cfn yaml template (#1103) --- moto/cloudformation/models.py | 3 +- moto/cloudformation/responses.py | 3 +- moto/cloudformation/utils.py | 20 ++++ .../test_cloudformation_stack_crud_boto3.py | 103 +++++++++++++++++- .../test_cloudformation/test_stack_parsing.py | 47 ++++++++ 5 files changed, 172 insertions(+), 4 deletions(-) diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index ec922d8f5..e579e4c08 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -9,7 +9,7 @@ from moto.compat import OrderedDict from moto.core import BaseBackend, BaseModel from .parsing import ResourceMap, OutputMap -from .utils import generate_stack_id +from .utils import generate_stack_id, yaml_tag_constructor from .exceptions import ValidationError @@ -74,6 +74,7 @@ class FakeStack(BaseModel): )) def _parse_template(self): + yaml.add_multi_constructor('', yaml_tag_constructor) try: self.template_dict = yaml.load(self.template) except yaml.parser.ParserError: diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index d66a172a8..423cf92c1 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -391,8 +391,7 @@ LIST_STACKS_RESOURCES_RESPONSE = """ GET_TEMPLATE_RESPONSE_TEMPLATE = """ - {{ stack.template }} - + {{ stack.template }} b9b4b068-3a41-11e5-94eb-example diff --git a/moto/cloudformation/utils.py b/moto/cloudformation/utils.py index 1d629c76b..384ea5401 100644 --- a/moto/cloudformation/utils.py +++ b/moto/cloudformation/utils.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import uuid import six import random +import yaml def generate_stack_id(stack_name): @@ -13,3 +14,22 @@ def random_suffix(): size = 12 chars = list(range(10)) + ['A-Z'] return ''.join(six.text_type(random.choice(chars)) for x in range(size)) + + +def yaml_tag_constructor(loader, tag, node): + """convert shorthand intrinsic function to full name + """ + def _f(loader, tag, node): + if tag == '!GetAtt': + return node.value.split('.') + elif type(node) == yaml.SequenceNode: + return loader.construct_sequence(node) + else: + return node.value + + if tag == '!Ref': + key = 'Ref' + else: + key = 'Fn::{}'.format(tag[1:]) + + return {key: _f(loader, tag, node)} diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py index e428d1f63..ed2ee8337 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -39,6 +39,68 @@ dummy_template = { } } + +dummy_template_yaml = """--- +AWSTemplateFormatVersion: 2010-09-09 +Description: Stack1 with yaml template +Resources: + EC2Instance1: + Type: AWS::EC2::Instance + Properties: + ImageId: ami-d3adb33f + KeyName: dummy + InstanceType: t2.micro + Tags: + - Key: Description + Value: Test tag + - Key: Name + Value: Name tag for tests +""" + + +dummy_template_yaml_with_short_form_func = """--- +AWSTemplateFormatVersion: 2010-09-09 +Description: Stack1 with yaml template +Resources: + EC2Instance1: + Type: AWS::EC2::Instance + Properties: + ImageId: ami-d3adb33f + KeyName: !Join [ ":", [ du, m, my ] ] + InstanceType: t2.micro + Tags: + - Key: Description + Value: Test tag + - Key: Name + Value: Name tag for tests +""" + + +dummy_template_yaml_with_ref = """--- +AWSTemplateFormatVersion: 2010-09-09 +Description: Stack1 with yaml template +Parameters: + TagDescription: + Type: String + TagName: + Type: String + +Resources: + EC2Instance1: + Type: AWS::EC2::Instance + Properties: + ImageId: ami-d3adb33f + KeyName: dummy + InstanceType: t2.micro + Tags: + - Key: Description + Value: + Ref: TagDescription + - Key: Name + Value: !Ref TagName +""" + + dummy_update_template = { "AWSTemplateFormatVersion": "2010-09-09", "Parameters": { @@ -110,6 +172,46 @@ def test_boto3_create_stack(): cf_conn.get_template(StackName="test_stack")['TemplateBody'].should.equal( dummy_template) +@mock_cloudformation +def test_boto3_create_stack_with_yaml(): + cf_conn = boto3.client('cloudformation', region_name='us-east-1') + cf_conn.create_stack( + StackName="test_stack", + TemplateBody=dummy_template_yaml, + ) + + cf_conn.get_template(StackName="test_stack")['TemplateBody'].should.equal( + dummy_template_yaml) + + +@mock_cloudformation +def test_boto3_create_stack_with_short_form_func_yaml(): + cf_conn = boto3.client('cloudformation', region_name='us-east-1') + cf_conn.create_stack( + StackName="test_stack", + TemplateBody=dummy_template_yaml_with_short_form_func, + ) + + cf_conn.get_template(StackName="test_stack")['TemplateBody'].should.equal( + dummy_template_yaml_with_short_form_func) + + +@mock_cloudformation +def test_boto3_create_stack_with_ref_yaml(): + cf_conn = boto3.client('cloudformation', region_name='us-east-1') + params = [ + {'ParameterKey': 'TagDescription', 'ParameterValue': 'desc_ref'}, + {'ParameterKey': 'TagName', 'ParameterValue': 'name_ref'}, + ] + cf_conn.create_stack( + StackName="test_stack", + TemplateBody=dummy_template_yaml_with_ref, + Parameters=params + ) + + cf_conn.get_template(StackName="test_stack")['TemplateBody'].should.equal( + dummy_template_yaml_with_ref) + @mock_cloudformation def test_creating_stacks_across_regions(): @@ -150,7 +252,6 @@ def test_create_stack_with_role_arn(): TemplateBody=dummy_template_json, RoleARN='arn:aws:iam::123456789012:role/moto', ) - stack = list(cf.stacks.all())[0] stack.role_arn.should.equal('arn:aws:iam::123456789012:role/moto') diff --git a/tests/test_cloudformation/test_stack_parsing.py b/tests/test_cloudformation/test_stack_parsing.py index ee53e9a68..d9fe4d80d 100644 --- a/tests/test_cloudformation/test_stack_parsing.py +++ b/tests/test_cloudformation/test_stack_parsing.py @@ -10,8 +10,11 @@ from moto.cloudformation.models import FakeStack from moto.cloudformation.parsing import resource_class_from_type, parse_condition, Export from moto.sqs.models import Queue from moto.s3.models import FakeBucket +from moto.cloudformation.utils import yaml_tag_constructor from boto.cloudformation.stack import Output + + dummy_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -380,3 +383,47 @@ def test_import(): queue = import_stack.resource_map['Queue'] queue.name.should.equal("value") + + + +def test_short_form_func_in_yaml_teamplate(): + template = """--- + KeyB64: !Base64 valueToEncode + KeyRef: !Ref foo + KeyAnd: !And + - A + - B + KeyEquals: !Equals [A, B] + KeyIf: !If [A, B, C] + KeyNot: !Not [A] + KeyOr: !Or [A, B] + KeyFindInMap: !FindInMap [A, B, C] + KeyGetAtt: !GetAtt A.B + KeyGetAZs: !GetAZs A + KeyImportValue: !ImportValue A + KeyJoin: !Join [ ":", [A, B, C] ] + KeySelect: !Select [A, B] + KeySplit: !Split [A, B] + KeySub: !Sub A + """ + yaml.add_multi_constructor('', yaml_tag_constructor) + template_dict = yaml.load(template) + key_and_expects = [ + ['KeyRef', {'Ref': 'foo'}], + ['KeyB64', {'Fn::Base64': 'valueToEncode'}], + ['KeyAnd', {'Fn::And': ['A', 'B']}], + ['KeyEquals', {'Fn::Equals': ['A', 'B']}], + ['KeyIf', {'Fn::If': ['A', 'B', 'C']}], + ['KeyNot', {'Fn::Not': ['A']}], + ['KeyOr', {'Fn::Or': ['A', 'B']}], + ['KeyFindInMap', {'Fn::FindInMap': ['A', 'B', 'C']}], + ['KeyGetAtt', {'Fn::GetAtt': ['A', 'B']}], + ['KeyGetAZs', {'Fn::GetAZs': 'A'}], + ['KeyImportValue', {'Fn::ImportValue': 'A'}], + ['KeyJoin', {'Fn::Join': [ ":", [ 'A', 'B', 'C' ] ]}], + ['KeySelect', {'Fn::Select': ['A', 'B']}], + ['KeySplit', {'Fn::Split': ['A', 'B']}], + ['KeySub', {'Fn::Sub': 'A'}], + ] + for k, v in key_and_expects: + template_dict.should.have.key(k).which.should.be.equal(v)