Skip to content

Commit cedcf33

Browse files
authored
Merge pull request #7787 from hssyoo/change-cfn-yml-spec
Use YAML 1.1 spec for CFN
2 parents e087ce6 + df32008 commit cedcf33

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)