Update jsonschema validator (#226)
* Update jsonschema validator Update jsonschema validator to support newer drafts. It now supports drafts 2019-09 and 2020-12. The logic for choosing the jsonschema validator class has changed so that the following enhancements are available: - When no draft is explicitly specified we now use use the validator draft that is specified in the "$schema" field of the criteria. This is done by the jsonschema module by default and should support possible future drafts without any changes to this code. - Optionally allow to disable format checks in the code. As format checks are not required by the spec there might be situations where people want to disable them. * Update requirements.txt * Skip all tests which dependas on jsonschema 4.5 * jsonschema: Refactor code to support python 3.6 * Fix jsonschema requirements for python<3.7 * jsonschema: Update code for compatibility * Better documentation and error handling for missing schema specifications * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Ashwini Mhatre <amhatre@redhat.com> Co-authored-by: ashwini-mhatre <mashu97@gmail.com>pull/254/head
parent
ed6555526d
commit
c2231a6b20
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
minor_changes:
|
||||
- validate - Add support for JSON Schema draft 2019-09 and 2020-12 as well as automatically choosing the draft
|
||||
from the `$schema` field of the criteria.
|
||||
- validate - Add option `check_format` for the jsonschema engine to disable JSON Schema format checking.
|
|
@ -13,4 +13,4 @@ issues: https://github.com/ansible-collections/ansible.utils/issues
|
|||
tags: [linux, networking, security, cloud, utilities, data, validation, utils]
|
||||
# NOTE(pabelanger): We create an empty version key to keep ansible-galaxy
|
||||
# happy. We dynamically inject version info based on git information.
|
||||
version: 2.9.1-dev
|
||||
version: 2.10.0-dev
|
||||
|
|
|
@ -22,17 +22,29 @@ DOCUMENTATION = """
|
|||
description:
|
||||
- This option provides the jsonschema specification that should be used
|
||||
for the validating the data. The I(criteria) option in the validate
|
||||
plugin should follow the specification as mentioned by this option
|
||||
default: draft7
|
||||
plugin should follow the specification as mentioned by this option.
|
||||
If this option is not specified, jsonschema will use the best validator
|
||||
for the I($schema) field in the criteria. Specifications 2019-09 and
|
||||
2020-12 are only available from jsonschema version 4.0 onwards.
|
||||
choices:
|
||||
- draft3
|
||||
- draft4
|
||||
- draft6
|
||||
- draft7
|
||||
- 2019-09
|
||||
- 2020-12
|
||||
env:
|
||||
- name: ANSIBLE_VALIDATE_JSONSCHEMA_DRAFT
|
||||
vars:
|
||||
- name: ansible_validate_jsonschema_draft
|
||||
check_format:
|
||||
description: If enabled, validate the I(format) specification in the criteria.
|
||||
type: bool
|
||||
default: true
|
||||
env:
|
||||
- name: ANSIBLE_VALIDATE_JSONSCHEMA_CHECK_FORMAT
|
||||
vars:
|
||||
- name: ansible_validate_jsonschema_check_format
|
||||
notes:
|
||||
- The value of I(data) option should be either a valid B(JSON) object or a B(JSON) string.
|
||||
- The value of I(criteria) should be B(list) of B(dict) or B(list) of B(strings) and each
|
||||
|
@ -45,11 +57,14 @@ from ansible.errors import AnsibleError
|
|||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.six import string_types
|
||||
from ansible.utils.display import Display
|
||||
|
||||
from ansible_collections.ansible.utils.plugins.module_utils.common.utils import to_list
|
||||
from ansible_collections.ansible.utils.plugins.plugin_utils.base.validate import ValidateBase
|
||||
|
||||
|
||||
display = Display()
|
||||
|
||||
# PY2 compatibility for JSONDecodeError
|
||||
try:
|
||||
from json.decoder import JSONDecodeError
|
||||
|
@ -58,6 +73,7 @@ except ImportError:
|
|||
|
||||
try:
|
||||
import jsonschema
|
||||
import jsonschema.validators
|
||||
|
||||
HAS_JSONSCHEMA = True
|
||||
except ImportError:
|
||||
|
@ -79,6 +95,34 @@ def json_path(absolute_path):
|
|||
|
||||
|
||||
class Validate(ValidateBase):
|
||||
# All available schema versions with the format_check and validator class names.
|
||||
_JSONSCHEMA_DRAFTS = {
|
||||
"draft3": {
|
||||
"validator_name": "Draft3Validator",
|
||||
"format_checker_name": "draft3_format_checker",
|
||||
},
|
||||
"draft4": {
|
||||
"validator_name": "Draft4Validator",
|
||||
"format_checker_name": "draft4_format_checker",
|
||||
},
|
||||
"draft6": {
|
||||
"validator_name": "Draft6Validator",
|
||||
"format_checker_name": "draft6_format_checker",
|
||||
},
|
||||
"draft7": {
|
||||
"validator_name": "Draft7Validator",
|
||||
"format_checker_name": "draft7_format_checker",
|
||||
},
|
||||
"2019-09": {
|
||||
"validator_name": "Draft201909Validator",
|
||||
"format_checker_name": "draft201909_format_checker",
|
||||
},
|
||||
"2020-12": {
|
||||
"validator_name": "Draft202012Validator",
|
||||
"format_checker_name": "draft202012_format_checker",
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _check_reqs():
|
||||
"""Check the prerequisites are installed for jsonschema
|
||||
|
@ -126,6 +170,28 @@ class Validate(ValidateBase):
|
|||
)
|
||||
raise AnsibleError(msg)
|
||||
|
||||
def _check_drafts(self):
|
||||
"""For every possible draft check if our jsonschema version supports it and exchange the class names with
|
||||
the actual classes. If it is not supported the draft is removed from the list.
|
||||
"""
|
||||
for draft in list(self._JSONSCHEMA_DRAFTS.keys()):
|
||||
draft_config = self._JSONSCHEMA_DRAFTS[draft]
|
||||
try:
|
||||
validator_class = getattr(jsonschema, draft_config["validator_name"])
|
||||
except AttributeError:
|
||||
display.vvv(
|
||||
'jsonschema draft "{draft}" not supported in this version'.format(draft=draft),
|
||||
)
|
||||
del self._JSONSCHEMA_DRAFTS[draft]
|
||||
continue
|
||||
draft_config["validator"] = validator_class
|
||||
try:
|
||||
format_checker_class = validator_class.FORMAT_CHECKER
|
||||
except AttributeError:
|
||||
# Older jsonschema version
|
||||
format_checker_class = getattr(jsonschema, draft_config["format_checker_name"])
|
||||
draft_config["format_checker"] = format_checker_class
|
||||
|
||||
def validate(self):
|
||||
"""Std entry point for a validate execution
|
||||
|
||||
|
@ -141,6 +207,7 @@ class Validate(ValidateBase):
|
|||
"""
|
||||
self._check_reqs()
|
||||
self._check_args()
|
||||
self._check_drafts()
|
||||
|
||||
try:
|
||||
self._validate_jsonschema()
|
||||
|
@ -153,30 +220,45 @@ class Validate(ValidateBase):
|
|||
error_messages = None
|
||||
|
||||
draft = self._get_sub_plugin_options("draft")
|
||||
check_format = self._get_sub_plugin_options("check_format")
|
||||
error_messages = []
|
||||
|
||||
for criteria in self._criteria:
|
||||
if draft == "draft3":
|
||||
validator = jsonschema.Draft3Validator(
|
||||
criteria,
|
||||
format_checker=jsonschema.draft3_format_checker,
|
||||
)
|
||||
elif draft == "draft4":
|
||||
validator = jsonschema.Draft4Validator(
|
||||
criteria,
|
||||
format_checker=jsonschema.draft4_format_checker,
|
||||
)
|
||||
elif draft == "draft6":
|
||||
validator = jsonschema.Draft6Validator(
|
||||
criteria,
|
||||
format_checker=jsonschema.draft6_format_checker,
|
||||
)
|
||||
else:
|
||||
validator = jsonschema.Draft7Validator(
|
||||
criteria,
|
||||
format_checker=jsonschema.draft7_format_checker,
|
||||
format_checker = None
|
||||
validator_class = None
|
||||
if draft is not None:
|
||||
try:
|
||||
validator_class = self._JSONSCHEMA_DRAFTS[draft]["validator"]
|
||||
except KeyError:
|
||||
display.warning(
|
||||
'No validator available for "{draft}", falling back to autodetection. A newer version of jsonschema might support this draft.'.format(
|
||||
draft=draft,
|
||||
),
|
||||
)
|
||||
if validator_class is None:
|
||||
# Either no draft was specified or specified draft has no validator class
|
||||
# in installed jsonschema version. Do autodetection instead.
|
||||
validator_class = jsonschema.validators.validator_for(criteria)
|
||||
|
||||
if check_format:
|
||||
try:
|
||||
format_checker = validator_class.FORMAT_CHECKER
|
||||
except AttributeError:
|
||||
# TODO: Remove when Python 3.6 support is dropped.
|
||||
# On jsonschema<4.5, there is no connection between a validator and the correct format checker.
|
||||
# So we iterate through our known list of validators and if one matches the current class
|
||||
# we use the format_checker from that validator.
|
||||
for draft, draft_config in self._JSONSCHEMA_DRAFTS.items():
|
||||
if validator_class == draft_config["validator"]:
|
||||
display.vvv(
|
||||
"Using format_checker for {draft} validator".format(draft=draft),
|
||||
)
|
||||
format_checker = draft_config["format_checker"]
|
||||
break
|
||||
else:
|
||||
display.warning("jsonschema format checks not available")
|
||||
|
||||
validator = validator_class(criteria, format_checker=format_checker)
|
||||
validation_errors = sorted(validator.iter_errors(self._data), key=lambda e: e.path)
|
||||
|
||||
if validation_errors:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
jsonschema ; python_version >= '2.7'
|
||||
jsonschema
|
||||
netaddr
|
||||
ttp
|
||||
textfsm
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
- ansible.builtin.assert:
|
||||
that:
|
||||
- "'value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0' in result.msg"
|
||||
- "'value of draft must be one of: draft3, draft4, draft6, draft7, 2019-09, 2020-12, got: draft0' in result.msg"
|
||||
|
||||
- name: invalid engine value
|
||||
ansible.builtin.set_fact:
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
|
||||
- ansible.builtin.assert:
|
||||
that:
|
||||
- "'value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0' in result.msg"
|
||||
- "'value of draft must be one of: draft3, draft4, draft6, draft7, 2019-09, 2020-12, got: draft0' in result.msg"
|
||||
|
||||
- name: test invalid plugin configuration option, passed as task varaible
|
||||
ansible.builtin.set_fact:
|
||||
|
@ -43,7 +43,7 @@
|
|||
|
||||
- ansible.builtin.assert:
|
||||
that:
|
||||
- "'value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0' in result.msg"
|
||||
- "'value of draft must be one of: draft3, draft4, draft6, draft7, 2019-09, 2020-12, got: draft0' in result.msg"
|
||||
|
||||
- name: invalid engine value
|
||||
ansible.builtin.set_fact:
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
that:
|
||||
- "'errors' not in result"
|
||||
- "result['failed'] == true"
|
||||
- "'value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0' in result.msg"
|
||||
- "'value of draft must be one of: draft3, draft4, draft6, draft7, 2019-09, 2020-12, got: draft0' in result.msg"
|
||||
|
||||
- name: invalid engine value
|
||||
ansible.utils.validate:
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
- ansible.builtin.assert:
|
||||
that:
|
||||
- "'value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0' in result.msg"
|
||||
- "'value of draft must be one of: draft3, draft4, draft6, draft7, 2019-09, 2020-12, got: draft0' in result.msg"
|
||||
|
||||
- name: invalid engine value
|
||||
ansible.builtin.set_fact:
|
||||
|
|
|
@ -186,7 +186,7 @@ class TestValidate(unittest.TestCase):
|
|||
|
||||
result = self._plugin.run(task_vars={"ansible_validate_jsonschema_draft": "draft0"})
|
||||
self.assertIn(
|
||||
"value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0",
|
||||
"value of draft must be one of: draft3, draft4, draft6, draft7, 2019-09, 2020-12, got: draft0",
|
||||
result["msg"],
|
||||
)
|
||||
|
||||
|
@ -285,3 +285,15 @@ class TestValidate(unittest.TestCase):
|
|||
|
||||
result = self._plugin.run(task_vars=None)
|
||||
self.assertIn("Validation errors were found", result["msg"])
|
||||
|
||||
def test_support_for_disabled_format_with_invalid_data(self):
|
||||
"""Check passing valid data as per criteria"""
|
||||
|
||||
self._plugin._task.args = {
|
||||
"engine": "ansible.utils.jsonschema",
|
||||
"data": IN_VALID_DATA,
|
||||
"criteria": CRITERIA_FORMAT_SUPPORT_CHECK,
|
||||
}
|
||||
|
||||
result = self._plugin.run(task_vars=dict(ansible_validate_jsonschema_check_format=False))
|
||||
self.assertIn("All checks passed", result["msg"])
|
||||
|
|
|
@ -150,7 +150,7 @@ class TestValidate(unittest.TestCase):
|
|||
with self.assertRaises(AnsibleFilterError) as error:
|
||||
validate(*args, **kwargs)
|
||||
self.assertIn(
|
||||
"value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0",
|
||||
"value of draft must be one of: draft3, draft4, draft6, draft7, 2019-09, 2020-12, got: draft0",
|
||||
str(error.exception),
|
||||
)
|
||||
|
||||
|
|
|
@ -148,7 +148,7 @@ class TestValidate(unittest.TestCase):
|
|||
with self.assertRaises(AnsibleError) as error:
|
||||
validate(*args, **kwargs)
|
||||
self.assertIn(
|
||||
"value of draft must be one of: draft3, draft4, draft6, draft7, got: draft0",
|
||||
"value of draft must be one of: draft3, draft4, draft6, draft7, 2019-09, 2020-12, got: draft0",
|
||||
str(error.exception),
|
||||
)
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
jsonschema ; python_version >= '2.7'
|
||||
jsonschema
|
||||
netaddr
|
||||
textfsm
|
||||
ttp
|
||||
|
|
Loading…
Reference in New Issue