From 71a9c3d690ac0bc281b39eee6dbe146045a6b21f Mon Sep 17 00:00:00 2001 From: Zane Bitter Date: Thu, 5 Mar 2020 22:30:58 -0500 Subject: [PATCH] Modify 'if' Macro to allow optional properties Change-Id: I931d88e79fc077d12fc9bd39009061ffe87f1262 Story: 2007388 Task: 38973 --- doc/source/template_guide/hot_spec.rst | 31 ++++++++++++- heat/engine/hot/functions.py | 39 +++++++++++++++- heat/engine/hot/template.py | 3 +- heat/tests/test_hot.py | 45 ++++++++++++++++--- ...-optional-properties-40647f036903731b.yaml | 8 ++++ 5 files changed, 117 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/if-macro-optional-properties-40647f036903731b.yaml diff --git a/doc/source/template_guide/hot_spec.rst b/doc/source/template_guide/hot_spec.rst index 694b22e465..b9e3952558 100644 --- a/doc/source/template_guide/hot_spec.rst +++ b/doc/source/template_guide/hot_spec.rst @@ -414,7 +414,15 @@ The complete list of supported condition functions is:: -------------------- The key with value ``2021-04-16`` or ``wallaby`` indicates that the YAML document is a HOT template and it may contain features added and/or removed -up until the Wallaby release. The complete list of supported functions is:: +up until the Wallaby release. + +This version adds a 2-argument variant of the ``if`` function. When the +condition is false and no third argument is supplied, the entire enclosing item +(which may be e.g. a list item, a key-value pair in a dict, or a property +value) will be elided. This allows for e.g. conditional definition of +properties while keeping the default value when the condition is false. + +The complete list of supported functions is:: digest filter @@ -1875,6 +1883,27 @@ template except for ``if`` conditions. You can use the ``if`` condition in the property values in the ``resources`` section and ``outputs`` sections of a template. +Beginning with the ``wallaby`` template version, the third argument is +optional. If only two arguments are passed, the entire enclosing item is +removed when the condition is false. + +For example: + +.. code-block:: yaml + + conditions: + override_name: {not: {equals: [{get_param: server_name}, ""]}} + + resources: + test_server: + type: OS::Nova::Server + properties: + name: {if: [override_name, {get_param: server_name}]} + +In this example, the default name for the server (which is generated by Heat +when the property value is not specified) would be used when the +``server_name`` parameter value is an empty string. + not --- The ``not`` function acts as a NOT operator. diff --git a/heat/engine/hot/functions.py b/heat/engine/hot/functions.py index cf4eaa0f52..a700ff5fe4 100644 --- a/heat/engine/hot/functions.py +++ b/heat/engine/hot/functions.py @@ -1275,13 +1275,16 @@ class If(function.Macro): evaluates to false. """ + def _read_args(self): + return self.args + def parse_args(self, parse_func): try: if (not self.args or not isinstance(self.args, collections.Sequence) or isinstance(self.args, str)): raise ValueError() - condition, value_if_true, value_if_false = self.args + condition, value_if_true, value_if_false = self._read_args() except ValueError: msg = _('Arguments to "%s" must be of the form: ' '[condition_name, value_if_true, value_if_false]') @@ -1299,6 +1302,40 @@ class If(function.Macro): return self.template.conditions(self.stack).is_enabled(cond) +class IfNullable(If): + """A function to return corresponding value based on condition evaluation. + + Takes the form:: + + if: + - + - + - + + The value_if_true to be returned if the specified condition evaluates + to true, the value_if_false to be returned if the specified condition + evaluates to false. + + If the value_if_false is omitted and the condition is false, the enclosing + item (list item, dictionary key/value pair, property definition) will be + treated as if it were not mentioned in the template:: + + if: + - + - + """ + + def _read_args(self): + if not (2 <= len(self.args) <= 3): + raise ValueError() + + if len(self.args) == 2: + condition, value_if_true = self.args + return condition, value_if_true, Ellipsis + + return self.args + + class ConditionBoolean(function.Function): """Abstract parent class of boolean condition functions.""" diff --git a/heat/engine/hot/template.py b/heat/engine/hot/template.py index 976734416f..ee56a6421d 100644 --- a/heat/engine/hot/template.py +++ b/heat/engine/hot/template.py @@ -783,7 +783,8 @@ class HOTemplate20210416(HOTemplate20180831): # functions added in 2016-10-14 'yaql': hot_funcs.Yaql, 'map_replace': hot_funcs.MapReplace, - 'if': hot_funcs.If, + # Modified in 2021-04-16 + 'if': hot_funcs.IfNullable, # functions added in 2017-02-24 'filter': hot_funcs.Filter, diff --git a/heat/tests/test_hot.py b/heat/tests/test_hot.py index 03a7054d6f..19a66119be 100644 --- a/heat/tests/test_hot.py +++ b/heat/tests/test_hot.py @@ -71,6 +71,10 @@ hot_pike_tpl_empty = template_format.parse(''' heat_template_version: 2017-09-01 ''') +hot_wallaby_tpl_empty = template_format.parse(''' +heat_template_version: 2021-04-16 +''') + hot_tpl_empty_sections = template_format.parse(''' heat_template_version: 2013-05-23 parameters: @@ -1506,13 +1510,42 @@ resources: self.assertEqual('', self.stack['AResource'].properties['Foo']) def test_if_invalid_args(self): - snippet = {'if': ['create_prod', 'one_value']} + snippets = [ + {'if': ['create_prod', 'one_value']}, + {'if': ['create_prod', 'one_value', 'two_values', 'three_values']}, + ] tmpl = template.Template(hot_newton_tpl_empty) - exc = self.assertRaises(exception.StackValidationFailed, - self.resolve, snippet, tmpl) - self.assertIn('Arguments to "if" must be of the form: ' - '[condition_name, value_if_true, value_if_false]', - str(exc)) + for snippet in snippets: + exc = self.assertRaises(exception.StackValidationFailed, + self.resolve, snippet, tmpl) + self.assertIn('Arguments to "if" must be of the form: ' + '[condition_name, value_if_true, value_if_false]', + str(exc)) + + def test_if_nullable_invalid_args(self): + snippets = [ + {'if': ['create_prod']}, + {'if': ['create_prod', 'one_value', 'two_values', 'three_values']}, + ] + tmpl = template.Template(hot_wallaby_tpl_empty) + for snippet in snippets: + exc = self.assertRaises(exception.StackValidationFailed, + self.resolve, snippet, tmpl) + self.assertIn('Arguments to "if" must be of the form: ' + '[condition_name, value_if_true, value_if_false]', + str(exc)) + + def test_if_nullable(self): + snippet = { + 'single': {'if': [False, 'value_if_true']}, + 'nested_true': {'if': [True, {'if': [False, 'foo']}, 'bar']}, + 'nested_false': {'if': [False, 'baz', {'if': [False, 'quux']}]}, + 'control': {'if': [False, True, None]}, + } + + tmpl = template.Template(hot_wallaby_tpl_empty) + resolved = self.resolve(snippet, tmpl, None) + self.assertEqual({'control': None}, resolved) def test_if_condition_name_non_existing(self): snippet = {'if': ['cd_not_existing', 'value_true', 'value_false']} diff --git a/releasenotes/notes/if-macro-optional-properties-40647f036903731b.yaml b/releasenotes/notes/if-macro-optional-properties-40647f036903731b.yaml new file mode 100644 index 0000000000..971c417f1e --- /dev/null +++ b/releasenotes/notes/if-macro-optional-properties-40647f036903731b.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + The ``wallaby`` template version introduces a new 2-argument form of the + ``if`` function. This allows users to specify optional property values, so + that when the condition is false Heat treats it the same as if no value + were specified for the property at all. The behaviour of existing templates + is unchanged, even after updating the template version to ``wallaby``.