Add the HOT fuction str_replace_vstrict

The already existing str_replace_strict function raises an error if a
param is not present in the template. str_replace_vstrict, newly added
in this patch, is identical but also raises an error if any of the
params have an empty value.

Change-Id: I5407135cc0435cfbad2d18964fe2119c999f67a3
This commit is contained in:
Crag Wolfe 2016-08-23 19:44:43 -04:00
parent 2111551d50
commit 1f8d70346a
4 changed files with 104 additions and 14 deletions

View File

@ -294,8 +294,9 @@ for the ``heat_template_version`` key:
The key with value ``2017-09-01`` or ``pike`` indicates that the YAML The key with value ``2017-09-01`` or ``pike`` 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 Pike release. This version adds the ``make_url`` function for up until the Pike release. This version adds the ``make_url`` function for
assembling URLs and the ``list_concat`` function for combining multiple assembling URLs, the ``list_concat`` function for combining multiple
lists. The complete list of supported functions is:: lists, and ``string_replace_vstrict`` which raises errors for
missing and empty params. The complete list of supported functions is::
digest digest
filter filter
@ -312,6 +313,7 @@ for the ``heat_template_version`` key:
resource_facade resource_facade
str_replace str_replace
str_replace_strict str_replace_strict
str_replace_vstrict
str_split str_split
yaql yaql
if if
@ -1494,6 +1496,15 @@ in the template. This may help catch typo's or other issues sooner
rather than later when processing a template. rather than later when processing a template.
str_replace_vstrict
------------------
``str_replace_vstrict`` behaves identically to the
``str_replace_strict`` function, only an error is raised if any of the
params are empty. This may help catch issues (i.e., prevent
resources from being created with bogus values) sooner rather than later if
it is known that all the params should be non-empty.
str_split str_split
--------- ---------
The ``str_split`` function allows for splitting a string into a list by The ``str_split`` function allows for splitting a string into a list by

View File

@ -359,6 +359,7 @@ class Replace(function.Function):
""" """
_strict = False _strict = False
_allow_empty_value = True
def __init__(self, stack, fn_name, args): def __init__(self, stack, fn_name, args):
super(Replace, self).__init__(stack, fn_name, args) super(Replace, self).__init__(stack, fn_name, args)
@ -388,15 +389,16 @@ class Replace(function.Function):
else: else:
return mapping, string return mapping, string
def _validate_replacement(self, value): def _validate_replacement(self, value, param):
if value is None: if value is None:
return '' return ''
if not isinstance(value, if not isinstance(value,
(six.string_types, six.integer_types, (six.string_types, six.integer_types,
float, bool)): float, bool)):
raise TypeError(_('"%s" params must be strings or numbers') % raise TypeError(_('"%(name)s" params must be strings or numbers, '
self.fn_name) 'param %(param)s is not valid') %
{'name': self.fn_name, 'param': param})
return six.text_type(value) return six.text_type(value)
@ -423,7 +425,8 @@ class Replace(function.Function):
self.fn_name) self.fn_name)
remaining_keys = keys[1:] remaining_keys = keys[1:]
value = self._validate_replacement(mapping[placeholder]) value = self._validate_replacement(mapping[placeholder],
placeholder)
def string_split(s): def string_split(s):
ss = s.split(placeholder) ss = s.split(placeholder)
@ -467,13 +470,25 @@ class ReplaceJson(Replace):
being substituted in. being substituted in.
""" """
def _validate_replacement(self, value): def _validate_replacement(self, value, param):
def _raise_empty_param_value_error():
raise ValueError(
_('%(name)s has an undefined or empty value for param '
'%(param)s, must be a defined non-empty value') %
{'name': self.fn_name, 'param': param})
if value is None: if value is None:
return '' if self._allow_empty_value:
return ''
else:
_raise_empty_param_value_error()
if not isinstance(value, (six.string_types, six.integer_types, if not isinstance(value, (six.string_types, six.integer_types,
float, bool)): float, bool)):
if isinstance(value, (collections.Mapping, collections.Sequence)): if isinstance(value, (collections.Mapping, collections.Sequence)):
if not self._allow_empty_value and len(value) == 0:
_raise_empty_param_value_error()
try: try:
return jsonutils.dumps(value, default=None, sort_keys=True) return jsonutils.dumps(value, default=None, sort_keys=True)
except TypeError: except TypeError:
@ -486,7 +501,10 @@ class ReplaceJson(Replace):
raise TypeError(_('"%s" params must be strings, numbers, ' raise TypeError(_('"%s" params must be strings, numbers, '
'list or map.') % self.fn_name) 'list or map.') % self.fn_name)
return six.text_type(value) ret_value = six.text_type(value)
if not self._allow_empty_value and not ret_value:
_raise_empty_param_value_error()
return ret_value
class ReplaceJsonStrict(ReplaceJson): class ReplaceJsonStrict(ReplaceJson):
@ -496,10 +514,19 @@ class ReplaceJsonStrict(ReplaceJson):
a ValueError is raised if any of the params are not present in a ValueError is raised if any of the params are not present in
the template. the template.
""" """
_strict = True _strict = True
class ReplaceJsonVeryStrict(ReplaceJsonStrict):
"""A function for performing string substituions.
str_replace_vstrict is identical to the str_replace_strict
function, only a ValueError is raised if any of the params are
None or empty.
"""
_allow_empty_value = False
class GetFile(function.Function): class GetFile(function.Function):
"""A function for including a file inline. """A function for including a file inline.

View File

@ -591,6 +591,7 @@ class HOTemplate20170901(HOTemplate20170224):
# functions added in 2017-09-01 # functions added in 2017-09-01
'make_url': hot_funcs.MakeURL, 'make_url': hot_funcs.MakeURL,
'list_concat': hot_funcs.ListConcat, 'list_concat': hot_funcs.ListConcat,
'str_replace_vstrict': hot_funcs.ReplaceJsonVeryStrict,
# functions removed from 2015-10-15 # functions removed from 2015-10-15
'Fn::Select': hot_funcs.Removed, 'Fn::Select': hot_funcs.Removed,

View File

@ -586,7 +586,18 @@ class HOTemplateTest(common.HeatTestCase):
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl)) self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl))
def test_str_replace_map_param(self): def test_str_replace_map_param(self):
"""Test str_replace function with non-string params.""" """Test old str_replace function with non-string map param."""
snippet = {'str_replace': {'template': 'jsonvar1',
'params': {'jsonvar1': {'foo': 123}}}}
tmpl = template.Template(hot_tpl_empty)
ex = self.assertRaises(TypeError, self.resolve, snippet, tmpl)
self.assertIn('"str_replace" params must be strings or numbers, '
'param jsonvar1 is not valid', six.text_type(ex))
def test_liberty_str_replace_map_param(self):
"""Test str_replace function with non-string map param."""
snippet = {'str_replace': {'template': 'jsonvar1', snippet = {'str_replace': {'template': 'jsonvar1',
'params': {'jsonvar1': {'foo': 123}}}} 'params': {'jsonvar1': {'foo': 123}}}}
@ -596,7 +607,18 @@ class HOTemplateTest(common.HeatTestCase):
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl)) self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl))
def test_str_replace_list_param(self): def test_str_replace_list_param(self):
"""Test str_replace function with non-string params.""" """Test old str_replace function with non-string list param."""
snippet = {'str_replace': {'template': 'listvar1',
'params': {'listvar1': ['foo', 123]}}}
tmpl = template.Template(hot_tpl_empty)
ex = self.assertRaises(TypeError, self.resolve, snippet, tmpl)
self.assertIn('"str_replace" params must be strings or numbers, '
'param listvar1 is not valid', six.text_type(ex))
def test_liberty_str_replace_list_param(self):
"""Test str_replace function with non-string param."""
snippet = {'str_replace': {'template': 'listvar1', snippet = {'str_replace': {'template': 'listvar1',
'params': {'listvar1': ['foo', 123]}}} 'params': {'listvar1': ['foo', 123]}}}
@ -717,7 +739,7 @@ class HOTemplateTest(common.HeatTestCase):
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl)) self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl))
def test_str_replace_strict_missing_param(self): def test_str_replace_strict_missing_param(self):
"""Test str_replace_strict function missing param (s)raises error.""" """Test str_replace_strict function missing param(s) raises error."""
snippet = {'str_replace_strict': snippet = {'str_replace_strict':
{'template': 'Template var1 string var2', {'template': 'Template var1 string var2',
@ -738,16 +760,45 @@ class HOTemplateTest(common.HeatTestCase):
self.assertEqual('The following params were not found in the ' self.assertEqual('The following params were not found in the '
'template: var0', six.text_type(ex)) 'template: var0', six.text_type(ex))
snippet = {'str_replace_strict': # str_replace_vstrict has same behaviour
snippet = {'str_replace_vstrict':
{'template': 'Template var1 string var2', {'template': 'Template var1 string var2',
'params': {'var1': 'foo', 'var2': 'bar', 'params': {'var1': 'foo', 'var2': 'bar',
'var0': 'zed', 'var': 'z', 'var0': 'zed', 'var': 'z',
'longvarname': 'q'}}} 'longvarname': 'q'}}}
tmpl = template.Template(hot_pike_tpl_empty)
ex = self.assertRaises(ValueError, self.resolve, snippet, tmpl) ex = self.assertRaises(ValueError, self.resolve, snippet, tmpl)
self.assertEqual('The following params were not found in the ' self.assertEqual('The following params were not found in the '
'template: longvarname,var0,var', six.text_type(ex)) 'template: longvarname,var0,var', six.text_type(ex))
def test_str_replace_strict_empty_param_ok(self):
"""Test str_replace_strict function with empty params."""
snippet = {'str_replace_strict':
{'template': 'Template var1 string var2',
'params': {'var1': 'foo', 'var2': ''}}}
tmpl = template.Template(hot_ocata_tpl_empty)
self.assertEqual('Template foo string ', self.resolve(snippet, tmpl))
def test_str_replace_vstrict_empty_param_not_ok(self):
"""Test str_replace_vstrict function with empty params.
Raise ValueError when any of the params are None or empty.
"""
snippet = {'str_replace_vstrict':
{'template': 'Template var1 string var2',
'params': {'var1': 'foo', 'var2': ''}}}
tmpl = template.Template(hot_pike_tpl_empty)
for val in (None, '', {}, []):
snippet['str_replace_vstrict']['params']['var2'] = val
ex = self.assertRaises(ValueError, self.resolve, snippet, tmpl)
self.assertIn('str_replace_vstrict has an undefined or empty '
'value for param var2', six.text_type(ex))
def test_str_replace_invalid_param_keys(self): def test_str_replace_invalid_param_keys(self):
"""Test str_replace function parameter keys. """Test str_replace function parameter keys.