Immutable parameters
Add a new "immutable" boolean field to the parameters section in a HOT template. A value of True will result in the engine rejecting stack-updates that include changes to that parameter. When not specified in the template, "immutable" defaults to False to ensure backwards compatibility with old templates. blueprint: immutable-parameters Change-Id: If17df0b97e96ff2a1926451634e4744d6c3e26f1
This commit is contained in:
parent
834ac4a041
commit
98262c10d8
|
@ -133,6 +133,16 @@ class InvalidTemplateParameter(HeatException):
|
|||
msg_fmt = _("The Parameter (%(key)s) has no attributes.")
|
||||
|
||||
|
||||
class ImmutableParameterModified(HeatException):
|
||||
msg_fmt = _("The following parameters are immutable and may not be "
|
||||
"updated: %(keys)s")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if args:
|
||||
kwargs.update({'keys': ", ".join(args)})
|
||||
super(ImmutableParameterModified, self).__init__(**kwargs)
|
||||
|
||||
|
||||
class InvalidTemplateAttribute(HeatException):
|
||||
msg_fmt = _("The Referenced Attribute (%(resource)s %(key)s)"
|
||||
" is incorrect.")
|
||||
|
|
|
@ -64,8 +64,10 @@ class Schema(collections.Mapping):
|
|||
|
||||
KEYS = (
|
||||
TYPE, DESCRIPTION, DEFAULT, SCHEMA, REQUIRED, CONSTRAINTS,
|
||||
IMMUTABLE,
|
||||
) = (
|
||||
'type', 'description', 'default', 'schema', 'required', 'constraints',
|
||||
'immutable',
|
||||
)
|
||||
|
||||
# Keywords for data types; each Schema subclass can define its respective
|
||||
|
@ -88,7 +90,8 @@ class Schema(collections.Mapping):
|
|||
|
||||
def __init__(self, data_type, description=None,
|
||||
default=None, schema=None,
|
||||
required=False, constraints=None, label=None):
|
||||
required=False, constraints=None, label=None,
|
||||
immutable=False):
|
||||
self._len = None
|
||||
self.label = label
|
||||
self.type = data_type
|
||||
|
@ -102,6 +105,7 @@ class Schema(collections.Mapping):
|
|||
|
||||
self.description = description
|
||||
self.required = required
|
||||
self.immutable = immutable
|
||||
|
||||
if isinstance(schema, type(self)):
|
||||
if self.type != self.LIST:
|
||||
|
|
|
@ -33,10 +33,10 @@ class HOTParamSchema(parameters.Schema):
|
|||
|
||||
KEYS = (
|
||||
TYPE, DESCRIPTION, DEFAULT, SCHEMA, CONSTRAINTS,
|
||||
HIDDEN, LABEL
|
||||
HIDDEN, LABEL, IMMUTABLE
|
||||
) = (
|
||||
'type', 'description', 'default', 'schema', 'constraints',
|
||||
'hidden', 'label'
|
||||
'hidden', 'label', 'immutable'
|
||||
)
|
||||
|
||||
# For Parameters the type name for Schema.LIST is comma_delimited_list
|
||||
|
@ -105,7 +105,8 @@ class HOTParamSchema(parameters.Schema):
|
|||
default=schema_dict.get(HOTParamSchema.DEFAULT),
|
||||
constraints=list(constraints()),
|
||||
hidden=schema_dict.get(HOTParamSchema.HIDDEN, False),
|
||||
label=schema_dict.get(HOTParamSchema.LABEL))
|
||||
label=schema_dict.get(HOTParamSchema.LABEL),
|
||||
immutable=schema_dict.get(HOTParamSchema.IMMUTABLE, False))
|
||||
|
||||
|
||||
class HOTParameters(parameters.Parameters):
|
||||
|
|
|
@ -39,10 +39,11 @@ class Schema(constr.Schema):
|
|||
"""Parameter schema."""
|
||||
|
||||
KEYS = (
|
||||
TYPE, DESCRIPTION, DEFAULT, SCHEMA, CONSTRAINTS, HIDDEN, LABEL
|
||||
TYPE, DESCRIPTION, DEFAULT, SCHEMA, CONSTRAINTS, HIDDEN,
|
||||
LABEL, IMMUTABLE
|
||||
) = (
|
||||
'Type', 'Description', 'Default', 'Schema', 'Constraints', 'NoEcho',
|
||||
'Label'
|
||||
'Label', 'Immutable'
|
||||
)
|
||||
|
||||
PARAMETER_KEYS = PARAMETER_KEYS
|
||||
|
@ -56,14 +57,15 @@ class Schema(constr.Schema):
|
|||
)
|
||||
|
||||
def __init__(self, data_type, description=None, default=None, schema=None,
|
||||
constraints=None, hidden=False, label=None):
|
||||
constraints=None, hidden=False, label=None, immutable=False):
|
||||
super(Schema, self).__init__(data_type=data_type,
|
||||
description=description,
|
||||
default=default,
|
||||
schema=schema,
|
||||
required=default is None,
|
||||
constraints=constraints,
|
||||
label=label)
|
||||
label=label,
|
||||
immutable=immutable)
|
||||
self.hidden = hidden
|
||||
|
||||
# Schema class validates default value for lists assuming list type. For
|
||||
|
@ -493,6 +495,8 @@ class Parameters(collections.Mapping):
|
|||
self.params = dict((p.name,
|
||||
p) for p in itertools.chain(pseudo_parameters,
|
||||
user_parameters))
|
||||
self.non_pseudo_param_keys = [p for p in self.params if p not in
|
||||
self.PSEUDO_PARAMETERS]
|
||||
|
||||
for pd in six.iterkeys(param_defaults):
|
||||
if pd in self.params:
|
||||
|
@ -585,3 +589,21 @@ class Parameters(collections.Mapping):
|
|||
'ap-southeast-1',
|
||||
'ap-northeast-1']
|
||||
)]))
|
||||
|
||||
def immutable_params_modified(self, new_parameters, input_params):
|
||||
# A parameter must have been present in the old stack for its
|
||||
# immutability to be enforced
|
||||
common_params = list(set(new_parameters.non_pseudo_param_keys)
|
||||
& set(self.non_pseudo_param_keys))
|
||||
invalid_params = []
|
||||
for param in common_params:
|
||||
old_value = self.params[param]
|
||||
if param in input_params:
|
||||
new_value = input_params[param]
|
||||
else:
|
||||
new_value = new_parameters[param]
|
||||
immutable = new_parameters.params[param].schema.immutable
|
||||
if immutable and old_value.value() != new_value:
|
||||
invalid_params.append(param)
|
||||
if invalid_params:
|
||||
return invalid_params
|
||||
|
|
|
@ -62,10 +62,10 @@ class Schema(constr.Schema):
|
|||
support_status=support.SupportStatus(),
|
||||
allow_conversion=False):
|
||||
super(Schema, self).__init__(data_type, description, default,
|
||||
schema, required, constraints)
|
||||
schema, required, constraints,
|
||||
immutable=immutable)
|
||||
self.implemented = implemented
|
||||
self.update_allowed = update_allowed
|
||||
self.immutable = immutable
|
||||
self.support_status = support_status
|
||||
self.allow_conversion = allow_conversion
|
||||
# validate structural correctness of schema itself
|
||||
|
|
|
@ -851,6 +851,12 @@ class EngineService(service.Service):
|
|||
current_kwargs.update(common_params)
|
||||
updated_stack = parser.Stack(cnxt, stack_name, tmpl,
|
||||
**current_kwargs)
|
||||
|
||||
invalid_params = current_stack.parameters.immutable_params_modified(
|
||||
updated_stack.parameters, params)
|
||||
if invalid_params:
|
||||
raise exception.ImmutableParameterModified(*invalid_params)
|
||||
|
||||
self.resource_enforcer.enforce_stack(updated_stack)
|
||||
updated_stack.parameters.set_stack_id(current_stack.identifier())
|
||||
|
||||
|
|
|
@ -675,6 +675,112 @@ class ServiceStackUpdateTest(common.HeatTestCase):
|
|||
self.assertIn("PATCH update to non-COMPLETE stack",
|
||||
six.text_type(ex.exc_info[1]))
|
||||
|
||||
def test_update_immutable_parameter_disallowed(self):
|
||||
|
||||
template = '''
|
||||
heat_template_version: 2014-10-16
|
||||
parameters:
|
||||
param1:
|
||||
type: string
|
||||
immutable: true
|
||||
default: foo
|
||||
'''
|
||||
|
||||
self.ctx = utils.dummy_context(password=None)
|
||||
stack_name = 'test_update_immutable_parameters'
|
||||
params = {}
|
||||
old_stack = tools.get_stack(stack_name, self.ctx,
|
||||
template=template)
|
||||
sid = old_stack.store()
|
||||
old_stack.set_stack_user_project_id('1234')
|
||||
s = stack_object.Stack.get_by_id(self.ctx, sid)
|
||||
|
||||
# prepare mocks
|
||||
self.patchobject(self.man, '_get_stack', return_value=s)
|
||||
self.patchobject(stack, 'Stack', return_value=old_stack)
|
||||
self.patchobject(stack.Stack, 'load', return_value=old_stack)
|
||||
self.patchobject(templatem, 'Template', return_value=old_stack.t)
|
||||
self.patchobject(environment, 'Environment',
|
||||
return_value=old_stack.env)
|
||||
|
||||
params = {'param1': 'bar'}
|
||||
exc = self.assertRaises(dispatcher.ExpectedException,
|
||||
self.man.update_stack,
|
||||
self.ctx, old_stack.identifier(),
|
||||
templatem.Template(template), params,
|
||||
None, {})
|
||||
self.assertEqual(exception.ImmutableParameterModified, exc.exc_info[0])
|
||||
self.assertEqual('The following parameters are immutable and may not '
|
||||
'be updated: param1', exc.exc_info[1].message)
|
||||
|
||||
def test_update_mutable_parameter_allowed(self):
|
||||
|
||||
template = '''
|
||||
heat_template_version: 2014-10-16
|
||||
parameters:
|
||||
param1:
|
||||
type: string
|
||||
immutable: false
|
||||
default: foo
|
||||
'''
|
||||
|
||||
self.ctx = utils.dummy_context(password=None)
|
||||
stack_name = 'test_update_immutable_parameters'
|
||||
params = {}
|
||||
old_stack = tools.get_stack(stack_name, self.ctx,
|
||||
template=template)
|
||||
sid = old_stack.store()
|
||||
old_stack.set_stack_user_project_id('1234')
|
||||
s = stack_object.Stack.get_by_id(self.ctx, sid)
|
||||
|
||||
# prepare mocks
|
||||
self.patchobject(self.man, '_get_stack', return_value=s)
|
||||
self.patchobject(stack, 'Stack', return_value=old_stack)
|
||||
self.patchobject(stack.Stack, 'load', return_value=old_stack)
|
||||
self.patchobject(templatem, 'Template', return_value=old_stack.t)
|
||||
self.patchobject(environment, 'Environment',
|
||||
return_value=old_stack.env)
|
||||
|
||||
params = {'param1': 'bar'}
|
||||
result = self.man.update_stack(self.ctx, old_stack.identifier(),
|
||||
templatem.Template(template), params,
|
||||
None, {})
|
||||
self.assertEqual(s.id, result['stack_id'])
|
||||
|
||||
def test_update_immutable_parameter_same_value(self):
|
||||
|
||||
template = '''
|
||||
heat_template_version: 2014-10-16
|
||||
parameters:
|
||||
param1:
|
||||
type: string
|
||||
immutable: true
|
||||
default: foo
|
||||
'''
|
||||
|
||||
self.ctx = utils.dummy_context(password=None)
|
||||
stack_name = 'test_update_immutable_parameters'
|
||||
params = {}
|
||||
old_stack = tools.get_stack(stack_name, self.ctx,
|
||||
template=template)
|
||||
sid = old_stack.store()
|
||||
old_stack.set_stack_user_project_id('1234')
|
||||
s = stack_object.Stack.get_by_id(self.ctx, sid)
|
||||
|
||||
# prepare mocks
|
||||
self.patchobject(self.man, '_get_stack', return_value=s)
|
||||
self.patchobject(stack, 'Stack', return_value=old_stack)
|
||||
self.patchobject(stack.Stack, 'load', return_value=old_stack)
|
||||
self.patchobject(templatem, 'Template', return_value=old_stack.t)
|
||||
self.patchobject(environment, 'Environment',
|
||||
return_value=old_stack.env)
|
||||
|
||||
params = {'param1': 'foo'}
|
||||
result = self.man.update_stack(self.ctx, old_stack.identifier(),
|
||||
templatem.Template(template), params,
|
||||
None, {})
|
||||
self.assertEqual(s.id, result['stack_id'])
|
||||
|
||||
|
||||
class ServiceStackUpdatePreviewTest(common.HeatTestCase):
|
||||
|
||||
|
|
Loading…
Reference in New Issue