Skip to content

Commit df32008

Browse files
committed
Use YAML 1.1 spec for CFN
This commit also adds logic to quote nonoctal integers such as 08888888 so they're not treated as integers when deployed to a stack.
1 parent 192185e commit df32008

File tree

5 files changed

+54
-22
lines changed

5 files changed

+54
-22
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "bugfix",
3+
"category": "cloudformation",
4+
"description": "Fixes `#3991 <https://github.com/aws/aws-cli/issues/3991>`__. Use YAML 1.1 spec in alignment with CloudFormation YAML support."
5+
}

awscli/customizations/cloudformation/yamlhelper.py

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -63,21 +63,17 @@ def _dict_representer(dumper, data):
6363
return dumper.represent_dict(data.items())
6464

6565

66-
def _add_yaml_1_1_boolean_resolvers(resolver_cls):
67-
# CloudFormation treats unquoted values that are YAML 1.1 native
68-
# booleans as booleans, rather than strings. In YAML 1.2, the only
69-
# boolean values are "true" and "false" so values such as "yes" and "no"
70-
# when loaded as strings are not quoted when dumped. This logic ensures
71-
# that we dump these values with quotes so that CloudFormation treats
72-
# these values as strings and not booleans.
73-
boolean_regex = re.compile(
74-
'^(?:yes|Yes|YES|no|No|NO'
75-
'|true|True|TRUE|false|False|FALSE'
76-
'|on|On|ON|off|Off|OFF)$'
77-
)
78-
boolean_first_chars = list(u'yYnNtTfFoO')
79-
resolver_cls.add_implicit_resolver(
80-
'tag:yaml.org,2002:bool', boolean_regex, boolean_first_chars)
66+
def _str_representer(dumper, data):
67+
# ruamel removes quotes from values that are unambiguously strings.
68+
# Values like 0888888 can only be a string because integers can't
69+
# have leading 0s and octals can't have the digits 8 and 9.
70+
# However, CloudFormation treats these nonoctal values as integers
71+
# and removes the leading 0s. This logic ensures that nonoctal
72+
# values are quoted when dumped.
73+
style = None
74+
if re.match('^0[0-9]*[89][0-9]*$', data):
75+
style = "'"
76+
return dumper.represent_scalar('tag:yaml.org,2002:str', data, style=style)
8177

8278

8379
def yaml_dump(dict_to_dump):
@@ -88,10 +84,11 @@ def yaml_dump(dict_to_dump):
8884
"""
8985

9086
yaml = ruamel.yaml.YAML(typ="safe", pure=True)
87+
yaml.version = (1, 1)
9188
yaml.default_flow_style = False
9289
yaml.Representer = FlattenAliasRepresenter
93-
_add_yaml_1_1_boolean_resolvers(yaml.Resolver)
9490
yaml.Representer.add_representer(OrderedDict, _dict_representer)
91+
yaml.Representer.add_representer(str, _str_representer)
9592

9693
return dump_yaml_to_str(yaml, dict_to_dump)
9794

@@ -111,12 +108,12 @@ def yaml_parse(yamlstr):
111108
return json.loads(yamlstr, object_pairs_hook=OrderedDict)
112109
except ValueError:
113110
yaml = ruamel.yaml.YAML(typ="safe", pure=True)
111+
yaml.version = (1, 1)
114112
yaml.Constructor.add_constructor(
115113
ruamel.yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
116114
_dict_constructor)
117115
yaml.Constructor.add_multi_constructor(
118116
"!", intrinsics_multi_constructor)
119-
_add_yaml_1_1_boolean_resolvers(yaml.Resolver)
120117

121118
return yaml.load(yamlstr)
122119

awscli/testutils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from pprint import pformat
3535
from subprocess import Popen, PIPE
3636
import unittest
37+
import re
3738

3839
from awscli.compat import StringIO
3940

@@ -58,6 +59,7 @@
5859
import awscli.clidriver
5960
from awscli.plugin import load_plugins
6061
from awscli.clidriver import CLIDriver
62+
from awscli.customizations.cloudformation.yamlhelper import yaml_dump
6163

6264

6365
_LOADER = botocore.loaders.Loader()
@@ -1012,3 +1014,8 @@ def wait(self, check, *args, **kwargs):
10121014
def _fail_message(self, attempts, successes):
10131015
format_args = (attempts, successes)
10141016
return 'Failed after %s attempts, only had %s successes' % format_args
1017+
1018+
1019+
def yaml_dump_without_header(dict_to_dump):
1020+
dumped_yaml = yaml_dump(dict_to_dump)
1021+
return re.sub('%YAML 1.1\n---(\n| )', '', dumped_yaml)

tests/functional/cloudformation/test_package.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from awscli.customizations.cloudformation.artifact_exporter import make_zip
2424
from awscli.customizations.cloudformation.yamlhelper import yaml_dump
2525
from awscli.customizations.cloudformation.artifact_exporter import Template
26-
from awscli.testutils import skip_if_windows
26+
from awscli.testutils import yaml_dump_without_header, skip_if_windows
2727

2828

2929
class TestPackageZipFiles(TestCase):
@@ -87,7 +87,7 @@ def _generate_template_cases():
8787
def test_known_templates(input_template, output_template):
8888
template = Template(input_template, os.getcwd(), None)
8989
exported = template.export()
90-
result = yaml_dump(exported)
90+
result = yaml_dump_without_header(exported)
9191
expected = open(output_template, 'r').read()
9292

9393
assert result == expected, (

tests/unit/customizations/cloudformation/test_yamlhelper.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from awscli.customizations.cloudformation.deployer import Deployer
2121
from awscli.customizations.cloudformation.yamlhelper import yaml_parse, yaml_dump
2222
from tests.unit.customizations.cloudformation import BaseYAMLTest
23+
from awscli.testutils import yaml_dump_without_header
2324

2425

2526
class TestYaml(BaseYAMLTest):
@@ -145,7 +146,7 @@ def test_parse_yaml_preserve_elements_order(self):
145146
])
146147
self.assertEqual(expected_dict, output_dict)
147148

148-
output_template = yaml_dump(output_dict)
149+
output_template = yaml_dump_without_header(output_dict)
149150
self.assertEqual(input_template, output_template)
150151

151152
def test_yaml_merge_tag(self):
@@ -182,7 +183,7 @@ def test_unroll_yaml_anchors(self):
182183
' Foo: bar\n'
183184
' Spam: eggs\n'
184185
)
185-
actual = yaml_dump(template)
186+
actual = yaml_dump_without_header(template)
186187
self.assertEqual(actual, expected)
187188

188189
def test_yaml_dump_quotes_boolean_strings(self):
@@ -193,4 +194,26 @@ def test_yaml_dump_quotes_boolean_strings(self):
193194
]
194195
for bool_as_string in bools_as_strings:
195196
self.assertEqual(
196-
yaml_dump(bool_as_string), "'%s'\n" % bool_as_string)
197+
yaml_dump_without_header(bool_as_string),
198+
"'%s'\n" % bool_as_string)
199+
200+
def test_yaml_dump_quotes_octal_strings(self):
201+
octals_as_strings = [
202+
'01111111', '02222222', '03333333',
203+
'04444444', '05555555', '06666666',
204+
'07777777', '08888888', '09999999'
205+
]
206+
for octal_as_string in octals_as_strings:
207+
self.assertEqual(
208+
yaml_dump_without_header(octal_as_string),
209+
"'%s'\n" % octal_as_string)
210+
211+
def test_yaml_dump_include_1_1_header(self):
212+
template = {"Resources": {"Foo": "Bar"}}
213+
expected = (
214+
'%YAML 1.1\n---\n'
215+
'Resources:\n'
216+
' Foo: Bar\n'
217+
)
218+
actual = yaml_dump(template)
219+
self.assertEqual(actual, expected)

0 commit comments

Comments
 (0)