Skip to content

Commit 21eda43

Browse files
authored
Merge pull request #538 from troyready/fix_yaml_template_load
fix loading of yaml templates with CFN tags
2 parents 3a3326f + a4b95b8 commit 21eda43

6 files changed

Lines changed: 142 additions & 8 deletions

File tree

stacker/actions/diff.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@
44
import sys
55
from operator import attrgetter
66

7-
import yaml
8-
97
from .base import plan, build_walker
108
from . import build
119
from .. import exceptions
10+
from ..util import parse_cloudformation_template
1211
from ..status import NotSubmittedStatus, NotUpdatedStatus, COMPLETE
1312

1413
logger = logging.getLogger(__name__)
@@ -228,13 +227,13 @@ def _diff_stack(self, stack, **kwargs):
228227
self._print_new_stack(new_stack, parameters)
229228
else:
230229
# Diff our old & new stack/parameters
231-
old_template = yaml.load(old_template)
232-
if isinstance(old_template, str):
230+
old_template = parse_cloudformation_template(old_template)
231+
if isinstance(old_template, (str, unicode)):
233232
# YAML templates returned from CFN need parsing again
234233
# "AWSTemplateFormatVersion: \"2010-09-09\"\nParam..."
235234
# ->
236235
# AWSTemplateFormatVersion: "2010-09-09"
237-
old_template = yaml.load(old_template)
236+
old_template = parse_cloudformation_template(old_template)
238237
old_stack = self._normalize_json(
239238
json.dumps(old_template,
240239
sort_keys=True,

stacker/awscli_yamlhelper.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Copyright 2012-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"). You
4+
# may not use this file except in compliance with the License. A copy of
5+
# the License is located at
6+
#
7+
# http://aws.amazon.com/apache2.0/
8+
#
9+
# or in the "license" file accompanying this file. This file is
10+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11+
# ANY KIND, either express or implied. See the License for the specific
12+
# language governing permissions and limitations under the License.
13+
import json
14+
import yaml
15+
from yaml.resolver import ScalarNode, SequenceNode
16+
17+
from botocore.compat import six
18+
19+
20+
def intrinsics_multi_constructor(loader, tag_prefix, node):
21+
"""
22+
YAML constructor to parse CloudFormation intrinsics.
23+
This will return a dictionary with key being the instrinsic name
24+
"""
25+
26+
# Get the actual tag name excluding the first exclamation
27+
tag = node.tag[1:]
28+
29+
# Some intrinsic functions doesn't support prefix "Fn::"
30+
prefix = "Fn::"
31+
if tag in ["Ref", "Condition"]:
32+
prefix = ""
33+
34+
cfntag = prefix + tag
35+
36+
if tag == "GetAtt" and isinstance(node.value, six.string_types):
37+
# ShortHand notation for !GetAtt accepts Resource.Attribute format
38+
# while the standard notation is to use an array
39+
# [Resource, Attribute]. Convert shorthand to standard format
40+
value = node.value.split(".", 1)
41+
42+
elif isinstance(node, ScalarNode):
43+
# Value of this node is scalar
44+
value = loader.construct_scalar(node)
45+
46+
elif isinstance(node, SequenceNode):
47+
# Value of this node is an array (Ex: [1,2])
48+
value = loader.construct_sequence(node)
49+
50+
else:
51+
# Value of this node is an mapping (ex: {foo: bar})
52+
value = loader.construct_mapping(node)
53+
54+
return {cfntag: value}
55+
56+
57+
def yaml_dump(dict_to_dump):
58+
"""
59+
Dumps the dictionary as a YAML document
60+
:param dict_to_dump:
61+
:return:
62+
"""
63+
return yaml.safe_dump(dict_to_dump, default_flow_style=False)
64+
65+
66+
def yaml_parse(yamlstr):
67+
"""Parse a yaml string"""
68+
try:
69+
# PyYAML doesn't support json as well as it should, so if the input
70+
# is actually just json it is better to parse it with the standard
71+
# json parser.
72+
return json.loads(yamlstr)
73+
except ValueError:
74+
yaml.SafeLoader.add_multi_constructor(
75+
"!", intrinsics_multi_constructor)
76+
return yaml.safe_load(yamlstr)

stacker/blueprints/raw.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
import hashlib
44
import json
55

6-
import yaml
7-
6+
from ..util import parse_cloudformation_template
87
from ..exceptions import MissingVariable, UnresolvedVariable
98

109

@@ -101,7 +100,7 @@ def to_dict(self):
101100
dict: the loaded template as a python dictionary
102101
103102
"""
104-
return yaml.load(self.rendered)
103+
return parse_cloudformation_template(self.rendered)
105104

106105
def render_template(self):
107106
"""Load template and generate its md5 hash."""

stacker/tests/fixtures/cfn_template.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ Parameters:
77
Default: default
88
Type: CommaDelimitedList
99
Resources:
10+
Bucket:
11+
Type: AWS::S3::Bucket
12+
Properties:
13+
BucketName:
14+
!Join
15+
- "-"
16+
- - !Ref "AWS::StackName"
17+
- !Ref "AWS::Region"
1018
Dummy:
1119
Type: AWS::CloudFormation::WaitConditionHandle
1220
Outputs:

stacker/tests/test_util.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
get_client_region,
2020
get_s3_endpoint,
2121
s3_bucket_location_constraint,
22+
parse_cloudformation_template,
2223
Extractor,
2324
TarExtractor,
2425
TarGzipExtractor,
@@ -144,6 +145,45 @@ def test_s3_bucket_location_constraint(self):
144145
result
145146
)
146147

148+
def test_parse_cloudformation_template(self):
149+
template = """AWSTemplateFormatVersion: "2010-09-09"
150+
Parameters:
151+
Param1:
152+
Type: String
153+
Resources:
154+
Bucket:
155+
Type: AWS::S3::Bucket
156+
Properties:
157+
BucketName:
158+
!Join
159+
- "-"
160+
- - !Ref "AWS::StackName"
161+
- !Ref "AWS::Region"
162+
Outputs:
163+
DummyId:
164+
Value: dummy-1234"""
165+
parsed_template = {
166+
'AWSTemplateFormatVersion': '2010-09-09',
167+
'Outputs': {'DummyId': {'Value': 'dummy-1234'}},
168+
'Parameters': {'Param1': {'Type': 'String'}},
169+
'Resources': {
170+
'Bucket': {'Type': 'AWS::S3::Bucket',
171+
'Properties': {
172+
'BucketName': {
173+
u'Fn::Join': [
174+
'-',
175+
[{u'Ref': u'AWS::StackName'},
176+
{u'Ref': u'AWS::Region'}]
177+
]
178+
}
179+
}}
180+
}
181+
}
182+
self.assertEqual(
183+
parse_cloudformation_template(template),
184+
parsed_template
185+
)
186+
147187
def test_extractors(self):
148188
self.assertEqual(Extractor('test.zip').archive, 'test.zip')
149189
self.assertEqual(TarExtractor().extension(), '.tar')

stacker/util.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from yaml.constructor import ConstructorError
2222
from yaml.nodes import MappingNode
2323

24+
from .awscli_yamlhelper import yaml_parse
2425
from stacker.session_cache import get_session
2526

2627
logger = logging.getLogger(__name__)
@@ -496,6 +497,17 @@ def ensure_s3_bucket(s3_client, bucket_name, bucket_region):
496497
raise
497498

498499

500+
def parse_cloudformation_template(template):
501+
"""Parse CFN template string.
502+
503+
Leverages the vendored aws-cli yamlhelper to handle JSON or YAML templates.
504+
505+
Args:
506+
template (str): The template body.
507+
"""
508+
return yaml_parse(template)
509+
510+
499511
class Extractor(object):
500512
"""Base class for extractors."""
501513

0 commit comments

Comments
 (0)