diff --git a/doc/source/template_guide/hot_spec.rst b/doc/source/template_guide/hot_spec.rst index 8a68938575..2507201e6e 100644 --- a/doc/source/template_guide/hot_spec.rst +++ b/doc/source/template_guide/hot_spec.rst @@ -296,8 +296,9 @@ for the ``heat_template_version`` key: up until the Pike release. This version adds the ``make_url`` function for assembling URLs, the ``list_concat`` function for combining multiple lists, the ``list_concat_unique`` function for combining multiple - lists without repeating items, and the``string_replace_vstrict`` which - raises errors for missing and empty params. The complete list of + lists without repeating items, the``string_replace_vstrict`` which + raises errors for missing and empty params, and the ``contains`` which + checks whether specific value is in a sequence. The complete list of supported functions is:: digest @@ -310,6 +311,7 @@ for the ``heat_template_version`` key: make_url list_concat list_concat_unique + contains map_merge map_replace repeat @@ -321,7 +323,7 @@ for the ``heat_template_version`` key: yaql if - We support 'yaql' as condition function in this version. + We support 'yaql' and 'contains' as condition functions in this version. The complete list of supported condition functions is:: equals @@ -330,6 +332,7 @@ for the ``heat_template_version`` key: and or yaql + contains .. _hot_spec_parameter_groups: @@ -1958,3 +1961,25 @@ For example list_concat_unique: [['v1', 'v2'], ['v2', 'v43']] Will resolve to the list ``['v1', 'v2', 'v3']``. + +contains +-------- + +The ``contains`` function checks whether the specific value is +in a sequence. + +The syntax of the ``contains`` function is + +.. code-block:: yaml + + contains: [, ] + +This function returns true if value is in sequence or false if it isn't. + +For example + +.. code-block:: yaml + + contains: ['v1', ['v1', 'v2', 'v3']] + +Will resolve to boolean true. diff --git a/heat/engine/hot/functions.py b/heat/engine/hot/functions.py index bf36b8dd59..37428e6fea 100644 --- a/heat/engine/hot/functions.py +++ b/heat/engine/hot/functions.py @@ -1527,3 +1527,43 @@ class ListConcatUnique(ListConcat): """ _unique = True + + +class Contains(function.Function): + """A function for checking whether specific value is in sequence. + + Takes the form:: + + contains: + - + - + + The value can be any type that you want to check. Returns true + if the specific value is in the sequence, otherwise returns false. + """ + + def __init__(self, stack, fn_name, args): + super(Contains, self).__init__(stack, fn_name, args) + example = '"%s" : [ "value1", [ "value1", "value2"]]' % self.fn_name + fmt_data = {'fn_name': self.fn_name, + 'example': example} + + if not self.args or not isinstance(self.args, list): + raise TypeError(_('Incorrect arguments to "%(fn_name)s" ' + 'should be: %(example)s') % fmt_data) + try: + self.value, self.sequence = self.args + except ValueError: + msg = _('Arguments to "%s" must be of the form: ' + '[value1, [value1, value2]]') + raise ValueError(msg % self.fn_name) + + def result(self): + resolved_value = function.resolve(self.value) + resolved_sequence = function.resolve(self.sequence) + + if not isinstance(resolved_sequence, collections.Sequence): + raise TypeError(_('Second argument to "%s" should be ' + 'a sequence.') % self.fn_name) + + return resolved_value in resolved_sequence diff --git a/heat/engine/hot/template.py b/heat/engine/hot/template.py index 4aa2aa2d64..f8b3b88229 100644 --- a/heat/engine/hot/template.py +++ b/heat/engine/hot/template.py @@ -593,6 +593,7 @@ class HOTemplate20170901(HOTemplate20170224): 'list_concat': hot_funcs.ListConcat, 'str_replace_vstrict': hot_funcs.ReplaceJsonVeryStrict, 'list_concat_unique': hot_funcs.ListConcatUnique, + 'contains': hot_funcs.Contains, # functions removed from 2015-10-15 'Fn::Select': hot_funcs.Removed, @@ -616,5 +617,6 @@ class HOTemplate20170901(HOTemplate20170224): 'or': hot_funcs.Or, # functions added in 2017-09-01 - 'yaql': hot_funcs.Yaql + 'yaql': hot_funcs.Yaql, + 'contains': hot_funcs.Contains } diff --git a/heat/tests/test_hot.py b/heat/tests/test_hot.py index a94a24fa0e..8d4df604f3 100644 --- a/heat/tests/test_hot.py +++ b/heat/tests/test_hot.py @@ -2201,6 +2201,42 @@ conditions: snippet = {'list_concat': ['v1', 'v2']} self._test_list_concat_invalid(snippet) + def test_contains_with_list(self): + snippet = {'contains': ['v1', ['v1', 'v2']]} + tmpl = template.Template(hot_pike_tpl_empty) + resolved = self.resolve(snippet, tmpl) + self.assertTrue(resolved) + + def test_contains_with_string(self): + snippet = {'contains': ['a', 'abc']} + tmpl = template.Template(hot_pike_tpl_empty) + resolved = self.resolve(snippet, tmpl) + self.assertTrue(resolved) + + def test_contains_with_invalid_args_type(self): + snippet = {'contains': {'key': 'value'}} + tmpl = template.Template(hot_pike_tpl_empty) + exc = self.assertRaises(exception.StackValidationFailed, + self.resolve, snippet, tmpl) + msg = 'Incorrect arguments to ' + self.assertIn(msg, six.text_type(exc)) + + def test_contains_with_invalid_args_number(self): + snippet = {'contains': ['v1', ['v1', 'v2'], 'redundant']} + tmpl = template.Template(hot_pike_tpl_empty) + exc = self.assertRaises(exception.StackValidationFailed, + self.resolve, snippet, tmpl) + msg = 'must be of the form: [value1, [value1, value2]]' + self.assertIn(msg, six.text_type(exc)) + + def test_contains_with_invalid_sequence(self): + snippet = {'contains': ['v1', {'key': 'value'}]} + tmpl = template.Template(hot_pike_tpl_empty) + exc = self.assertRaises(TypeError, + self.resolve, snippet, tmpl) + msg = 'should be a sequence' + self.assertIn(msg, six.text_type(exc)) + class HotStackTest(common.HeatTestCase): """Test stack function when stack was created from HOT template."""