Merge ""repeat" function for HOT templates"
This commit is contained in:
commit
f1fd6e9f06
@ -140,6 +140,23 @@ For example, Heat currently supports the following values for the
|
||||
str_replace
|
||||
Fn::Select
|
||||
|
||||
2015-04-30
|
||||
The key with value *2015-04-30* indicates that the YAML document is a HOT
|
||||
template and it may contain features added and/or removed up until the Kilo
|
||||
release. This version adds the *repeat* function. So the complete list of
|
||||
supported functions is:
|
||||
|
||||
::
|
||||
get_attr
|
||||
get_file
|
||||
get_param
|
||||
get_resource
|
||||
list_join
|
||||
repeat
|
||||
resource_facade
|
||||
str_replace
|
||||
Fn::Select
|
||||
|
||||
|
||||
.. _hot_spec_parameter_groups:
|
||||
|
||||
@ -801,6 +818,97 @@ A sample use of this function with a simple list is shown below.
|
||||
This would resolve to "one, two, and three".
|
||||
|
||||
|
||||
repeat
|
||||
------
|
||||
The *repeat* function allows for dynamically transforming lists by iterating
|
||||
over the contents of one or more source lists and replacing the list elements
|
||||
into a template. The result of this function is a new list, where the elements
|
||||
are set to the template, rendered for each list item.
|
||||
|
||||
The syntax of the repeat function is as follows:
|
||||
|
||||
::
|
||||
|
||||
repeat:
|
||||
template:
|
||||
<template>
|
||||
for_each:
|
||||
<var>: <list>
|
||||
|
||||
template
|
||||
The *template* argument defines the content generated for each iteration,
|
||||
with placeholders for the elements that need to be replaced at runtime.
|
||||
This argument can be of any supported type.
|
||||
for_each
|
||||
The *for_each* argument is a dictionary that defines how to generate the
|
||||
repetitions of the template and perform substitutions. In this dictionary
|
||||
the keys are the placeholder names that will be replaced in the template,
|
||||
and the values are the lists to iterate on. On each iteration, the function
|
||||
will render the template by performing substitution with elements of the
|
||||
given lists. If a single key/value pair is given in this argument, the
|
||||
template will be rendered once for each element in the list. When more
|
||||
than one key/value pairs are given, the iterations will be performed on all
|
||||
the permutations of values between the given lists. The values in this
|
||||
dictionary can be given as functions such as get_attr or get_param.
|
||||
|
||||
The example below shows how a security group resource can be defined to
|
||||
include a list of ports given as a parameter.
|
||||
|
||||
::
|
||||
|
||||
parameters:
|
||||
ports:
|
||||
type: comma_delimited_list
|
||||
label: ports
|
||||
default: "80,443,8080"
|
||||
|
||||
resources:
|
||||
security_group:
|
||||
type: OS::Neutron::SecurityGroup
|
||||
properties:
|
||||
name: web_server_security_group
|
||||
rules:
|
||||
repeat:
|
||||
for_each:
|
||||
%port%: { get_param: ports }
|
||||
template:
|
||||
protocol: tcp
|
||||
port_range_min: %port%
|
||||
port_range_max: %port%
|
||||
|
||||
The next example demonstrates how the use of multiple lists enables the
|
||||
security group to also include parameterized protocols.
|
||||
|
||||
::
|
||||
|
||||
parameters:
|
||||
ports:
|
||||
type: comma_delimited_list
|
||||
label: ports
|
||||
default: "80,443,8080"
|
||||
protocols:
|
||||
type: comma_delimited_list
|
||||
label: protocols
|
||||
default: "tcp,udp"
|
||||
|
||||
resources:
|
||||
security_group:
|
||||
type: OS::Neutron::SecurityGroup
|
||||
properties:
|
||||
name: web_server_security_group
|
||||
rules:
|
||||
repeat:
|
||||
for_each:
|
||||
%port%: { get_param: ports }
|
||||
%protocol%: { get_param: protocols }
|
||||
template:
|
||||
protocol: %protocol%
|
||||
port_range_min: %port%
|
||||
|
||||
Note how multiple entries in the ``for_each`` argument are equivalent to
|
||||
nested for-loops in most programming languages.
|
||||
|
||||
|
||||
resource_facade
|
||||
---------------
|
||||
The *resource_facade* function allows a provider template to retrieve data
|
||||
|
@ -12,6 +12,7 @@
|
||||
# under the License.
|
||||
|
||||
import collections
|
||||
import itertools
|
||||
|
||||
import six
|
||||
|
||||
@ -264,3 +265,72 @@ class Removed(function.Function):
|
||||
|
||||
def result(self):
|
||||
return super(Removed, self).result()
|
||||
|
||||
|
||||
class Repeat(function.Function):
|
||||
'''
|
||||
A function for iterating over a list of items.
|
||||
|
||||
Takes the form::
|
||||
|
||||
repeat:
|
||||
template:
|
||||
<body>
|
||||
for_each:
|
||||
<var>: <list>
|
||||
|
||||
The result is a new list of the same size as <list>, where each element
|
||||
is a copy of <body> with any occurrences of <var> replaced with the
|
||||
corresponding item of <list>.
|
||||
'''
|
||||
def __init__(self, stack, fn_name, args):
|
||||
super(Repeat, self).__init__(stack, fn_name, args)
|
||||
|
||||
self._for_each, self._template = self._parse_args()
|
||||
|
||||
def _parse_args(self):
|
||||
if not isinstance(self.args, collections.Mapping):
|
||||
raise TypeError(_('Arguments to "%s" must be a map') %
|
||||
self.fn_name)
|
||||
|
||||
try:
|
||||
for_each = self.args['for_each']
|
||||
template = self.args['template']
|
||||
except (KeyError, TypeError):
|
||||
example = ('''repeat:
|
||||
template: This is %var%
|
||||
for_each:
|
||||
%var%: ['a', 'b', 'c']''')
|
||||
raise KeyError(_('"repeat" syntax should be %s') %
|
||||
example)
|
||||
|
||||
if not isinstance(for_each, collections.Mapping):
|
||||
raise TypeError(_('The "for_each" argument to "%s" must contain '
|
||||
'a map') % self.fn_name)
|
||||
for v in six.itervalues(for_each):
|
||||
if not isinstance(v, list):
|
||||
raise TypeError(_('The values of the "for_each" argument to '
|
||||
'"%s" must be lists') % self.fn_name)
|
||||
|
||||
return for_each, template
|
||||
|
||||
def _do_replacement(self, keys, values, template):
|
||||
if isinstance(template, six.string_types):
|
||||
for (key, value) in zip(keys, values):
|
||||
template = template.replace(key, value)
|
||||
return template
|
||||
elif isinstance(template, collections.Sequence):
|
||||
return [self._do_replacement(keys, values, elem)
|
||||
for elem in template]
|
||||
elif isinstance(template, collections.Mapping):
|
||||
return dict((self._do_replacement(keys, values, k),
|
||||
self._do_replacement(keys, values, v))
|
||||
for (k, v) in template.items())
|
||||
|
||||
def result(self):
|
||||
for_each = function.resolve(self._for_each)
|
||||
keys = for_each.keys()
|
||||
lists = [for_each[key] for key in keys]
|
||||
template = function.resolve(self._template)
|
||||
return [self._do_replacement(keys, items, template)
|
||||
for items in itertools.product(*lists)]
|
||||
|
@ -293,3 +293,28 @@ class HOTemplate20141016(HOTemplate20130523):
|
||||
'Fn::ResourceFacade': hot_funcs.Removed,
|
||||
'Ref': hot_funcs.Removed,
|
||||
}
|
||||
|
||||
|
||||
class HOTemplate20150430(HOTemplate20141016):
|
||||
functions = {
|
||||
'get_attr': hot_funcs.GetAtt,
|
||||
'get_file': hot_funcs.GetFile,
|
||||
'get_param': hot_funcs.GetParam,
|
||||
'get_resource': cfn_funcs.ResourceRef,
|
||||
'list_join': hot_funcs.Join,
|
||||
'repeat': hot_funcs.Repeat,
|
||||
'resource_facade': hot_funcs.ResourceFacade,
|
||||
'str_replace': hot_funcs.Replace,
|
||||
|
||||
'Fn::Select': cfn_funcs.Select,
|
||||
|
||||
# functions removed from 20130523
|
||||
'Fn::GetAZs': hot_funcs.Removed,
|
||||
'Fn::Join': hot_funcs.Removed,
|
||||
'Fn::Split': hot_funcs.Removed,
|
||||
'Fn::Replace': hot_funcs.Removed,
|
||||
'Fn::Base64': hot_funcs.Removed,
|
||||
'Fn::MemberListToMap': hot_funcs.Removed,
|
||||
'Fn::ResourceFacade': hot_funcs.Removed,
|
||||
'Ref': hot_funcs.Removed,
|
||||
}
|
||||
|
@ -43,6 +43,10 @@ hot_juno_tpl_empty = template_format.parse('''
|
||||
heat_template_version: 2014-10-16
|
||||
''')
|
||||
|
||||
hot_kilo_tpl_empty = template_format.parse('''
|
||||
heat_template_version: 2015-04-30
|
||||
''')
|
||||
|
||||
hot_tpl_empty_sections = template_format.parse('''
|
||||
heat_template_version: 2013-05-23
|
||||
parameters:
|
||||
@ -585,6 +589,86 @@ class HOTemplateTest(common.HeatTestCase):
|
||||
|
||||
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl, stack))
|
||||
|
||||
def test_repeat(self):
|
||||
"""Test repeat function."""
|
||||
snippet = {'repeat': {'template': 'this is %var%',
|
||||
'for_each': {'%var%': ['a', 'b', 'c']}}}
|
||||
snippet_resolved = ['this is a', 'this is b', 'this is c']
|
||||
|
||||
tmpl = parser.Template(hot_kilo_tpl_empty)
|
||||
|
||||
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl))
|
||||
|
||||
def test_repeat_dict_template(self):
|
||||
"""Test repeat function with a dictionary as a template."""
|
||||
snippet = {'repeat': {'template': {'key-%var%': 'this is %var%'},
|
||||
'for_each': {'%var%': ['a', 'b', 'c']}}}
|
||||
snippet_resolved = [{'key-a': 'this is a'},
|
||||
{'key-b': 'this is b'},
|
||||
{'key-c': 'this is c'}]
|
||||
|
||||
tmpl = parser.Template(hot_kilo_tpl_empty)
|
||||
|
||||
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl))
|
||||
|
||||
def test_repeat_list_template(self):
|
||||
"""Test repeat function with a list as a template."""
|
||||
snippet = {'repeat': {'template': ['this is %var%', 'static'],
|
||||
'for_each': {'%var%': ['a', 'b', 'c']}}}
|
||||
snippet_resolved = [['this is a', 'static'],
|
||||
['this is b', 'static'],
|
||||
['this is c', 'static']]
|
||||
|
||||
tmpl = parser.Template(hot_kilo_tpl_empty)
|
||||
|
||||
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl))
|
||||
|
||||
def test_repeat_multi_list(self):
|
||||
"""Test repeat function with multiple input lists."""
|
||||
snippet = {'repeat': {'template': 'this is %var1%-%var2%',
|
||||
'for_each': {'%var1%': ['a', 'b', 'c'],
|
||||
'%var2%': ['1', '2']}}}
|
||||
snippet_resolved = ['this is a-1', 'this is b-1', 'this is c-1',
|
||||
'this is a-2', 'this is b-2', 'this is c-2']
|
||||
|
||||
tmpl = parser.Template(hot_kilo_tpl_empty)
|
||||
|
||||
result = self.resolve(snippet, tmpl)
|
||||
self.assertEqual(len(result), len(snippet_resolved))
|
||||
for item in result:
|
||||
self.assertIn(item, snippet_resolved)
|
||||
|
||||
def test_repeat_bad_args(self):
|
||||
"""
|
||||
Test that the repeat function reports a proper error when missing
|
||||
or invalid arguments.
|
||||
"""
|
||||
tmpl = parser.Template(hot_kilo_tpl_empty)
|
||||
|
||||
# missing for_each
|
||||
snippet = {'repeat': {'template': 'this is %var%'}}
|
||||
self.assertRaises(KeyError, self.resolve, snippet, tmpl)
|
||||
|
||||
# mispelled for_each
|
||||
snippet = {'repeat': {'template': 'this is %var%',
|
||||
'foreach': {'%var%': ['a', 'b', 'c']}}}
|
||||
self.assertRaises(KeyError, self.resolve, snippet, tmpl)
|
||||
|
||||
# for_each is not a map
|
||||
snippet = {'repeat': {'template': 'this is %var%',
|
||||
'for_each': '%var%'}}
|
||||
self.assertRaises(TypeError, self.resolve, snippet, tmpl)
|
||||
|
||||
# value given to for_each entry is not a list
|
||||
snippet = {'repeat': {'template': 'this is %var%',
|
||||
'for_each': {'%var%': 'a'}}}
|
||||
self.assertRaises(TypeError, self.resolve, snippet, tmpl)
|
||||
|
||||
# mispelled template
|
||||
snippet = {'repeat': {'templte': 'this is %var%',
|
||||
'for_each': {'%var%': ['a', 'b', 'c']}}}
|
||||
self.assertRaises(KeyError, self.resolve, snippet, tmpl)
|
||||
|
||||
def test_prevent_parameters_access(self):
|
||||
"""
|
||||
Test that the parameters section can't be accessed using the template
|
||||
|
@ -198,7 +198,7 @@ class TemplateTest(common.HeatTestCase):
|
||||
}''')
|
||||
init_ex = self.assertRaises(exception.InvalidTemplateVersion,
|
||||
parser.Template, invalid_hot_version_tmp)
|
||||
valid_versions = ['2014-10-16', '2013-05-23']
|
||||
valid_versions = ['2014-10-16', '2013-05-23', '2015-04-30']
|
||||
ex_error_msg = ('The template version is invalid: '
|
||||
'"heat_template_version: 2012-12-12". '
|
||||
'"heat_template_version" should be one of: %s'
|
||||
|
@ -77,6 +77,7 @@ heat.stack_lifecycle_plugins =
|
||||
heat.templates =
|
||||
heat_template_version.2013-05-23 = heat.engine.hot.template:HOTemplate20130523
|
||||
heat_template_version.2014-10-16 = heat.engine.hot.template:HOTemplate20141016
|
||||
heat_template_version.2015-04-30 = heat.engine.hot.template:HOTemplate20150430
|
||||
HeatTemplateFormatVersion.2012-12-12 = heat.engine.cfn.template:HeatTemplate
|
||||
AWSTemplateFormatVersion.2010-09-09 = heat.engine.cfn.template:CfnTemplate
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user