Allow nested validation for template-validate

Adds an optional flag that causes template-validate to recursively
validate nested stacks and expose a schema of the parameters for
all stacks in the tree.  This uses a similar method to stack preview
in that it builds the tree in memory so we can then walk it and
extract the parameter data we need.

Change-Id: I7e3d752124997a0a7b8ff53e774ed8034dd2f81d
Co-Authored-By: Jay Dobies <jdobies@redhat.com>
Implements-blueprint: nested-validation
This commit is contained in:
Steven Hardy 2015-09-16 14:07:58 +01:00
parent a7ec465031
commit 506919fcf6
8 changed files with 155 additions and 15 deletions

View File

@ -535,10 +535,20 @@ class StackController(object):
data = InstantiationData(body)
whitelist = {'show_nested': 'single'}
params = util.get_allowed_params(req.params, whitelist)
show_nested = False
p_name = rpc_api.PARAM_SHOW_NESTED
if p_name in params:
params[p_name] = self._extract_bool_param(p_name, params[p_name])
show_nested = params[p_name]
result = self.rpc_client.validate_template(req.context,
data.template(),
data.environment(),
files=data.files())
files=data.files(),
show_nested=show_nested)
if 'Error' in result:
raise exc.HTTPBadRequest(result['Error'])

View File

@ -271,7 +271,7 @@ class EngineService(service.Service):
by the RPC caller.
"""
RPC_API_VERSION = '1.17'
RPC_API_VERSION = '1.18'
def __init__(self, host, topic):
super(EngineService, self).__init__()
@ -953,13 +953,17 @@ class EngineService(service.Service):
engine_id=engine_id)
@context.request_context
def validate_template(self, cnxt, template, params=None, files=None):
"""Uses the stack parser to check the validity of a template.
def validate_template(self, cnxt, template, params=None, files=None,
show_nested=False):
"""
The validate_template method uses the stack parser to check
the validity of a template.
:param cnxt: RPC context.
:param template: Template of stack you want to create.
:param params: Stack Input Params
:param files: Files referenced from the template
:param show_nested: if True, any nested templates will be checked
"""
LOG.info(_LI('validate_template'))
if template is None:
@ -996,6 +1000,30 @@ class EngineService(service.Service):
if param_groups.parameter_groups:
result['ParameterGroups'] = param_groups.parameter_groups
if show_nested:
# Note preview_resources is needed here to build the tree
# of nested resources/stacks in memory, otherwise the
# nested/has_nested() tests below won't work
stack.preview_resources()
def nested_params(stk):
n_result = {}
for r in stk:
if stk[r].has_nested():
n_params = stk[r].nested().parameters.map(
api.format_validate_parameter,
filter_func=filter_parameter)
n_result[r] = {
'Type': stk[r].type(),
'Description': stk[r].nested().t.get(
'Description', ''),
'Parameters': n_params
}
n_result[r].update(nested_params(stk[r].nested()))
return {'NestedParameters': n_result} if n_result else {}
result.update(nested_params(stack))
return result
@context.request_context

View File

@ -38,6 +38,7 @@ class EngineClient(object):
1.15 - Add preview_update_stack() call
1.16 - Adds version, type_name to list_resource_types()
1.17 - Add files to validate_template
1.18 - Add show_nested to validate_template
'''
BASE_RPC_API_VERSION = '1.0'
@ -293,7 +294,8 @@ class EngineClient(object):
),
version='1.15')
def validate_template(self, ctxt, template, params=None, files=None):
def validate_template(self, ctxt, template, params=None, files=None,
show_nested=False):
"""
The validate_template method uses the stack parser to check
the validity of a template.
@ -302,12 +304,14 @@ class EngineClient(object):
:param template: Template of stack you want to create.
:param params: Stack Input Params/Environment
:param files: files referenced from the environment/template.
:param show_nested: if True nested templates will be validated
"""
return self.call(ctxt, self.make_msg('validate_template',
template=template,
params=params,
files=files),
version='1.17')
files=files,
show_nested=show_nested),
version='1.18')
def authenticated_to_backend(self, ctxt):
"""

View File

@ -1074,8 +1074,8 @@ class CfnStackControllerTest(common.HeatTestCase):
rpc_client.EngineClient.call(
dummy_req.context,
('validate_template', {'template': json_template, 'params': None,
'files': None}),
version='1.17'
'files': None, 'show_nested': False}),
version='1.18'
).AndReturn(response)
self.m.ReplayAll()

View File

@ -2059,8 +2059,9 @@ class StackControllerTest(tools.ControllerTest, common.HeatTestCase):
'encrypted_param_names': [],
'parameter_defaults': {},
'resource_registry': {}},
'files': {}}),
version='1.17'
'files': {},
'show_nested': False}),
version='1.18'
).AndReturn(engine_response)
self.m.ReplayAll()
@ -2086,8 +2087,9 @@ class StackControllerTest(tools.ControllerTest, common.HeatTestCase):
'encrypted_param_names': [],
'parameter_defaults': {},
'resource_registry': {}},
'files': {}}),
version='1.17'
'files': {},
'show_nested': False}),
version='1.18'
).AndReturn({'Error': 'fubar'})
self.m.ReplayAll()

View File

@ -39,7 +39,7 @@ class ServiceEngineTest(common.HeatTestCase):
def test_make_sure_rpc_version(self):
self.assertEqual(
'1.17',
'1.18',
service.EngineService.RPC_API_VERSION,
('RPC version is changed, please update this test to new version '
'and make sure additional test cases are added for RPC APIs '

View File

@ -199,7 +199,8 @@ class EngineRpcAPITestCase(common.HeatTestCase):
self._test_engine_api('validate_template', 'call',
template={u'Foo': u'bar'},
params={u'Egg': u'spam'},
files=None)
files=None,
show_nested=False)
def test_list_resource_types(self):
self._test_engine_api('list_resource_types',

View File

@ -35,6 +35,29 @@ resources:
length: {get_param: aparam}
'''
parent_template = '''
heat_template_version: 2014-10-16
description: the parent template
parameters:
pparam:
type: number
default: 5
description: the param description
resources:
nres:
type: mynested.yaml
properties:
aparam: {get_param: pparam}
'''
parent_template_noprop = '''
heat_template_version: 2014-10-16
description: the parent template
resources:
nres:
type: mynested.yaml
'''
random_template_groups = '''
heat_template_version: 2014-10-16
description: the stack description
@ -147,3 +170,75 @@ resources:
'NoEcho': 'true',
'Type': 'String'}}}
self.assertEqual(expected, ret)
def test_template_validate_nested_off(self):
files = {'mynested.yaml': self.random_template}
ret = self.client.stacks.validate(template=self.parent_template,
files=files)
expected = {'Description': 'the parent template',
'Parameters': {
'pparam': {'Default': 5,
'Description': 'the param description',
'Label': 'pparam',
'NoEcho': 'false',
'Type': 'Number'}}}
self.assertEqual(expected, ret)
def test_template_validate_nested_on(self):
files = {'mynested.yaml': self.random_template}
ret = self.client.stacks.validate(template=self.parent_template_noprop,
files=files,
show_nested=True)
expected = {'Description': 'the parent template',
'Parameters': {},
'NestedParameters': {
'nres': {'Description': 'the stack description',
'Parameters': {'aparam': {'Default': 10,
'Description':
'the param '
'description',
'Label': 'aparam',
'NoEcho': 'false',
'Type': 'Number'}},
'Type': 'mynested.yaml'}}}
self.assertEqual(expected, ret)
def test_template_validate_nested_on_multiple(self):
# parent_template -> nested_template -> random_template
nested_template = self.random_template.replace(
'OS::Heat::RandomString', 'mynested2.yaml')
files = {'mynested.yaml': nested_template,
'mynested2.yaml': self.random_template}
ret = self.client.stacks.validate(template=self.parent_template,
files=files,
show_nested=True)
n_param2 = {'myres': {'Description': 'the stack description',
'Parameters': {'aparam': {'Default': 10,
'Description':
'the param '
'description',
'Label': 'aparam',
'NoEcho': 'false',
'Type': 'Number'}},
'Type': 'mynested2.yaml'}}
expected = {'Description': 'the parent template',
'Parameters': {
'pparam': {'Default': 5,
'Description': 'the param description',
'Label': 'pparam',
'NoEcho': 'false',
'Type': 'Number'}},
'NestedParameters': {
'nres': {'Description': 'the stack description',
'Parameters': {'aparam': {'Default': 10,
'Description':
'the param '
'description',
'Label': 'aparam',
'Value': 5,
'NoEcho': 'false',
'Type': 'Number'}},
'NestedParameters': n_param2,
'Type': 'mynested.yaml'}}}
self.assertEqual(expected, ret)