summaryrefslogtreecommitdiffstats
path: root/src/ansiblelint/rules/schema.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/ansiblelint/rules/schema.py')
-rw-r--r--src/ansiblelint/rules/schema.py257
1 files changed, 257 insertions, 0 deletions
diff --git a/src/ansiblelint/rules/schema.py b/src/ansiblelint/rules/schema.py
new file mode 100644
index 0000000..26ba1d9
--- /dev/null
+++ b/src/ansiblelint/rules/schema.py
@@ -0,0 +1,257 @@
+"""Rule definition for JSON Schema Validations."""
+from __future__ import annotations
+
+import logging
+import sys
+from typing import Any
+
+from ansiblelint.errors import MatchError
+from ansiblelint.file_utils import Lintable
+from ansiblelint.rules import AnsibleLintRule
+from ansiblelint.schemas import JSON_SCHEMAS, validate_file_schema
+
+_logger = logging.getLogger(__name__)
+
+
+DESCRIPTION_MD = """ Returned errors will not include exact line numbers, but they will mention
+the schema name being used as a tag, like ``schema[playbook]``,
+``schema[tasks]``.
+
+This rule is not skippable and stops further processing of the file.
+
+If incorrect schema was picked, you might want to either:
+
+* move the file to standard location, so its file is detected correctly.
+* use ``kinds:`` option in linter config to help it pick correct file type.
+"""
+
+pre_checks = {
+ "task": {
+ "with_flattened": {
+ "msg": "with_flattened was moved to with_community.general.flattened in 2.10",
+ "tag": "moves",
+ },
+ "with_filetree": {
+ "msg": "with_filetree was moved to with_community.general.flattened in 2.10",
+ "tag": "moves",
+ },
+ "with_cartesian": {
+ "msg": "with_cartesian was moved to with_community.general.flattened in 2.10",
+ "tag": "moves",
+ },
+ }
+}
+
+
+class ValidateSchemaRule(AnsibleLintRule):
+ """Perform JSON Schema Validation for known lintable kinds."""
+
+ description = DESCRIPTION_MD
+
+ id = "schema"
+ severity = "VERY_HIGH"
+ tags = ["core"]
+ version_added = "v6.1.0"
+
+ def matchtask(
+ self, task: dict[str, Any], file: Lintable | None = None
+ ) -> bool | str | MatchError | list[MatchError]:
+ result = []
+ for key in pre_checks["task"]:
+ if key in task:
+ msg = pre_checks["task"][key]["msg"]
+ tag = pre_checks["task"][key]["tag"]
+ result.append(
+ MatchError(
+ message=msg,
+ filename=file,
+ rule=ValidateSchemaRule(),
+ details=ValidateSchemaRule.description,
+ tag=f"schema[{tag}]",
+ )
+ )
+ return result
+
+ def matchyaml(self, file: Lintable) -> list[MatchError]:
+ """Return JSON validation errors found as a list of MatchError(s)."""
+ result = []
+ if file.kind not in JSON_SCHEMAS:
+ return []
+
+ errors = validate_file_schema(file)
+ if errors:
+ if errors[0].startswith("Failed to load YAML file"):
+ _logger.debug(
+ "Ignored failure to load %s for schema validation, as !vault may cause it."
+ )
+ return []
+
+ result.append(
+ MatchError(
+ message=errors[0],
+ filename=file,
+ rule=ValidateSchemaRule(),
+ details=ValidateSchemaRule.description,
+ tag=f"schema[{file.kind}]",
+ )
+ )
+ return result
+
+
+# testing code to be loaded only with pytest or when executed the rule file
+if "pytest" in sys.modules:
+ import pytest
+
+ # pylint: disable=ungrouped-imports
+ from ansiblelint.config import options
+ from ansiblelint.rules import RulesCollection
+ from ansiblelint.runner import Runner
+
+ @pytest.mark.parametrize(
+ ("file", "expected_kind", "expected"),
+ (
+ (
+ "examples/collection/galaxy.yml",
+ "galaxy",
+ ["'GPL' is not one of"],
+ ),
+ (
+ "examples/roles/invalid_requirements_schema/meta/requirements.yml",
+ "requirements",
+ ["{'foo': 'bar'} is not valid under any of the given schemas"],
+ ),
+ (
+ "examples/roles/invalid_meta_schema/meta/main.yml",
+ "meta",
+ ["False is not of type 'string'"],
+ ),
+ (
+ "examples/playbooks/vars/invalid_vars_schema.yml",
+ "vars",
+ ["'123' does not match any of the regexes"],
+ ),
+ (
+ "examples/execution-environment.yml",
+ "execution-environment",
+ [],
+ ),
+ (
+ "examples/ee_broken/execution-environment.yml",
+ "execution-environment",
+ ["Additional properties are not allowed ('foo' was unexpected)"],
+ ),
+ ("examples/meta/runtime.yml", "meta-runtime", []),
+ (
+ "examples/broken_collection_meta_runtime/meta/runtime.yml",
+ "meta-runtime",
+ ["Additional properties are not allowed ('foo' was unexpected)"],
+ ),
+ (
+ "examples/inventory/production.yml",
+ "inventory",
+ [],
+ ),
+ (
+ "examples/inventory/broken_dev_inventory.yml",
+ "inventory",
+ ["Additional properties are not allowed ('foo' was unexpected)"],
+ ),
+ (
+ ".ansible-lint",
+ "ansible-lint-config",
+ [],
+ ),
+ (
+ "examples/.config/ansible-lint.yml",
+ "ansible-lint-config",
+ [],
+ ),
+ (
+ "examples/broken/.ansible-lint",
+ "ansible-lint-config",
+ ["Additional properties are not allowed ('foo' was unexpected)"],
+ ),
+ (
+ "examples/ansible-navigator.yml",
+ "ansible-navigator-config",
+ [],
+ ),
+ (
+ "examples/broken/ansible-navigator.yml",
+ "ansible-navigator-config",
+ ["Additional properties are not allowed ('ansible' was unexpected)"],
+ ),
+ (
+ "examples/roles/hello/meta/argument_specs.yml",
+ "arg_specs",
+ [],
+ ),
+ (
+ "examples/roles/broken_argument_specs/meta/argument_specs.yml",
+ "arg_specs",
+ ["Additional properties are not allowed ('foo' was unexpected)"],
+ ),
+ ),
+ ids=(
+ # "playbook-fail",
+ "galaxy",
+ "requirements",
+ "meta",
+ "vars",
+ "ee",
+ "ee-broken",
+ "meta-runtime",
+ "meta-runtime-broken",
+ "inventory",
+ "inventory-broken",
+ "lint-config",
+ "lint-config2",
+ "lint-config-broken",
+ "navigator",
+ "navigator-broken",
+ "argspecs",
+ "argspecs-broken",
+ ),
+ )
+ def test_schema(file: str, expected_kind: str, expected: list[str]) -> None:
+ """Validate parsing of ansible output."""
+ lintable = Lintable(file)
+ assert lintable.kind == expected_kind
+
+ rules = RulesCollection(options=options)
+ rules.register(ValidateSchemaRule())
+ results = Runner(lintable, rules=rules).run()
+
+ assert len(results) == len(expected), results
+ for idx, result in enumerate(results):
+ assert result.filename.endswith(file)
+ assert expected[idx] in result.message
+ assert result.tag == f"schema[{expected_kind}]"
+
+ @pytest.mark.parametrize(
+ ("file", "expected_kind", "expected_tag", "count"),
+ (
+ pytest.param(
+ "examples/playbooks/rule-syntax-moves.yml",
+ "playbook",
+ "schema[moves]",
+ 3,
+ id="playbook",
+ ),
+ ),
+ )
+ def test_schema_moves(
+ file: str, expected_kind: str, expected_tag: str, count: int
+ ) -> None:
+ """Validate ability to detect schema[moves]."""
+ lintable = Lintable(file)
+ assert lintable.kind == expected_kind
+
+ rules = RulesCollection(options=options)
+ rules.register(ValidateSchemaRule())
+ results = Runner(lintable, rules=rules).run()
+
+ assert len(results) == count, results
+ for result in results:
+ assert result.filename.endswith(file)
+ assert result.tag == expected_tag