Allow lists and strings for Json parameters via provider resources

Currently we have a somewhat bogus internal equivalence between the
"json" parameter type and the MAP property type.  This is a problem
when exposing json parameters via provider resources, because the
schema of the exposed property does not exactly match the capabilities
of the underlying parameter.

Specifically, json parameters can take strings (json serialized list or
map), and also lists and maps directly.  Currently, we can only pass
maps, which means some data, for example a list of maps (which is possible
when interacting directly with a json parameter) is not possible when
using the same parameter abstracted via a TemplateResource.

To work around this add a flag which relaxes the type rules and enables
some coercion of map values, only when they're going via a schema derived
from a json parameter.

Change-Id: I8eff36c4343a07b644b84aa9f2f74eceeb62b4a9
Closes-Bug: #1420196
This commit is contained in:
Steven Hardy 2015-02-18 16:53:43 +00:00
parent 1dc46db686
commit 413fde3247
2 changed files with 32 additions and 2 deletions

View File

@ -13,6 +13,7 @@
import collections
from oslo_serialization import jsonutils
import six
from heat.common import exception
@ -58,13 +59,15 @@ class Schema(constr.Schema):
implemented=True,
update_allowed=False,
immutable=False,
support_status=support.SupportStatus()):
support_status=support.SupportStatus(),
allow_conversion=False):
super(Schema, self).__init__(data_type, description, default,
schema, required, constraints)
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
self.validate()
@ -146,6 +149,13 @@ class Schema(constr.Schema):
param.BOOLEAN: cls.BOOLEAN
}
# allow_conversion allows slightly more flexible type conversion
# where property->parameter types don't align, primarily when
# a json parameter value is passed via a Map property, which requires
# some coercion to pass strings or lists (which are both valid for
# Json parameters but not for Map properties).
allow_conversion = param.type == param.MAP
# make update_allowed true by default on TemplateResources
# as the template should deal with this.
return cls(data_type=param_type_map.get(param.type, cls.MAP),
@ -153,7 +163,8 @@ class Schema(constr.Schema):
required=param.required,
constraints=param.constraints,
update_allowed=True,
immutable=False)
immutable=False,
allow_conversion=allow_conversion)
def allowed_param_prop_type(self):
"""
@ -269,6 +280,14 @@ class Property(object):
if value is None:
value = self.has_default() and self.default() or {}
if not isinstance(value, collections.Mapping):
# This is to handle passing Lists via Json parameters exposed
# via a provider resource, in particular lists-of-dicts which
# cannot be handled correctly via comma_delimited_list
if self.schema.allow_conversion:
if isinstance(value, six.string_types):
return value
elif isinstance(value, collections.Sequence):
return jsonutils.dumps(value)
raise TypeError(_('"%s" is not a map') % value)
return dict(self._get_children(six.iteritems(value),

View File

@ -11,6 +11,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo_serialization import jsonutils
import six
import testtools
@ -468,6 +469,7 @@ class PropertySchemaTest(testtools.TestCase):
self.assertTrue(schema.required)
self.assertIsNone(schema.default)
self.assertEqual(0, len(schema.constraints))
self.assertFalse(schema.allow_conversion)
def test_from_number_param_min(self):
default = "42"
@ -546,6 +548,7 @@ class PropertySchemaTest(testtools.TestCase):
self.assertIsNone(schema.default)
self.assertFalse(schema.required)
self.assertEqual(1, len(schema.constraints))
self.assertFalse(schema.allow_conversion)
allowed_constraint = schema.constraints[0]
@ -563,6 +566,7 @@ class PropertySchemaTest(testtools.TestCase):
self.assertEqual(properties.Schema.LIST, schema.type)
self.assertIsNone(schema.default)
self.assertFalse(schema.required)
self.assertFalse(schema.allow_conversion)
def test_from_json_param(self):
param = parameters.Schema.from_dict('name', {
@ -575,6 +579,7 @@ class PropertySchemaTest(testtools.TestCase):
self.assertEqual(properties.Schema.MAP, schema.type)
self.assertIsNone(schema.default)
self.assertFalse(schema.required)
self.assertTrue(schema.allow_conversion)
class PropertyTest(testtools.TestCase):
@ -862,6 +867,12 @@ class PropertyTest(testtools.TestCase):
p = properties.Property({'Type': 'Map'})
self.assertRaises(TypeError, p.get_value, ['foo'])
def test_map_allow_conversion(self):
p = properties.Property({'Type': 'Map'})
p.schema.allow_conversion = True
self.assertEqual('foo', p.get_value('foo'))
self.assertEqual(jsonutils.dumps(['foo']), p.get_value(['foo']))
def test_map_schema_good(self):
map_schema = {'valid': {'Type': 'Boolean'}}
p = properties.Property({'Type': 'Map', 'Schema': map_schema})