diff --git a/doc/source/template_guide/hot_spec.rst b/doc/source/template_guide/hot_spec.rst index 73f00b75c..6696b79a6 100644 --- a/doc/source/template_guide/hot_spec.rst +++ b/doc/source/template_guide/hot_spec.rst @@ -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: + - + - + - ... + +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 {}. diff --git a/heat/engine/hot/functions.py b/heat/engine/hot/functions.py index c54534f20..e740e2ecc 100644 --- a/heat/engine/hot/functions.py +++ b/heat/engine/hot/functions.py @@ -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. diff --git a/heat/engine/hot/template.py b/heat/engine/hot/template.py index 13292215f..34a4fae79 100644 --- a/heat/engine/hot/template.py +++ b/heat/engine/hot/template.py @@ -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, + } diff --git a/heat/tests/test_hot.py b/heat/tests/test_hot.py index d9c80bbe9..5ae92f0bf 100644 --- a/heat/tests/test_hot.py +++ b/heat/tests/test_hot.py @@ -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(''' diff --git a/heat/tests/test_template.py b/heat/tests/test_template.py index 29477faa5..0e8b6064c 100644 --- a/heat/tests/test_template.py +++ b/heat/tests/test_template.py @@ -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' diff --git a/setup.cfg b/setup.cfg index a87261640..3f41ee95b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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