diff --git a/src/ansiblelint/rules/args.py b/src/ansiblelint/rules/args.py index 272de1e271..f127026cc7 100644 --- a/src/ansiblelint/rules/args.py +++ b/src/ansiblelint/rules/args.py @@ -8,7 +8,6 @@ import logging import re import sys -from functools import lru_cache from typing import TYPE_CHECKING, Any # pylint: disable=preferred-module @@ -18,11 +17,11 @@ # pylint: disable=reimported import ansible.module_utils.basic as mock_ansible_module from ansible.module_utils import basic -from ansible.plugins.loader import PluginLoadContext, module_loader from ansiblelint.constants import LINE_NUMBER_KEY from ansiblelint.rules import AnsibleLintRule, RulesCollection from ansiblelint.text import has_jinja +from ansiblelint.utils import load_plugin from ansiblelint.yaml_utils import clean_json if TYPE_CHECKING: @@ -66,12 +65,6 @@ } -@lru_cache -def load_module(module_name: str) -> PluginLoadContext: - """Load plugin from module name and cache it.""" - return module_loader.find_plugin_with_context(module_name) - - class ValidationPassedError(Exception): """Exception to be raised when validation passes.""" @@ -111,7 +104,7 @@ def matchtask( if module_name in self.module_aliases: return [] - loaded_module = load_module(module_name) + loaded_module = load_plugin(module_name) # https://github.com/ansible/ansible-lint/issues/3200 # since "ps1" modules cannot be executed on POSIX platforms, we will diff --git a/src/ansiblelint/rules/fqcn.py b/src/ansiblelint/rules/fqcn.py index 5a7b335db0..6bc44721b6 100644 --- a/src/ansiblelint/rules/fqcn.py +++ b/src/ansiblelint/rules/fqcn.py @@ -5,10 +5,9 @@ import sys from typing import TYPE_CHECKING, Any -from ansible.plugins.loader import module_loader - from ansiblelint.constants import LINE_NUMBER_KEY from ansiblelint.rules import AnsibleLintRule, TransformMixin +from ansiblelint.utils import load_plugin if TYPE_CHECKING: from ruamel.yaml.comments import CommentedMap, CommentedSeq @@ -116,9 +115,12 @@ def matchtask( ) -> list[MatchError]: result = [] module = task["action"]["__ansible_module_original__"] + if not isinstance(module, str): + msg = "Invalid data for module." + raise RuntimeError(msg) if module not in self.module_aliases: - loaded_module = module_loader.find_plugin_with_context(module) + loaded_module = load_plugin(module) target = loaded_module.resolved_fqcn self.module_aliases[module] = target if target is None: @@ -137,10 +139,16 @@ def matchtask( 1, ) if module != legacy_module: + if module == "ansible.builtin.include": + message = f"Avoid deprecated module ({module})" + details = "Use `ansible.builtin.include_task` or `ansible.builtin.import_tasks` instead." + else: + message = f"Use FQCN for builtin module actions ({module})." + details = f"Use `{module_alias}` or `{legacy_module}` instead." result.append( self.create_matcherror( - message=f"Use FQCN for builtin module actions ({module}).", - details=f"Use `{module_alias}` or `{legacy_module}` instead.", + message=message, + details=details, filename=file, lineno=task["__line__"], tag="fqcn[action-core]", diff --git a/src/ansiblelint/schemas/__store__.json b/src/ansiblelint/schemas/__store__.json index c5c0f7902d..779dbc2527 100644 --- a/src/ansiblelint/schemas/__store__.json +++ b/src/ansiblelint/schemas/__store__.json @@ -24,7 +24,7 @@ "url": "https://raw.githubusercontent.com/ansible/ansible-lint/main/src/ansiblelint/schemas/inventory.json" }, "meta": { - "etag": "9eb5c611e25cc9e0a180119904f8dfe39c59409b37da972f66a6c60f040a3a08", + "etag": "097a20155bc7936b6eae292a556bb38202d34a0a333ff5cbaaa1b4d3a4cc7bf5", "url": "https://raw.githubusercontent.com/ansible/ansible-lint/main/src/ansiblelint/schemas/meta.json" }, "meta-runtime": { @@ -36,7 +36,7 @@ "url": "https://raw.githubusercontent.com/ansible/ansible-lint/main/src/ansiblelint/schemas/molecule.json" }, "playbook": { - "etag": "48d584b10235804c13de78177dcfeeba433bd4d196ef3872aac7fc26a26303f5", + "etag": "8ae42e48318ff6da41f6d383dc19b643221258ea6521886f834e31cb8d3a7322", "url": "https://raw.githubusercontent.com/ansible/ansible-lint/main/src/ansiblelint/schemas/playbook.json" }, "requirements": { @@ -48,7 +48,7 @@ "url": "https://raw.githubusercontent.com/ansible/ansible-lint/main/src/ansiblelint/schemas/role-arg-spec.json" }, "rulebook": { - "etag": "b8c4ddf5d8d276d7a27335f2042554b3528ad9a091f13f1ffe7241be08a9671b", + "etag": "8a421671574fa65a57fb0d08e45879bfa005f271aa8c0243982a84b49fb0fe54", "url": "https://raw.githubusercontent.com/ansible/ansible-rulebook/main/ansible_rulebook/schema/ruleset_schema.json" }, "tasks": { diff --git a/src/ansiblelint/schemas/rulebook.json b/src/ansiblelint/schemas/rulebook.json index e312b959f7..0f13a9daef 100644 --- a/src/ansiblelint/schemas/rulebook.json +++ b/src/ansiblelint/schemas/rulebook.json @@ -156,6 +156,9 @@ { "type": "string" }, + { + "type": "boolean" + }, { "$ref": "#/$defs/all-condition" }, diff --git a/src/ansiblelint/utils.py b/src/ansiblelint/utils.py index 7189272794..460bae94f7 100644 --- a/src/ansiblelint/utils.py +++ b/src/ansiblelint/utils.py @@ -30,7 +30,7 @@ import re from collections.abc import Generator, ItemsView, Iterator, Mapping, Sequence from dataclasses import _MISSING_TYPE, dataclass, field -from functools import cache +from functools import cache, lru_cache from pathlib import Path from typing import Any @@ -44,7 +44,7 @@ from ansible.parsing.yaml.constructor import AnsibleConstructor, AnsibleMapping from ansible.parsing.yaml.loader import AnsibleLoader from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject, AnsibleSequence -from ansible.plugins.loader import add_all_plugin_dirs +from ansible.plugins.loader import PluginLoadContext, add_all_plugin_dirs, module_loader from ansible.template import Templar from ansible.utils.collection_loader import AnsibleCollectionConfig from yaml.composer import Composer @@ -1056,3 +1056,21 @@ def parse_examples_from_plugin(lintable: Lintable) -> tuple[int, str]: # Ignore the leading newline and lack of document start # as including those in EXAMPLES would be weird. return offset, (f"---{examples}" if examples else "") + + +@lru_cache +def load_plugin(name: str) -> PluginLoadContext: + """Return loaded ansible plugin/module.""" + loaded_module = module_loader.find_plugin_with_context( + name, + ignore_deprecated=True, + check_aliases=True, + ) + if not loaded_module.resolved and name.startswith("ansible.builtin."): + # fallback to core behavior of using legacy + loaded_module = module_loader.find_plugin_with_context( + name.replace("ansible.builtin.", "ansible.legacy."), + ignore_deprecated=True, + check_aliases=True, + ) + return loaded_module diff --git a/test/test_task_includes.py b/test/test_task_includes.py index 3b02d00ce5..9f962132c1 100644 --- a/test/test_task_includes.py +++ b/test/test_task_includes.py @@ -1,4 +1,6 @@ """Tests related to task inclusions.""" +import sys + import pytest from ansiblelint.file_utils import Lintable @@ -9,7 +11,14 @@ @pytest.mark.parametrize( ("filename", "file_count", "match_count"), ( - pytest.param("examples/playbooks/blockincludes.yml", 4, 3, id="blockincludes"), + pytest.param( + "examples/playbooks/blockincludes.yml", + 4, + 3 + if sys.version_info >= (3, 10, 0) + else 4, # 3 with py310/ansible2.16, or 4 with py39/ansible2.15, + id="blockincludes", + ), pytest.param( "examples/playbooks/blockincludes2.yml", 4,