summaryrefslogtreecommitdiffstats
path: root/src/ansiblelint/rules/only_builtins.py
blob: 78ad93a9dff45f63425df9836e2b042b7b909070 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
"""Rule definition for usage of builtin actions only."""
from __future__ import annotations

import os
import sys
from typing import TYPE_CHECKING

from ansiblelint.config import options
from ansiblelint.rules import AnsibleLintRule
from ansiblelint.rules.fqcn import builtins
from ansiblelint.skip_utils import is_nested_task

if TYPE_CHECKING:
    from ansiblelint.file_utils import Lintable
    from ansiblelint.utils import Task


class OnlyBuiltinsRule(AnsibleLintRule):
    """Use only builtin actions."""

    id = "only-builtins"
    severity = "MEDIUM"
    description = "Check whether the playbook uses anything but ``ansible.builtin``"
    tags = ["opt-in", "experimental"]

    def matchtask(
        self,
        task: Task,
        file: Lintable | None = None,
    ) -> bool | str:
        module = task["action"]["__ansible_module_original__"]

        allowed_collections = [
            "ansible.builtin",
            "ansible.legacy",
            *options.only_builtins_allow_collections,
        ]
        allowed_modules = builtins + options.only_builtins_allow_modules

        is_allowed = (
            any(module.startswith(f"{prefix}.") for prefix in allowed_collections)
            or module in allowed_modules
        )

        return not is_allowed and not is_nested_task(task)


# testing code to be loaded only with pytest or when executed the rule file
if "pytest" in sys.modules:
    # pylint: disable=ungrouped-imports
    import pytest

    from ansiblelint.constants import RC
    from ansiblelint.testing import RunFromText, run_ansible_lint

    SUCCESS_PLAY = """
- hosts: localhost
  tasks:
  - name: A block
    block:
    - name: Shell (fqcn)
      ansible.builtin.shell: echo This rule should not get matched by the only-builtins rule
    - name: Command with legacy FQCN
      ansible.legacy.command: echo This rule should not get matched by the only-builtins rule
    """

    def test_only_builtins_fail() -> None:
        """Test rule matches."""
        env = os.environ.copy()
        env["NO_COLOR"] = "1"
        result = run_ansible_lint(
            "--strict",
            "--warn-list=",
            "--enable-list",
            "only-builtins",
            "examples/playbooks/rule-only-builtins.yml",
            env=env,
        )
        assert result.returncode == RC.VIOLATIONS_FOUND
        assert "Failed" in result.stderr
        assert "warning(s)" in result.stderr
        assert "only-builtins: Use only builtin actions" in result.stdout

    def test_only_builtins_allow() -> None:
        """Test rule doesn't match."""
        conf_path = "examples/playbooks/.ansible-lint-only-builtins-allow"
        result = run_ansible_lint(
            f"--config-file={conf_path}",
            "--strict",
            "--warn-list=",
            "--enable-list",
            "only-builtins",
            "examples/playbooks/rule-only-builtins.yml",
        )
        assert "only-builtins" not in result.stdout
        assert result.returncode == RC.SUCCESS

    @pytest.mark.parametrize(
        "rule_runner",
        (OnlyBuiltinsRule,),
        indirect=["rule_runner"],
    )
    def test_only_builtin_pass(rule_runner: RunFromText) -> None:
        """Test rule does not match."""
        results = rule_runner.run_playbook(SUCCESS_PLAY)
        assert len(results) == 0, results