summaryrefslogtreecommitdiffstats
path: root/src/ansiblelint/rules/meta_runtime.py
blob: 3df28268f1f380d668d31c1f7477e87867d0cabd (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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
"""Implementation of meta-runtime rule."""

from __future__ import annotations

import sys
from typing import TYPE_CHECKING

from packaging.specifiers import SpecifierSet

from ansiblelint.rules import AnsibleLintRule

# Copyright (c) 2018, Ansible Project


if TYPE_CHECKING:
    from ansiblelint.errors import MatchError
    from ansiblelint.file_utils import Lintable


class CheckRequiresAnsibleVersion(AnsibleLintRule):
    """Required ansible version in meta/runtime.yml must be a supported version."""

    id = "meta-runtime"
    description = (
        "The ``requires_ansible`` key in runtime.yml must specify "
        "a supported platform version of ansible-core and be a valid version value "
        "in x.y.z format."
    )
    severity = "VERY_HIGH"
    tags = ["metadata"]
    version_added = "v6.11.0 (last update)"

    _ids = {
        "meta-runtime[unsupported-version]": "'requires_ansible' key must refer to a currently supported version",
        "meta-runtime[invalid-version]": "'requires_ansible' is not a valid requirement specification",
    }

    def matchyaml(self, file: Lintable) -> list[MatchError]:
        """Find violations inside meta files.

        :param file: Input lintable file that is a match for `meta-runtime`
        :returns: List of errors matched to the input file
        """
        results = []

        if file.kind != "meta-runtime":
            return []

        requires_ansible = file.data.get("requires_ansible", None)

        if requires_ansible:
            if self.options and not any(
                version in requires_ansible
                for version in self.options.supported_ansible
            ):
                supported_ansible = [f">={x}0" for x in self.options.supported_ansible]
                msg = f"'requires_ansible' key must refer to a currently supported version such as: {', '.join(supported_ansible)}"

                results.append(
                    self.create_matcherror(
                        message=msg,
                        tag="meta-runtime[unsupported-version]",
                        filename=file,
                    ),
                )

            try:
                SpecifierSet(requires_ansible)
            except ValueError:
                results.append(
                    self.create_matcherror(
                        message="'requires_ansible' is not a valid requirement specification",
                        tag="meta-runtime[invalid-version]",
                        filename=file,
                    ),
                )

        return results


# 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.rules import RulesCollection
    from ansiblelint.runner import Runner

    @pytest.mark.parametrize(
        ("test_file", "failures", "tags"),
        (
            pytest.param(
                "examples/meta_runtime_version_checks/pass_0/meta/runtime.yml",
                0,
                "meta-runtime[unsupported-version]",
                id="pass0",
            ),
            pytest.param(
                "examples/meta_runtime_version_checks/fail_0/meta/runtime.yml",
                1,
                "meta-runtime[unsupported-version]",
                id="fail0",
            ),
            pytest.param(
                "examples/meta_runtime_version_checks/fail_1/meta/runtime.yml",
                1,
                "meta-runtime[unsupported-version]",
                id="fail1",
            ),
            pytest.param(
                "examples/meta_runtime_version_checks/fail_2/meta/runtime.yml",
                1,
                "meta-runtime[invalid-version]",
                id="fail2",
            ),
        ),
    )
    def test_default_meta_supported_version(
        default_rules_collection: RulesCollection,
        test_file: str,
        failures: int,
        tags: str,
    ) -> None:
        """Test for default supported ansible versions."""
        default_rules_collection.register(CheckRequiresAnsibleVersion())
        results = Runner(test_file, rules=default_rules_collection).run()
        for result in results:
            assert result.rule.id == CheckRequiresAnsibleVersion().id
            assert result.tag == tags
        assert len(results) == failures

    @pytest.mark.parametrize(
        ("test_file", "failures"),
        (
            pytest.param(
                "examples/meta_runtime_version_checks/pass_1/meta/runtime.yml",
                0,
                id="pass1",
            ),
        ),
    )
    def test_added_meta_supported_version(
        default_rules_collection: RulesCollection,
        test_file: str,
        failures: int,
    ) -> None:
        """Test for added supported ansible versions in the config."""
        default_rules_collection.register(CheckRequiresAnsibleVersion())
        default_rules_collection.options.supported_ansible_also = ["2.9"]
        results = Runner(test_file, rules=default_rules_collection).run()
        assert len(results) == failures