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
Sebastian Wiesinger 2023-04-05 17:01:12 +02:00 committed by GitHub
parent ed6555526d
commit c2231a6b20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 132 additions and 33 deletions

View File

@ -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.

View File

@ -13,4 +13,4 @@ issues: https://github.com/ansible-collections/ansible.utils/issues
tags: [linux, networking, security, cloud, utilities, data, validation, utils] tags: [linux, networking, security, cloud, utilities, data, validation, utils]
# NOTE(pabelanger): We create an empty version key to keep ansible-galaxy # NOTE(pabelanger): We create an empty version key to keep ansible-galaxy
# happy. We dynamically inject version info based on git information. # happy. We dynamically inject version info based on git information.
version: 2.9.1-dev version: 2.10.0-dev

View File

@ -22,17 +22,29 @@ DOCUMENTATION = """
description: description:
- This option provides the jsonschema specification that should be used - This option provides the jsonschema specification that should be used
for the validating the data. The I(criteria) option in the validate for the validating the data. The I(criteria) option in the validate
plugin should follow the specification as mentioned by this option plugin should follow the specification as mentioned by this option.
default: draft7 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: choices:
- draft3 - draft3
- draft4 - draft4
- draft6 - draft6
- draft7 - draft7
- 2019-09
- 2020-12
env: env:
- name: ANSIBLE_VALIDATE_JSONSCHEMA_DRAFT - name: ANSIBLE_VALIDATE_JSONSCHEMA_DRAFT
vars: vars:
- name: ansible_validate_jsonschema_draft - 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: notes:
- The value of I(data) option should be either a valid B(JSON) object or a B(JSON) string. - 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 - 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._text import to_text
from ansible.module_utils.basic import missing_required_lib from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.six import string_types 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.module_utils.common.utils import to_list
from ansible_collections.ansible.utils.plugins.plugin_utils.base.validate import ValidateBase from ansible_collections.ansible.utils.plugins.plugin_utils.base.validate import ValidateBase
display = Display()
# PY2 compatibility for JSONDecodeError # PY2 compatibility for JSONDecodeError
try: try:
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError
@ -58,6 +73,7 @@ except ImportError:
try: try:
import jsonschema import jsonschema
import jsonschema.validators
HAS_JSONSCHEMA = True HAS_JSONSCHEMA = True
except ImportError: except ImportError:
@ -79,6 +95,34 @@ def json_path(absolute_path):
class Validate(ValidateBase): 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 @staticmethod
def _check_reqs(): def _check_reqs():
"""Check the prerequisites are installed for jsonschema """Check the prerequisites are installed for jsonschema
@ -126,6 +170,28 @@ class Validate(ValidateBase):
) )
raise AnsibleError(msg) 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): def validate(self):
"""Std entry point for a validate execution """Std entry point for a validate execution
@ -141,6 +207,7 @@ class Validate(ValidateBase):
""" """
self._check_reqs() self._check_reqs()
self._check_args() self._check_args()
self._check_drafts()
try: try:
self._validate_jsonschema() self._validate_jsonschema()
@ -153,30 +220,45 @@ class Validate(ValidateBase):
error_messages = None error_messages = None
draft = self._get_sub_plugin_options("draft") draft = self._get_sub_plugin_options("draft")
check_format = self._get_sub_plugin_options("check_format")
error_messages = [] error_messages = []
for criteria in self._criteria: for criteria in self._criteria:
if draft == "draft3": format_checker = None
validator = jsonschema.Draft3Validator( validator_class = None
criteria, if draft is not None:
format_checker=jsonschema.draft3_format_checker, try:
) validator_class = self._JSONSCHEMA_DRAFTS[draft]["validator"]
elif draft == "draft4": except KeyError:
validator = jsonschema.Draft4Validator( display.warning(
criteria, 'No validator available for "{draft}", falling back to autodetection. A newer version of jsonschema might support this draft.'.format(
format_checker=jsonschema.draft4_format_checker, draft=draft,
) ),
elif draft == "draft6": )
validator = jsonschema.Draft6Validator( if validator_class is None:
criteria, # Either no draft was specified or specified draft has no validator class
format_checker=jsonschema.draft6_format_checker, # in installed jsonschema version. Do autodetection instead.
) validator_class = jsonschema.validators.validator_for(criteria)
else:
validator = jsonschema.Draft7Validator(
criteria,
format_checker=jsonschema.draft7_format_checker,
)
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) validation_errors = sorted(validator.iter_errors(self._data), key=lambda e: e.path)
if validation_errors: if validation_errors:

View File

@ -1,4 +1,4 @@
jsonschema ; python_version >= '2.7' jsonschema
netaddr netaddr
ttp ttp
textfsm textfsm

View File

@ -25,7 +25,7 @@
- ansible.builtin.assert: - ansible.builtin.assert:
that: 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 - name: invalid engine value
ansible.builtin.set_fact: ansible.builtin.set_fact:

View File

@ -31,7 +31,7 @@
- ansible.builtin.assert: - ansible.builtin.assert:
that: 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 - name: test invalid plugin configuration option, passed as task varaible
ansible.builtin.set_fact: ansible.builtin.set_fact:
@ -43,7 +43,7 @@
- ansible.builtin.assert: - ansible.builtin.assert:
that: 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 - name: invalid engine value
ansible.builtin.set_fact: ansible.builtin.set_fact:

View File

@ -52,7 +52,7 @@
that: that:
- "'errors' not in result" - "'errors' not in result"
- "result['failed'] == true" - "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 - name: invalid engine value
ansible.utils.validate: ansible.utils.validate:

View File

@ -23,7 +23,7 @@
- ansible.builtin.assert: - ansible.builtin.assert:
that: 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 - name: invalid engine value
ansible.builtin.set_fact: ansible.builtin.set_fact:

View File

@ -186,7 +186,7 @@ class TestValidate(unittest.TestCase):
result = self._plugin.run(task_vars={"ansible_validate_jsonschema_draft": "draft0"}) result = self._plugin.run(task_vars={"ansible_validate_jsonschema_draft": "draft0"})
self.assertIn( 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"], result["msg"],
) )
@ -285,3 +285,15 @@ class TestValidate(unittest.TestCase):
result = self._plugin.run(task_vars=None) result = self._plugin.run(task_vars=None)
self.assertIn("Validation errors were found", result["msg"]) 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"])

View File

@ -150,7 +150,7 @@ class TestValidate(unittest.TestCase):
with self.assertRaises(AnsibleFilterError) as error: with self.assertRaises(AnsibleFilterError) as error:
validate(*args, **kwargs) validate(*args, **kwargs)
self.assertIn( 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), str(error.exception),
) )

View File

@ -148,7 +148,7 @@ class TestValidate(unittest.TestCase):
with self.assertRaises(AnsibleError) as error: with self.assertRaises(AnsibleError) as error:
validate(*args, **kwargs) validate(*args, **kwargs)
self.assertIn( 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), str(error.exception),
) )

View File

@ -1,4 +1,4 @@
jsonschema ; python_version >= '2.7' jsonschema
netaddr netaddr
textfsm textfsm
ttp ttp