Modify 'if' Macro to allow optional properties

Change-Id: I931d88e79fc077d12fc9bd39009061ffe87f1262
Story: 2007388
Task: 38973
This commit is contained in:
Zane Bitter 2020-03-05 22:30:58 -05:00
parent 674a62ae9b
commit 71a9c3d690
5 changed files with 117 additions and 9 deletions

View File

@ -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 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 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 digest
filter 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 in the property values in the ``resources`` section and ``outputs`` sections
of a template. 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 not
--- ---
The ``not`` function acts as a NOT operator. The ``not`` function acts as a NOT operator.

View File

@ -1275,13 +1275,16 @@ class If(function.Macro):
evaluates to false. evaluates to false.
""" """
def _read_args(self):
return self.args
def parse_args(self, parse_func): def parse_args(self, parse_func):
try: try:
if (not self.args or if (not self.args or
not isinstance(self.args, collections.Sequence) or not isinstance(self.args, collections.Sequence) or
isinstance(self.args, str)): isinstance(self.args, str)):
raise ValueError() raise ValueError()
condition, value_if_true, value_if_false = self.args condition, value_if_true, value_if_false = self._read_args()
except ValueError: except ValueError:
msg = _('Arguments to "%s" must be of the form: ' msg = _('Arguments to "%s" must be of the form: '
'[condition_name, value_if_true, value_if_false]') '[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) 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:
- <condition_name>
- <value_if_true>
- <value_if_false>
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:
- <condition_name>
- <value_if_true>
"""
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): class ConditionBoolean(function.Function):
"""Abstract parent class of boolean condition functions.""" """Abstract parent class of boolean condition functions."""

View File

@ -783,7 +783,8 @@ class HOTemplate20210416(HOTemplate20180831):
# functions added in 2016-10-14 # functions added in 2016-10-14
'yaql': hot_funcs.Yaql, 'yaql': hot_funcs.Yaql,
'map_replace': hot_funcs.MapReplace, 'map_replace': hot_funcs.MapReplace,
'if': hot_funcs.If, # Modified in 2021-04-16
'if': hot_funcs.IfNullable,
# functions added in 2017-02-24 # functions added in 2017-02-24
'filter': hot_funcs.Filter, 'filter': hot_funcs.Filter,

View File

@ -71,6 +71,10 @@ hot_pike_tpl_empty = template_format.parse('''
heat_template_version: 2017-09-01 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(''' hot_tpl_empty_sections = template_format.parse('''
heat_template_version: 2013-05-23 heat_template_version: 2013-05-23
parameters: parameters:
@ -1506,13 +1510,42 @@ resources:
self.assertEqual('', self.stack['AResource'].properties['Foo']) self.assertEqual('', self.stack['AResource'].properties['Foo'])
def test_if_invalid_args(self): 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) tmpl = template.Template(hot_newton_tpl_empty)
exc = self.assertRaises(exception.StackValidationFailed, for snippet in snippets:
self.resolve, snippet, tmpl) exc = self.assertRaises(exception.StackValidationFailed,
self.assertIn('Arguments to "if" must be of the form: ' self.resolve, snippet, tmpl)
'[condition_name, value_if_true, value_if_false]', self.assertIn('Arguments to "if" must be of the form: '
str(exc)) '[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): def test_if_condition_name_non_existing(self):
snippet = {'if': ['cd_not_existing', 'value_true', 'value_false']} snippet = {'if': ['cd_not_existing', 'value_true', 'value_false']}

View File

@ -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``.