Fix update preview to handle nested stacks
Currently the update preview code has no support for previewing the effect of an update on nested stacks, which I assume was an oversight in the original implementation. So this adds a show_nested flag to the API which allows enabling recursive preview of the whole update including nested stacks. Closes-Bug: #1521971 Depends-On: I06f3b52d5d48dd5e6e266321e58ca8e6116d6017 Change-Id: I96af4d2f07056846aac7ae9ad9b6eb160e8bd51a
This commit is contained in:
parent
8b83e14d84
commit
4cfd9a10ac
@ -486,12 +486,23 @@ class StackController(object):
|
||||
|
||||
raise exc.HTTPAccepted()
|
||||
|
||||
def _param_show_nested(self, req):
|
||||
whitelist = {'show_nested': 'single'}
|
||||
params = util.get_allowed_params(req.params, whitelist)
|
||||
|
||||
p_name = 'show_nested'
|
||||
if p_name in params:
|
||||
return self._extract_bool_param(p_name, params[p_name])
|
||||
|
||||
@util.identified_stack
|
||||
def preview_update(self, req, identity, body):
|
||||
"""Preview update for existing stack with a new template/parameters."""
|
||||
data = InstantiationData(body)
|
||||
|
||||
args = self.prepare_args(data)
|
||||
show_nested = self._param_show_nested(req)
|
||||
if show_nested is not None:
|
||||
args[rpc_api.PARAM_SHOW_NESTED] = show_nested
|
||||
changes = self.rpc_client.preview_update_stack(
|
||||
req.context,
|
||||
identity,
|
||||
@ -509,6 +520,9 @@ class StackController(object):
|
||||
data = InstantiationData(body, patch=True)
|
||||
|
||||
args = self.prepare_args(data)
|
||||
show_nested = self._param_show_nested(req)
|
||||
if show_nested is not None:
|
||||
args['show_nested'] = show_nested
|
||||
changes = self.rpc_client.preview_update_stack(
|
||||
req.context,
|
||||
identity,
|
||||
|
@ -930,20 +930,81 @@ class EngineService(service.Service):
|
||||
|
||||
actions = update_task.preview()
|
||||
|
||||
def fmt_updated_res(k):
|
||||
return api.format_stack_resource(updated_stack.resources.get(k))
|
||||
def fmt_action_map(current, updated, act):
|
||||
def fmt_updated_res(k):
|
||||
return api.format_stack_resource(updated.resources.get(k))
|
||||
|
||||
def fmt_current_res(k):
|
||||
return api.format_stack_resource(current_stack.resources.get(k))
|
||||
def fmt_current_res(k):
|
||||
return api.format_stack_resource(current.resources.get(k))
|
||||
|
||||
return {
|
||||
'unchanged': map(fmt_updated_res, act.get('unchanged', [])),
|
||||
'updated': map(fmt_current_res, act.get('updated', [])),
|
||||
'replaced': map(fmt_updated_res, act.get('replaced', [])),
|
||||
'added': map(fmt_updated_res, act.get('added', [])),
|
||||
'deleted': map(fmt_current_res, act.get('deleted', [])),
|
||||
}
|
||||
|
||||
updated_stack.id = current_stack.id
|
||||
return {
|
||||
'unchanged': map(fmt_updated_res, actions['unchanged']),
|
||||
'updated': map(fmt_current_res, actions['updated']),
|
||||
'replaced': map(fmt_updated_res, actions['replaced']),
|
||||
'added': map(fmt_updated_res, actions['added']),
|
||||
'deleted': map(fmt_current_res, actions['deleted']),
|
||||
}
|
||||
fmt_actions = fmt_action_map(current_stack, updated_stack, actions)
|
||||
|
||||
if args.get(rpc_api.PARAM_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
|
||||
updated_stack.preview_resources()
|
||||
|
||||
def nested_fmt_actions(current, updated, act):
|
||||
updated.id = current.id
|
||||
|
||||
# Recurse for resources deleted from the current stack,
|
||||
# which is all those marked as deleted or replaced
|
||||
def _n_deleted(stk, deleted):
|
||||
for rsrc in deleted:
|
||||
deleted_rsrc = stk.resources.get(rsrc)
|
||||
if deleted_rsrc.has_nested():
|
||||
nested_stk = deleted_rsrc.nested()
|
||||
nested_rsrc = nested_stk.resources.keys()
|
||||
n_fmt = fmt_action_map(
|
||||
nested_stk, None, {'deleted': nested_rsrc})
|
||||
fmt_actions['deleted'].extend(n_fmt['deleted'])
|
||||
_n_deleted(nested_stk, nested_rsrc)
|
||||
_n_deleted(current, act['deleted'] + act['replaced'])
|
||||
|
||||
# Recurse for all resources added to the updated stack,
|
||||
# which is all those marked added or replaced
|
||||
def _n_added(stk, added):
|
||||
for rsrc in added:
|
||||
added_rsrc = stk.resources.get(rsrc)
|
||||
if added_rsrc.has_nested():
|
||||
nested_stk = added_rsrc.nested()
|
||||
nested_rsrc = nested_stk.resources.keys()
|
||||
n_fmt = fmt_action_map(
|
||||
None, nested_stk, {'added': nested_rsrc})
|
||||
fmt_actions['added'].extend(n_fmt['added'])
|
||||
_n_added(nested_stk, nested_rsrc)
|
||||
_n_added(updated, act['added'] + act['replaced'])
|
||||
|
||||
# Recursively preview all "updated" resources
|
||||
for rsrc in act['updated']:
|
||||
current_rsrc = current.resources.get(rsrc)
|
||||
updated_rsrc = updated.resources.get(rsrc)
|
||||
if current_rsrc.has_nested() and updated_rsrc.has_nested():
|
||||
current_nested = current_rsrc.nested()
|
||||
updated_nested = updated_rsrc.nested()
|
||||
update_task = update.StackUpdate(
|
||||
current_nested, updated_nested, None)
|
||||
n_actions = update_task.preview()
|
||||
n_fmt_actions = fmt_action_map(
|
||||
current_nested, updated_nested, n_actions)
|
||||
for k in fmt_actions:
|
||||
fmt_actions[k].extend(n_fmt_actions[k])
|
||||
nested_fmt_actions(current_nested, updated_nested,
|
||||
n_actions)
|
||||
# Start the recursive nested_fmt_actions with the parent stack.
|
||||
nested_fmt_actions(current_stack, updated_stack, actions)
|
||||
|
||||
return fmt_actions
|
||||
|
||||
@context.request_context
|
||||
def stack_cancel_update(self, cnxt, stack_identity,
|
||||
|
@ -259,6 +259,6 @@ class StackUpdate(object):
|
||||
set(updated_keys + replaced_keys))),
|
||||
'updated': updated_keys,
|
||||
'replaced': replaced_keys,
|
||||
'added': added_keys,
|
||||
'deleted': deleted_keys,
|
||||
'added': list(added_keys),
|
||||
'deleted': list(deleted_keys),
|
||||
}
|
||||
|
@ -392,7 +392,8 @@ class HeatIntegrationTest(testscenarios.WithScenarios,
|
||||
|
||||
def preview_update_stack(self, stack_identifier, template,
|
||||
environment=None, files=None, parameters=None,
|
||||
tags=None, disable_rollback=True):
|
||||
tags=None, disable_rollback=True,
|
||||
show_nested=False):
|
||||
env = environment or {}
|
||||
env_files = files or {}
|
||||
parameters = parameters or {}
|
||||
@ -406,7 +407,8 @@ class HeatIntegrationTest(testscenarios.WithScenarios,
|
||||
disable_rollback=disable_rollback,
|
||||
parameters=parameters,
|
||||
environment=env,
|
||||
tags=tags
|
||||
tags=tags,
|
||||
show_nested=show_nested
|
||||
)
|
||||
|
||||
def assert_resource_is_a_stack(self, stack_identifier, res_name,
|
||||
|
@ -54,7 +54,14 @@ test_template_two_resource = {
|
||||
}
|
||||
|
||||
|
||||
class UpdatePreviewStackTest(functional_base.FunctionalTestsBase):
|
||||
class UpdatePreviewBase(functional_base.FunctionalTestsBase):
|
||||
|
||||
def assert_empty_sections(self, changes, empty_sections):
|
||||
for section in empty_sections:
|
||||
self.assertEqual([], changes[section])
|
||||
|
||||
|
||||
class UpdatePreviewStackTest(UpdatePreviewBase):
|
||||
|
||||
def test_add_resource(self):
|
||||
self.stack_identifier = self.stack_create(
|
||||
@ -69,9 +76,7 @@ class UpdatePreviewStackTest(functional_base.FunctionalTestsBase):
|
||||
added = changes['added'][0]['resource_name']
|
||||
self.assertEqual('test2', added)
|
||||
|
||||
empty_sections = ('updated', 'replaced', 'deleted')
|
||||
for section in empty_sections:
|
||||
self.assertEqual([], changes[section])
|
||||
self.assert_empty_sections(changes, ['updated', 'replaced', 'deleted'])
|
||||
|
||||
def test_no_change(self):
|
||||
self.stack_identifier = self.stack_create(
|
||||
@ -83,9 +88,8 @@ class UpdatePreviewStackTest(functional_base.FunctionalTestsBase):
|
||||
unchanged = changes['unchanged'][0]['resource_name']
|
||||
self.assertEqual('test1', unchanged)
|
||||
|
||||
empty_sections = ('updated', 'replaced', 'deleted', 'added')
|
||||
for section in empty_sections:
|
||||
self.assertEqual([], changes[section])
|
||||
self.assert_empty_sections(
|
||||
changes, ['updated', 'replaced', 'deleted', 'added'])
|
||||
|
||||
def test_update_resource(self):
|
||||
self.stack_identifier = self.stack_create(
|
||||
@ -113,9 +117,8 @@ class UpdatePreviewStackTest(functional_base.FunctionalTestsBase):
|
||||
updated = changes['updated'][0]['resource_name']
|
||||
self.assertEqual('test1', updated)
|
||||
|
||||
empty_sections = ('added', 'unchanged', 'replaced', 'deleted')
|
||||
for section in empty_sections:
|
||||
self.assertEqual([], changes[section])
|
||||
self.assert_empty_sections(
|
||||
changes, ['added', 'unchanged', 'replaced', 'deleted'])
|
||||
|
||||
def test_replaced_resource(self):
|
||||
self.stack_identifier = self.stack_create(
|
||||
@ -139,9 +142,8 @@ class UpdatePreviewStackTest(functional_base.FunctionalTestsBase):
|
||||
replaced = changes['replaced'][0]['resource_name']
|
||||
self.assertEqual('test1', replaced)
|
||||
|
||||
empty_sections = ('added', 'unchanged', 'updated', 'deleted')
|
||||
for section in empty_sections:
|
||||
self.assertEqual([], changes[section])
|
||||
self.assert_empty_sections(
|
||||
changes, ['added', 'unchanged', 'updated', 'deleted'])
|
||||
|
||||
def test_delete_resource(self):
|
||||
self.stack_identifier = self.stack_create(
|
||||
@ -156,6 +158,141 @@ class UpdatePreviewStackTest(functional_base.FunctionalTestsBase):
|
||||
deleted = changes['deleted'][0]['resource_name']
|
||||
self.assertEqual('test2', deleted)
|
||||
|
||||
empty_sections = ('updated', 'replaced', 'added')
|
||||
for section in empty_sections:
|
||||
self.assertEqual([], changes[section])
|
||||
self.assert_empty_sections(changes, ['updated', 'replaced', 'added'])
|
||||
|
||||
|
||||
class UpdatePreviewStackTestNested(UpdatePreviewBase):
|
||||
template_nested_parent = '''
|
||||
heat_template_version: 2016-04-08
|
||||
resources:
|
||||
nested1:
|
||||
type: nested1.yaml
|
||||
'''
|
||||
|
||||
template_nested1 = '''
|
||||
heat_template_version: 2016-04-08
|
||||
resources:
|
||||
nested2:
|
||||
type: nested2.yaml
|
||||
'''
|
||||
|
||||
template_nested2 = '''
|
||||
heat_template_version: 2016-04-08
|
||||
resources:
|
||||
random:
|
||||
type: OS::Heat::RandomString
|
||||
'''
|
||||
|
||||
template_nested2_2 = '''
|
||||
heat_template_version: 2016-04-08
|
||||
resources:
|
||||
random:
|
||||
type: OS::Heat::RandomString
|
||||
random2:
|
||||
type: OS::Heat::RandomString
|
||||
'''
|
||||
|
||||
def _get_by_resource_name(self, changes, name, action):
|
||||
filtered_l = [x for x in changes[action]
|
||||
if x['resource_name'] == name]
|
||||
self.assertEqual(1, len(filtered_l))
|
||||
return filtered_l[0]
|
||||
|
||||
def test_nested_resources_nochange(self):
|
||||
files = {'nested1.yaml': self.template_nested1,
|
||||
'nested2.yaml': self.template_nested2}
|
||||
self.stack_identifier = self.stack_create(
|
||||
template=self.template_nested_parent, files=files)
|
||||
result = self.preview_update_stack(
|
||||
self.stack_identifier,
|
||||
template=self.template_nested_parent,
|
||||
files=files, show_nested=True)
|
||||
changes = result['resource_changes']
|
||||
|
||||
# The nested random resource should be unchanged, but we always
|
||||
# update nested stacks even when there are no changes
|
||||
self.assertEqual(1, len(changes['unchanged']))
|
||||
self.assertEqual('random', changes['unchanged'][0]['resource_name'])
|
||||
self.assertEqual('nested2', changes['unchanged'][0]['parent_resource'])
|
||||
|
||||
self.assertEqual(2, len(changes['updated']))
|
||||
u_nested1 = self._get_by_resource_name(changes, 'nested1', 'updated')
|
||||
self.assertNotIn('parent_resource', u_nested1)
|
||||
u_nested2 = self._get_by_resource_name(changes, 'nested2', 'updated')
|
||||
self.assertEqual('nested1', u_nested2['parent_resource'])
|
||||
|
||||
self.assert_empty_sections(changes, ['replaced', 'deleted', 'added'])
|
||||
|
||||
def test_nested_resources_add(self):
|
||||
files = {'nested1.yaml': self.template_nested1,
|
||||
'nested2.yaml': self.template_nested2}
|
||||
self.stack_identifier = self.stack_create(
|
||||
template=self.template_nested_parent, files=files)
|
||||
files['nested2.yaml'] = self.template_nested2_2
|
||||
result = self.preview_update_stack(
|
||||
self.stack_identifier,
|
||||
template=self.template_nested_parent,
|
||||
files=files, show_nested=True)
|
||||
changes = result['resource_changes']
|
||||
|
||||
# The nested random resource should be unchanged, but we always
|
||||
# update nested stacks even when there are no changes
|
||||
self.assertEqual(1, len(changes['unchanged']))
|
||||
self.assertEqual('random', changes['unchanged'][0]['resource_name'])
|
||||
self.assertEqual('nested2', changes['unchanged'][0]['parent_resource'])
|
||||
|
||||
self.assertEqual(1, len(changes['added']))
|
||||
self.assertEqual('random2', changes['added'][0]['resource_name'])
|
||||
self.assertEqual('nested2', changes['added'][0]['parent_resource'])
|
||||
|
||||
self.assert_empty_sections(changes, ['replaced', 'deleted'])
|
||||
|
||||
def test_nested_resources_delete(self):
|
||||
files = {'nested1.yaml': self.template_nested1,
|
||||
'nested2.yaml': self.template_nested2_2}
|
||||
self.stack_identifier = self.stack_create(
|
||||
template=self.template_nested_parent, files=files)
|
||||
files['nested2.yaml'] = self.template_nested2
|
||||
result = self.preview_update_stack(
|
||||
self.stack_identifier,
|
||||
template=self.template_nested_parent,
|
||||
files=files, show_nested=True)
|
||||
changes = result['resource_changes']
|
||||
|
||||
# The nested random resource should be unchanged, but we always
|
||||
# update nested stacks even when there are no changes
|
||||
self.assertEqual(1, len(changes['unchanged']))
|
||||
self.assertEqual('random', changes['unchanged'][0]['resource_name'])
|
||||
self.assertEqual('nested2', changes['unchanged'][0]['parent_resource'])
|
||||
|
||||
self.assertEqual(1, len(changes['deleted']))
|
||||
self.assertEqual('random2', changes['deleted'][0]['resource_name'])
|
||||
self.assertEqual('nested2', changes['deleted'][0]['parent_resource'])
|
||||
|
||||
self.assert_empty_sections(changes, ['replaced', 'added'])
|
||||
|
||||
def test_nested_resources_replace(self):
|
||||
files = {'nested1.yaml': self.template_nested1,
|
||||
'nested2.yaml': self.template_nested2}
|
||||
self.stack_identifier = self.stack_create(
|
||||
template=self.template_nested_parent, files=files)
|
||||
parent_none = self.template_nested_parent.replace(
|
||||
'nested1.yaml', 'OS::Heat::None')
|
||||
result = self.preview_update_stack(
|
||||
self.stack_identifier,
|
||||
template=parent_none,
|
||||
show_nested=True)
|
||||
changes = result['resource_changes']
|
||||
|
||||
# The nested random resource should be unchanged, but we always
|
||||
# update nested stacks even when there are no changes
|
||||
self.assertEqual(1, len(changes['replaced']))
|
||||
self.assertEqual('nested1', changes['replaced'][0]['resource_name'])
|
||||
|
||||
self.assertEqual(2, len(changes['deleted']))
|
||||
d_random = self._get_by_resource_name(changes, 'random', 'deleted')
|
||||
self.assertEqual('nested2', d_random['parent_resource'])
|
||||
d_nested2 = self._get_by_resource_name(changes, 'nested2', 'deleted')
|
||||
self.assertEqual('nested1', d_nested2['parent_resource'])
|
||||
|
||||
self.assert_empty_sections(changes, ['updated', 'unchanged', 'added'])
|
||||
|
Loading…
Reference in New Issue
Block a user