[jsonschema] Require specifying `additionalProperties` explicitly

Validation of objects via jsonschema become meaningless if there is no
`additionalProperties: False`. In case of complex schemas, it is easy to
miss this property in some child property.

Let's require always specifing additionalProperties to simplify the life
of reviewers and do not miss anything.

Change-Id: I49693a7968e820010751909a48f06f3f784c5721
This commit is contained in:
Andrey Kurilin 2018-02-02 15:15:27 +02:00
parent dbdb175270
commit 8a95c29f55
13 changed files with 63 additions and 27 deletions

View File

@ -79,7 +79,8 @@ class CeilometerSampleGenerator(context.Context):
"created_at": {
"type": "string"
}
}
},
"additionalProperties": False
}
},
"batch_size": {

View File

@ -73,12 +73,15 @@ class HeatDataplane(context.Context):
},
"files": {
"type": "object",
"additionalProperties": True
},
"parameters": {
"type": "object",
"additionalProperties": True
},
"context_parameters": {
"type": "object",
"additionalProperties": True
},
},
"additionalProperties": False

View File

@ -39,7 +39,8 @@ class EC2ServerGenerator(context.Context):
"name": {
"type": "string"
}
}
},
"additionalProperties": False
},
"flavor": {
"type": "object",
@ -47,7 +48,8 @@ class EC2ServerGenerator(context.Context):
"name": {
"type": "string"
}
}
},
"additionalProperties": False
},
"servers_per_tenant": {
"type": "integer",

View File

@ -70,12 +70,13 @@ class ShareNetworks(context.Context):
"properties": {
"use_share_networks": {
"type": "boolean",
"description": "specifies whether manila should use share "
"description": "Specifies whether manila should use share "
"networks for share creation or not."},
"share_networks": {
"type": "object",
"description": SHARE_NETWORKS_ARG_DESCR
"description": SHARE_NETWORKS_ARG_DESCR,
"additionalProperties": True
},
},
"additionalProperties": False

View File

@ -48,7 +48,8 @@ class MonascaMetricGenerator(context.Context):
"url": {
"type": "string"
}
}
},
"additionalProperties": False
},
"metrics_per_tenant": {
"type": "integer",
@ -65,7 +66,8 @@ class MonascaMetricGenerator(context.Context):
"value_meta_value": {
"type": "string"
}
}
},
"additionalProperties": False
}
}
},

View File

@ -48,7 +48,8 @@ class Router(context.Context):
"properties": {
"network_id": {"type": "string"},
"enable_snat": {"type": "boolean"}
}
},
"additionalProperties": False
},
"network_id": {
"description": "Network ID",
@ -62,7 +63,8 @@ class Router(context.Context):
"properties": {
"ip_address": {"type": "string"},
"subnet_id": {"type": "string"}
}
},
"additionalProperties": False,
}
},
"distributed": {

View File

@ -32,7 +32,8 @@ class Lbaas(context.Context):
"$schema": consts.JSON_SCHEMA,
"properties": {
"pool": {
"type": "object"
"type": "object",
"additionalProperties": True
},
"lbaas_version": {
"type": "integer",

View File

@ -38,14 +38,16 @@ class ServerGenerator(context.Context):
"type": "object",
"properties": {
"name": {"type": "string"}
}
},
"additionalProperties": False
},
"flavor": {
"description": "Name of flavor to boot server(s) with.",
"type": "object",
"properties": {
"name": {"type": "string"}
}
},
"additionalProperties": False
},
"servers_per_tenant": {
"description": "Number of servers to boot in each Tenant.",
@ -60,11 +62,18 @@ class ServerGenerator(context.Context):
"type": "array",
"description": "List of networks to attach to server.",
"items": {"oneOf": [
{"type": "object",
"properties": {"net-id": {"type": "string"}},
"description": "Network ID in a format like OpenStack API"
" expects to see."},
{"type": "string", "description": "Network ID."}]},
{
"type": "object",
"properties": {"net-id": {"type": "string"}},
"description": "Network ID in a format like OpenStack "
"API expects to see.",
"additionalProperties": False
},
{
"type": "string",
"description": "Network ID."
}
]},
"minItems": 1
}
},

View File

@ -77,10 +77,12 @@ class SaharaCluster(context.Context):
}
},
"node_configs": {
"type": "object"
"type": "object",
"additionalProperties": True
},
"cluster_configs": {
"type": "object"
"type": "object",
"additionalProperties": True
},
"enable_anti_affinity": {
"type": "boolean"

View File

@ -34,6 +34,7 @@ class ProfilesGenerator(context.Context):
},
"properties": {
"type": "object",
"additionalProperties": True,
}
},
"additionalProperties": False,

View File

@ -55,7 +55,8 @@ class BaseCustomImageGenerator(context.Context):
"name": {
"type": "string"
}
}
},
"additionalProperties": False
},
"flavor": {
"type": "object",
@ -63,7 +64,8 @@ class BaseCustomImageGenerator(context.Context):
"name": {
"type": "string"
}
}
},
"additionalProperties": False
},
"username": {
"type": "string"

View File

@ -49,7 +49,8 @@ class AuditTemplateGenerator(context.Context):
"name": {
"type": "string"
}
}
},
"additionalProperties": False
},
"strategy": {
"type": "object",
@ -57,9 +58,11 @@ class AuditTemplateGenerator(context.Context):
"name": {
"type": "string"
}
}
},
"additionalProperties": False
},
},
"additionalProperties": False,
},
}
},

View File

@ -13,6 +13,7 @@
# under the License.
import copy
import json
from rally.common.plugin import plugin
from rally import plugins
@ -35,9 +36,10 @@ class ConfigSchemasTestCase(test.TestCase):
def fail(self, p, schema, msg):
super(ConfigSchemasTestCase, self).fail(
"Config schema of plugin '%s' (%s) is invalid. %s "
"(Schema: %s)" % (p.get_name(),
"Schema: \n%s" % (p.get_name(),
"%s.%s" % (p.__module__, p.__name__),
msg, schema))
msg,
json.dumps(schema, indent=3)))
def _check_anyOf_or_oneOf(self, p, schema, definitions):
if "anyOf" in schema or "oneOf" in schema:
@ -61,6 +63,11 @@ class ConfigSchemasTestCase(test.TestCase):
if unexpected_keys:
self.fail(p, schema, ("Found unexpected key(s) for object type: "
"%s." % ", ".join(unexpected_keys)))
if "additionalProperties" not in schema:
self.fail(p, schema,
"'additionalProperties' is required field for objects. "
"Specify `'additionalProperties': True` explicitly to "
"accept not validated properties.")
if "patternProperties" in schema:
if "properties" in schema:
@ -147,7 +154,7 @@ class ConfigSchemasTestCase(test.TestCase):
pass
elif schema == {}:
# NOTE(andreykurilin): an empty dict means that the user can
# transmit whatever he want in whatever he want format. It is
# transmit whatever he wants in whatever he wants format. It is
# not the case which we want to support.
self.fail(p, schema, "Empty schema is not allowed.")
elif "$ref" in schema:
@ -161,7 +168,7 @@ class ConfigSchemasTestCase(test.TestCase):
@plugins.ensure_plugins_are_loaded
def test_schema_is_valid(self):
for p in plugin.Plugin.get_all():
if not hasattr(p, "CONFIG_SCHEMA"):
if not hasattr(p, "CONFIG_SCHEMA") or "tests.unit" in p.__module__:
continue
# allow only top level definitions
definitions = p.CONFIG_SCHEMA.get("definitions", {})