Allow map/list items for str_replace

Currently any attempt to substitute placeholders using data from a
json/map or list parameter/attribute results in an error, which is
inconvenient if you wish to provide such data to an instance, e.g
so it can be used in a script via jq/python or whatever.

So, tolerate non-string parameter values, and attempt to serialize
them as json, then substitute the resulting json strings.

Change-Id: I362cc76ac3f68d4649a62459455c0dcae2dcd25d
Closes-Bug: #1489028
This commit is contained in:
Steven Hardy 2015-09-09 14:34:28 +01:00
parent b8fb5c61e7
commit 66f4178d28
4 changed files with 77 additions and 2 deletions

View File

@ -1099,7 +1099,11 @@ template
params
Provides parameter mappings in the form of dictionary. Each key refers to a
placeholder used in the ``template`` attribute.
placeholder used in the ``template`` attribute. From HOT version
``2015-10-15`` you may optionally pass non-string parameter values
(e.g json/map/list parameters or attributes) and they will be serialized
as json before replacing, prior heat/HOT versions require string values.
The following example shows a simple use of the ``str_replace`` function in the
outputs section of a template to build a URL for logging into a deployed

View File

@ -262,6 +262,57 @@ class Replace(cfn_funcs.Replace):
return mapping, string
class ReplaceJson(Replace):
'''
A function for performing string substitutions.
Behaves the same as Replace, but tolerates non-string parameter
values, e.g map/list - these are serialized as json before doing
the string substitution.
'''
def result(self):
template = function.resolve(self._string)
mapping = function.resolve(self._mapping)
if not isinstance(template, six.string_types):
raise TypeError(_('"%s" template must be a string') % self.fn_name)
if not isinstance(mapping, collections.Mapping):
raise TypeError(_('"%s" params must be a map') % self.fn_name)
def replace(string, change):
placeholder, value = change
if not isinstance(placeholder, six.string_types):
raise TypeError(_('"%s" param placeholders must be strings') %
self.fn_name)
if value is None:
value = ''
if not isinstance(value,
(six.string_types, six.integer_types,
float, bool)):
if isinstance(value,
(collections.Mapping, collections.Sequence)):
try:
value = jsonutils.dumps(value, default=None)
except TypeError:
raise TypeError(_('"%(name)s" params must be strings, '
'numbers, list or map. '
'Failed to json serialize %(value)s'
) % {'name': self.fn_name,
'value': value})
else:
raise TypeError(_('"%s" params must be strings, numbers, '
'list or map. ') % self.fn_name)
return string.replace(placeholder, six.text_type(value))
return six.moves.reduce(replace, six.iteritems(mapping), template)
class GetFile(function.Function):
"""
A function for including a file inline.

View File

@ -335,7 +335,7 @@ class HOTemplate20151015(HOTemplate20150430):
'list_join': hot_funcs.JoinMultiple,
'repeat': hot_funcs.Repeat,
'resource_facade': hot_funcs.ResourceFacade,
'str_replace': hot_funcs.Replace,
'str_replace': hot_funcs.ReplaceJson,
# functions added since 20150430
'str_split': hot_funcs.StrSplit,

View File

@ -490,6 +490,26 @@ class HOTemplateTest(common.HeatTestCase):
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl))
def test_str_replace_map_param(self):
"""Test str_replace function with non-string params."""
snippet = {'str_replace': {'template': 'jsonvar1',
'params': {'jsonvar1': {'foo': 123}}}}
snippet_resolved = '{"foo": 123}'
tmpl = template.Template(hot_liberty_tpl_empty)
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl))
def test_str_replace_list_param(self):
"""Test str_replace function with non-string params."""
snippet = {'str_replace': {'template': 'listvar1',
'params': {'listvar1': ['foo', 123]}}}
snippet_resolved = '["foo", 123]'
tmpl = template.Template(hot_liberty_tpl_empty)
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl))
def test_str_replace_number(self):
"""Test str_replace function with numbers."""