Pass the parent's registry into child stacks

We make sure that there is no infinite recursion by not repeating
a TemplateResource within a resources inheritance.

Change-Id: I1ffa66886923b15b52a8075089dd4042f0a55862
Fixes-bug: #1297396
This commit is contained in:
Angus Salkeld 2014-05-29 10:03:40 +10:00
parent 0262a29325
commit 37bb332f35
5 changed files with 143 additions and 12 deletions

View File

@ -248,7 +248,7 @@ class ResourceRegistry(object):
yield self._registry[pattern]
def get_resource_info(self, resource_type, resource_name=None,
registry_type=None):
registry_type=None, accept_fn=None):
"""Find possible matches to the resource type and name.
chain the results from the global and user registry to find
a match.
@ -279,10 +279,11 @@ class ResourceRegistry(object):
for info in sorted(matches):
match = info.get_resource_info(resource_type,
resource_name)
if registry_type is None or isinstance(match, registry_type):
if ((registry_type is None or isinstance(match, registry_type)) and
(accept_fn is None or accept_fn(info))):
return match
def get_class(self, resource_type, resource_name=None):
def get_class(self, resource_type, resource_name=None, accept_fn=None):
if resource_type == "":
msg = _('Resource "%s" has no type') % resource_name
raise exception.StackValidationFailed(message=msg)
@ -295,7 +296,8 @@ class ResourceRegistry(object):
raise exception.StackValidationFailed(message=msg)
info = self.get_resource_info(resource_type,
resource_name=resource_name)
resource_name=resource_name,
accept_fn=accept_fn)
if info is None:
msg = _("Unknown resource Type : %s") % resource_type
raise exception.StackValidationFailed(message=msg)

View File

@ -22,6 +22,7 @@ from heat.common import identifier
from heat.common import short_id
from heat.db import api as db_api
from heat.engine.attributes import Attributes
from heat.engine import environment
from heat.engine import event
from heat.engine import function
from heat.engine.properties import Properties
@ -101,9 +102,32 @@ class Resource(object):
# Call is already for a subclass, so pass it through
return super(Resource, cls).__new__(cls)
# Select the correct subclass to instantiate
ResourceClass = stack.env.get_class(json.get('Type'),
resource_name=name)
from heat.engine.resources import template_resource
# Select the correct subclass to instantiate.
# Note: If the current stack is an implementation of
# a resource type (a TemplateResource mapped in the environment)
# then don't infinitely recurse by creating a child stack
# of the same type. Instead get the next match which will get
# us closer to a concrete class.
def get_ancestor_template_resources():
"""Return an ancestory list (TemplateResources only)."""
parent = stack.parent_resource
while parent is not None:
if isinstance(parent, template_resource.TemplateResource):
yield parent.template_name
parent = parent.stack.parent_resource
ancestor_list = set(get_ancestor_template_resources())
def accept_class(res_info):
if not isinstance(res_info, environment.TemplateResourceInfo):
return True
return res_info.template_name not in ancestor_list
ResourceClass = stack.env.registry.get_class(json.get('Type'),
resource_name=name,
accept_fn=accept_class)
return ResourceClass(name, json, stack)
def __init__(self, name, json_snippet, stack):

View File

@ -111,7 +111,7 @@ class StackResource(resource.Resource):
nested = parser.Stack(self.context,
name,
template,
environment.Environment(params),
self._nested_environment(params),
disable_rollback=True,
parent_resource=self,
owner_id=self.stack.id)
@ -125,6 +125,23 @@ class StackResource(resource.Resource):
message = exception.StackResourceLimitExceeded.msg_fmt
raise exception.RequestLimitExceeded(message=message)
def _nested_environment(self, user_params):
"""Build a sensible environment for the nested stack.
This is built from the user_params and the parent stack's registry
so we can use user-defined resources within nested stacks.
"""
nested_env = environment.Environment()
nested_env.registry = self.stack.env.registry
user_env = {environment.PARAMETERS: {}}
if user_params is not None:
if environment.PARAMETERS not in user_params:
user_env[environment.PARAMETERS] = user_params
else:
user_env.update(user_params)
nested_env.load(user_env)
return nested_env
def create_with_template(self, child_template, user_params,
timeout_mins=None, adopt_data=None):
'''
@ -146,7 +163,7 @@ class StackResource(resource.Resource):
nested = parser.Stack(self.context,
self.physical_resource_name(),
template,
environment.Environment(user_params),
self._nested_environment(user_params),
timeout_mins=timeout_mins,
disable_rollback=True,
parent_resource=self,
@ -203,7 +220,7 @@ class StackResource(resource.Resource):
stack = parser.Stack(self.context,
self.physical_resource_name(),
template,
environment.Environment(user_params),
self._nested_environment(user_params),
timeout_mins=timeout_mins,
disable_rollback=True,
parent_resource=self,

View File

@ -575,6 +575,94 @@ class ProviderTemplateTest(HeatTestCase):
self.m.VerifyAll()
class NestedProvider(HeatTestCase):
"""Prove that we can use the registry in a nested provider."""
def setUp(self):
super(NestedProvider, self).setUp()
utils.setup_dummy_db()
def test_nested_env(self):
main_templ = '''
heat_template_version: 2013-05-23
resources:
secret2:
type: My::NestedSecret
outputs:
secret1:
value: { get_attr: [secret1, value] }
'''
nested_templ = '''
heat_template_version: 2013-05-23
resources:
secret2:
type: My::Secret
outputs:
value:
value: { get_attr: [secret2, value] }
'''
env_templ = '''
resource_registry:
"My::Secret": "OS::Heat::RandomString"
"My::NestedSecret": nested.yaml
'''
env = environment.Environment()
env.load(yaml.load(env_templ))
templ = parser.Template(template_format.parse(main_templ),
files={'nested.yaml': nested_templ})
stack = parser.Stack(utils.dummy_context(),
utils.random_name(),
templ, env=env)
stack.store()
stack.create()
self.assertEqual((stack.CREATE, stack.COMPLETE), stack.state)
def test_no_infinite_recursion(self):
"""Prove that we can override a python resource.
And use that resource within the template resource.
"""
main_templ = '''
heat_template_version: 2013-05-23
resources:
secret2:
type: OS::Heat::RandomString
outputs:
secret1:
value: { get_attr: [secret1, value] }
'''
nested_templ = '''
heat_template_version: 2013-05-23
resources:
secret2:
type: OS::Heat::RandomString
outputs:
value:
value: { get_attr: [secret2, value] }
'''
env_templ = '''
resource_registry:
"OS::Heat::RandomString": nested.yaml
'''
env = environment.Environment()
env.load(yaml.load(env_templ))
templ = parser.Template(template_format.parse(main_templ),
files={'nested.yaml': nested_templ})
stack = parser.Stack(utils.dummy_context(),
utils.random_name(),
templ, env=env)
stack.store()
stack.create()
self.assertEqual((stack.CREATE, stack.COMPLETE), stack.state)
class ProviderTemplateUpdateTest(HeatTestCase):
main_template = '''
HeatTemplateFormatVersion: '2012-12-12'

View File

@ -136,7 +136,7 @@ class StackResourceTest(HeatTestCase):
self.stack = self.parent_resource.nested()
self.assertEqual({"foo": "bar"}, self.stack.t.files)
@mock.patch.object(stack_resource.environment, 'Environment')
@mock.patch.object(stack_resource.StackResource, '_nested_environment')
@mock.patch.object(stack_resource.parser, 'Template')
@mock.patch.object(stack_resource.parser, 'Stack')
def test_preview_with_implemented_child_resource(self, mock_stack_class,
@ -488,7 +488,7 @@ class StackResourceTest(HeatTestCase):
parser.Template(self.templ, files={}).AndReturn(templ)
self.m.StubOutWithMock(environment, 'Environment')
environment.Environment({"KeyName": "test"}).AndReturn(env)
environment.Environment().AndReturn(env)
self.m.StubOutWithMock(parser, 'Stack')
parser.Stack(ctx, phy_id, templ, env, timeout_mins=None,