summaryrefslogtreecommitdiffstats
path: root/src/ansiblelint/rules/run_once.py
blob: f8a059e9cd17c53d5445d8413f3e6eeeccb54b3c (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
"""Optional Ansible-lint rule to warn use of run_once with strategy free."""
from __future__ import annotations

import sys
from typing import TYPE_CHECKING, Any

from ansiblelint.constants import LINE_NUMBER_KEY
from ansiblelint.errors import MatchError
from ansiblelint.rules import AnsibleLintRule

if TYPE_CHECKING:
    from ansiblelint.file_utils import Lintable


class RunOnce(AnsibleLintRule):
    """Run once should use strategy other than free."""

    id = "run-once"
    link = "https://docs.ansible.com/ansible/latest/reference_appendices/playbooks_keywords.html"
    description = "When using run_once, we should avoid using strategy as free."

    tags = ["idiom"]
    severity = "MEDIUM"

    def matchplay(self, file: Lintable, data: dict[str, Any]) -> list[MatchError]:
        """Return matches found for a specific playbook."""
        # If the Play uses the 'strategy' and it's value is set to free

        if not file or file.kind != "playbook" or not data:
            return []

        strategy = data.get("strategy", None)
        run_once = data.get("run_once", False)
        if (not strategy and not run_once) or strategy != "free":
            return []
        return [
            self.create_matcherror(
                message="Play uses strategy: free",
                filename=file,
                tag=f"{self.id}[play]",
                # pylint: disable=protected-access
                linenumber=strategy._line_number,
            )
        ]

    def matchtask(
        self, task: dict[str, Any], file: Lintable | None = None
    ) -> list[MatchError]:
        """Return matches for a task."""
        if not file or file.kind != "playbook":
            return []

        run_once = task.get("run_once", False)
        if not run_once:
            return []
        return [
            self.create_matcherror(
                message="Using run_once may behave differently if strategy is set to free.",
                filename=file,
                tag=f"{self.id}[task]",
                linenumber=task[LINE_NUMBER_KEY],
            )
        ]


# testing code to be loaded only with pytest or when executed the rule file
if "pytest" in sys.modules:
    import pytest

    from ansiblelint.rules import RulesCollection  # pylint: disable=ungrouped-imports
    from ansiblelint.runner import Runner  # pylint: disable=ungrouped-imports

    @pytest.mark.parametrize(
        ("test_file", "failure"),
        (
            pytest.param("examples/playbooks/run-once-pass.yml", 0, id="pass"),
            pytest.param("examples/playbooks/run-once-fail.yml", 2, id="fail"),
        ),
    )
    def test_run_once(
        default_rules_collection: RulesCollection, test_file: str, failure: int
    ) -> None:
        """Test rule matches."""
        results = Runner(test_file, rules=default_rules_collection).run()
        for result in results:
            assert result.rule.id == RunOnce().id
        assert len(results) == failure