diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index bcfaa1f0ef..8dd097297e 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -59,7 +59,7 @@ jobs: WSLENV: FORCE_COLOR:PYTEST_REQPASS:TOXENV:GITHUB_STEP_SUMMARY # Number of expected test passes, safety measure for accidental skip of # tests. Update value if you add/remove tests. - PYTEST_REQPASS: 804 + PYTEST_REQPASS: 806 steps: - name: Activate WSL1 if: "contains(matrix.shell, 'wsl')" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d23782d7b4..b1455faebc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -146,7 +146,7 @@ repos: # empty args needed in order to match mypy cli behavior args: [--strict] additional_dependencies: - - ansible-compat>=4.0.1 + - ansible-compat>=4.1.0 - black>=22.10.0 - cryptography>=39.0.1 - filelock diff --git a/examples/playbooks/rule-schema-become-method-fail.yml b/examples/playbooks/rule-schema-become-method-fail.yml new file mode 100644 index 0000000000..555e14e12c --- /dev/null +++ b/examples/playbooks/rule-schema-become-method-fail.yml @@ -0,0 +1,10 @@ +--- +- name: Test 'become_method' plugin validity + hosts: localhost + become: true + become_method: this_is_not_an_installed_plugin + tasks: + - name: Another example + ansible.builtin.debug: + msg: "This should not be reached" + become_method: this_is_not_an_installed_plugin diff --git a/examples/playbooks/rule-schema-become-method-pass.yml b/examples/playbooks/rule-schema-become-method-pass.yml new file mode 100644 index 0000000000..23eca31bbb --- /dev/null +++ b/examples/playbooks/rule-schema-become-method-pass.yml @@ -0,0 +1,5 @@ +--- +- name: Test 'become_method' plugin validity + hosts: localhost + become: true + become_method: ansible.builtin.sudo diff --git a/src/ansiblelint/rules/schema.py b/src/ansiblelint/rules/schema.py index a1d013a249..13673d3c0a 100644 --- a/src/ansiblelint/rules/schema.py +++ b/src/ansiblelint/rules/schema.py @@ -3,13 +3,15 @@ import logging import sys -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any +from ansiblelint.app import get_app from ansiblelint.errors import MatchError from ansiblelint.file_utils import Lintable from ansiblelint.rules import AnsibleLintRule from ansiblelint.schemas.__main__ import JSON_SCHEMAS from ansiblelint.schemas.main import validate_file_schema +from ansiblelint.text import has_jinja if TYPE_CHECKING: from ansiblelint.utils import Task @@ -47,6 +49,10 @@ }, } +FIELD_CHECKS = { + "become_method": get_app().runtime.plugins.become.keys(), # pylint: disable=no-member +} + class ValidateSchemaRule(AnsibleLintRule): """Perform JSON Schema Validation for known lintable kinds.""" @@ -75,12 +81,49 @@ class ValidateSchemaRule(AnsibleLintRule): "schema[vars]": "", } + become_method_msg = f"'become_method' must be one of the currently installed plugins: {', '.join(FIELD_CHECKS['become_method'])}" + + def matchplay(self, file: Lintable, data: dict[str, Any]) -> list[MatchError]: + """Return matches found for a specific playbook.""" + results: list[MatchError] = [] + if not data or file.kind not in ("tasks", "handlers", "playbook"): + return results + # check at play level + for key, value in FIELD_CHECKS.items(): + if key in data: + plugin_value = data.get(key, None) + if not has_jinja(plugin_value) and plugin_value not in value: + results.append( + MatchError( + message=self.become_method_msg, + lintable=file or Lintable(""), + rule=ValidateSchemaRule(), + details=ValidateSchemaRule.description, + tag=f"schema[{file.kind}]", + ), + ) + + return results + def matchtask( self, task: Task, file: Lintable | None = None, ) -> bool | str | MatchError | list[MatchError]: result = [] + for key, value in FIELD_CHECKS.items(): + if key in task.raw_task: + plugin_value = task.raw_task.get(key, None) + if not has_jinja(plugin_value) and plugin_value not in value: + result.append( + MatchError( + message=self.become_method_msg, + lintable=file or Lintable(""), + rule=ValidateSchemaRule(), + details=ValidateSchemaRule.description, + tag=f"schema[{file.kind}]", # type: ignore[union-attr] + ), + ) for key in pre_checks["task"]: if key in task.raw_task: msg = pre_checks["task"][key]["msg"] @@ -98,9 +141,9 @@ def matchtask( def matchyaml(self, file: Lintable) -> list[MatchError]: """Return JSON validation errors found as a list of MatchError(s).""" - result = [] + result: list[MatchError] = [] if file.kind not in JSON_SCHEMAS: - return [] + return result errors = validate_file_schema(file) if errors: @@ -120,6 +163,9 @@ def matchyaml(self, file: Lintable) -> list[MatchError]: tag=f"schema[{file.kind}]", ), ) + + if not result: + result = super().matchyaml(file) return result @@ -252,6 +298,21 @@ def matchyaml(self, file: Lintable) -> list[MatchError]: [], id="rulebook2", ), + pytest.param( + "examples/playbooks/rule-schema-become-method-pass.yml", + "playbook", + [], + id="playbook", + ), + pytest.param( + "examples/playbooks/rule-schema-become-method-fail.yml", + "playbook", + [ + "'become_method' must be one of the currently installed plugins", + "'become_method' must be one of the currently installed plugins", + ], + id="playbook2", + ), ), ) def test_schema(file: str, expected_kind: str, expected: list[str]) -> None: