Implement new map_merge intrinsic function

Adds a new map_merge function for Mitaka which can be used
to merge maps. Values in latter maps override those in earlier
ones.

Also, creates a new HOT template version for Mitaka which includes
the new map_merge function.

Implements: blueprint map-merge-function
Change-Id: I2bdfc70d04a4fa14cefcf928ea5947fbe7529cf9
This commit is contained in:
Dan Prince 2015-10-17 22:58:35 -04:00
parent 9185cd7e2c
commit 7f8e92ee00
6 changed files with 153 additions and 1 deletions

View File

@ -179,6 +179,25 @@ For example, Heat currently supports the following values for the
str_replace
str_split
2016-04-08
The key with value ``2016-04-08`` indicates that the YAML document is a HOT
template and it may contain features added and/or removed up until the
Mitaka release. This version also adds the map_merge function which
can be used to merge the contents of maps. The complete list of supported
functions is::
digest
get_attr
get_file
get_param
get_resource
list_join
map_merge
repeat
resource_facade
str_replace
str_split
.. _hot_spec_parameter_groups:
Parameter groups section
@ -1209,3 +1228,28 @@ The result of which is:
Note: The index starts at zero, and any value outside the maximum (e.g the
length of the list minus one) will cause an error.
map_merge
---------
The ``map_merge`` function merges maps together. Values in the latter maps
override any values in earlier ones. Can be very useful when composing maps
that contain configuration data into a single consolidated map.
The syntax of the ``map_merge`` function is
.. code-block:: yaml
map_merge:
- <map 1>
- <map 2>
- ...
For example
.. code-block:: yaml
map_merge: [{'k1': 'v1', 'k2': 'v2'}, {'k1': 'v2'}]
This resolves to a map containing ``{'k1': 'v2', 'k2': 'v2'}``.
Maps containing no items resolve to {}.

View File

@ -429,6 +429,47 @@ class JoinMultiple(function.Function):
return delim.join(ensure_string(s) for s in strings)
class MapMerge(function.Function):
"""A function for merging maps.
Takes the form::
{ "map_merge" : [{'k1': 'v1', 'k2': 'v2'}, {'k1': 'v2'}] }
And resolves to::
{'k1': 'v2', 'k2': 'v2'}
"""
def __init__(self, stack, fn_name, args):
super(MapMerge, self).__init__(stack, fn_name, args)
example = (_('"%s" : [ { "key1": "val1" }, { "key2": "val2" } ]')
% fn_name)
self.fmt_data = {'fn_name': fn_name, 'example': example}
def result(self):
args = function.resolve(self.args)
if not isinstance(args, collections.Sequence):
raise TypeError(_('Incorrect arguments to "%(fn_name)s" '
'should be: %(example)s') % self.fmt_data)
def ensure_map(m):
if m is None:
return {}
elif isinstance(m, collections.Mapping):
return m
else:
msg = _('Incorrect arguments: Items to merge must be maps.')
raise TypeError(msg)
ret_map = {}
for m in args:
ret_map.update(ensure_map(m))
return ret_map
class ResourceFacade(cfn_funcs.ResourceFacade):
"""A function for retrieving data in a parent provider template.

View File

@ -363,3 +363,36 @@ class HOTemplate20151015(HOTemplate20150430):
'Fn::ResourceFacade': hot_funcs.Removed,
'Ref': hot_funcs.Removed,
}
class HOTemplate20160408(HOTemplate20151015):
functions = {
'digest': hot_funcs.Digest,
'get_attr': hot_funcs.GetAttAllAttributes,
'get_file': hot_funcs.GetFile,
'get_param': hot_funcs.GetParam,
'get_resource': cfn_funcs.ResourceRef,
'list_join': hot_funcs.JoinMultiple,
'repeat': hot_funcs.Repeat,
'resource_facade': hot_funcs.ResourceFacade,
'str_replace': hot_funcs.ReplaceJson,
# functions added since 20151015
'map_merge': hot_funcs.MapMerge,
# functions added since 20150430
'str_split': hot_funcs.StrSplit,
# functions removed from 20150430
'Fn::Select': hot_funcs.Removed,
# functions removed from 20130523
'Fn::GetAZs': hot_funcs.Removed,
'Fn::Join': hot_funcs.Removed,
'Fn::Split': hot_funcs.Removed,
'Fn::Replace': hot_funcs.Removed,
'Fn::Base64': hot_funcs.Removed,
'Fn::MemberListToMap': hot_funcs.Removed,
'Fn::ResourceFacade': hot_funcs.Removed,
'Ref': hot_funcs.Removed,
}

View File

@ -52,6 +52,10 @@ hot_liberty_tpl_empty = template_format.parse('''
heat_template_version: 2015-10-15
''')
hot_mitaka_tpl_empty = template_format.parse('''
heat_template_version: 2016-04-08
''')
hot_tpl_empty_sections = template_format.parse('''
heat_template_version: 2013-05-23
parameters:
@ -708,6 +712,35 @@ class HOTemplateTest(common.HeatTestCase):
exc = self.assertRaises(TypeError, self.resolve, snippet, tmpl)
self.assertIn('Incorrect arguments', six.text_type(exc))
def test_merge(self):
snippet = {'map_merge': [{'f1': 'b1', 'f2': 'b2'}, {'f1': 'b2'}]}
tmpl = template.Template(hot_mitaka_tpl_empty)
resolved = self.resolve(snippet, tmpl)
self.assertEqual('b2', resolved['f1'])
self.assertEqual('b2', resolved['f2'])
def test_merge_none(self):
snippet = {'map_merge': [{'f1': 'b1', 'f2': 'b2'}, None]}
tmpl = template.Template(hot_mitaka_tpl_empty)
resolved = self.resolve(snippet, tmpl)
self.assertEqual('b1', resolved['f1'])
self.assertEqual('b2', resolved['f2'])
def test_merge_invalid(self):
snippet = {'map_merge': [{'f1': 'b1', 'f2': 'b2'}, ['f1', 'b2']]}
tmpl = template.Template(hot_mitaka_tpl_empty)
exc = self.assertRaises(TypeError, self.resolve, snippet, tmpl)
self.assertIn('Incorrect arguments', six.text_type(exc))
def test_merge_containing_repeat(self):
snippet = {'map_merge': {'repeat': {'template': {'ROLE': 'ROLE'},
'for_each': {'ROLE': ['role1', 'role2']}}}}
tmpl = template.Template(hot_mitaka_tpl_empty)
resolved = self.resolve(snippet, tmpl)
self.assertEqual('role1', resolved['role1'])
self.assertEqual('role2', resolved['role2'])
def test_repeat(self):
"""Test repeat function."""
hot_tpl = template_format.parse('''

View File

@ -451,7 +451,7 @@ class TemplateTest(common.HeatTestCase):
init_ex = self.assertRaises(exception.InvalidTemplateVersion,
template.Template, invalid_hot_version_tmp)
valid_versions = ['2013-05-23', '2014-10-16',
'2015-04-30', '2015-10-15']
'2015-04-30', '2015-10-15', '2016-04-08']
ex_error_msg = ('The template version is invalid: '
'"heat_template_version: 2012-12-12". '
'"heat_template_version" should be one of: %s'

View File

@ -109,6 +109,7 @@ heat.templates =
heat_template_version.2014-10-16 = heat.engine.hot.template:HOTemplate20141016
heat_template_version.2015-04-30 = heat.engine.hot.template:HOTemplate20150430
heat_template_version.2015-10-15 = heat.engine.hot.template:HOTemplate20151015
heat_template_version.2016-04-08 = heat.engine.hot.template:HOTemplate20160408
HeatTemplateFormatVersion.2012-12-12 = heat.engine.cfn.template:HeatTemplate
AWSTemplateFormatVersion.2010-09-09 = heat.engine.cfn.template:CfnTemplate