diff --git a/.ansible-lint b/.ansible-lint index 9213bbeafc..4e92c017ee 100644 --- a/.ansible-lint +++ b/.ansible-lint @@ -122,3 +122,7 @@ task_name_prefix: "{stem} | " # Limit the depth of the nested blocks: # max_block_depth: 20 + +# Also recognize these versions of Ansible as supported: +# supported_ansible_also: +# - "2.14" diff --git a/examples/broken_supported_ansible_also/.ansible-lint b/examples/broken_supported_ansible_also/.ansible-lint new file mode 100644 index 0000000000..7897948167 --- /dev/null +++ b/examples/broken_supported_ansible_also/.ansible-lint @@ -0,0 +1,2 @@ +# Invalid supported_ansible_also type +supported_ansible_also: True diff --git a/examples/meta_runtime_version_checks/pass/meta/runtime.yml b/examples/meta_runtime_version_checks/pass/meta/runtime.yml deleted file mode 100644 index 81c378f752..0000000000 --- a/examples/meta_runtime_version_checks/pass/meta/runtime.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -requires_ansible: ">=2.15.0,<2.17" diff --git a/examples/meta_runtime_version_checks/pass_0/meta/runtime.yml b/examples/meta_runtime_version_checks/pass_0/meta/runtime.yml new file mode 100644 index 0000000000..9257a8c1f9 --- /dev/null +++ b/examples/meta_runtime_version_checks/pass_0/meta/runtime.yml @@ -0,0 +1,2 @@ +--- +requires_ansible: ">=2.15.0,<2.17.0" diff --git a/examples/meta_runtime_version_checks/pass_1/meta/runtime.yml b/examples/meta_runtime_version_checks/pass_1/meta/runtime.yml new file mode 100644 index 0000000000..460bbaf818 --- /dev/null +++ b/examples/meta_runtime_version_checks/pass_1/meta/runtime.yml @@ -0,0 +1,2 @@ +--- +requires_ansible: ">=2.9.10" diff --git a/src/ansiblelint/cli.py b/src/ansiblelint/cli.py index 9e6ec582f8..ce8d9ecde7 100644 --- a/src/ansiblelint/cli.py +++ b/src/ansiblelint/cli.py @@ -499,6 +499,7 @@ def merge_config(file_config: dict[Any, Any], cli_config: Options) -> Options: "enable_list": [], "only_builtins_allow_collections": [], "only_builtins_allow_modules": [], + "supported_ansible_also": [], # do not include "write_list" here. See special logic below. } diff --git a/src/ansiblelint/config.py b/src/ansiblelint/config.py index c73e4c5ce3..b0b791a84e 100644 --- a/src/ansiblelint/config.py +++ b/src/ansiblelint/config.py @@ -174,6 +174,9 @@ class Options: # pylint: disable=too-many-instance-attributes ignore_file: Path | None = None max_tasks: int = 100 max_block_depth: int = 20 + # Refer to https://docs.ansible.com/ansible/latest/reference_appendices/release_and_maintenance.html#ansible-core-support-matrix + _default_supported = ["2.15.", "2.16.", "2.17."] + supported_ansible_also: list[str] = field(default_factory=list) @property def nodeps(self) -> bool: @@ -186,6 +189,11 @@ def __post_init__(self) -> None: if self.nodeps: self.offline = True + @property + def supported_ansible(self) -> list[str]: + """Returns list of ansible versions that are considered supported.""" + return sorted([*self._default_supported, *self.supported_ansible_also]) + options = Options() diff --git a/src/ansiblelint/rules/meta_runtime.md b/src/ansiblelint/rules/meta_runtime.md index b3741f1662..3e05c595a4 100644 --- a/src/ansiblelint/rules/meta_runtime.md +++ b/src/ansiblelint/rules/meta_runtime.md @@ -1,15 +1,21 @@ # meta-runtime -This rule checks the meta/runtime.yml `requires_ansible` key against the list of currently supported versions of ansible-core. +This rule checks the meta/runtime.yml `requires_ansible` key against the list of +currently supported versions of ansible-core. This rule can produce messages such as: -- `meta-runtime[unsupported-version]` - `requires_ansible` key must refer to a currently supported version such as: >=2.14.0, >=2.15.0, >=2.16.0 -- `meta-runtime[invalid-version]` - `requires_ansible` is not a valid requirement specification +- `meta-runtime[unsupported-version]` - `requires_ansible` key must refer to a + currently supported version such as: >=2.14.0, >=2.15.0, >=2.16.0 +- `meta-runtime[invalid-version]` - `requires_ansible` is not a valid + requirement specification -Please note that the linter will allow only a full version of Ansible such `2.16.0` and not allow their short form, like `2.16`. This is a safety measure -for asking authors to mention an explicit version that they tested with. Over the years we spotted multiple problems caused by the use of the short versions, users -ended up trying an outdated version that was never tested against by the collection maintainer. +Please note that the linter will allow only a full version of Ansible such +`2.16.0` and not allow their short form, like `2.16`. This is a safety measure +for asking authors to mention an explicit version that they tested with. Over +the years we spotted multiple problems caused by the use of the short versions, +users ended up trying an outdated version that was never tested against by the +collection maintainer. ## Problematic code @@ -19,7 +25,6 @@ ended up trying an outdated version that was never tested against by the collect requires_ansible: ">=2.9" ``` - ```yaml # runtime.yml --- @@ -33,3 +38,15 @@ requires_ansible: "2.15" --- requires_ansible: ">=2.15.0" ``` + +## Configuration + +In addition to the internal list of supported Ansible versions, users can +configure additional values. This allows those that want to maintain content +that requires a version of ansible-core that is already out of support. + +```yaml +# Also recognize these versions of Ansible as supported: +supported_ansible_also: + - "2.14" +``` diff --git a/src/ansiblelint/rules/meta_runtime.py b/src/ansiblelint/rules/meta_runtime.py index be0e6985e1..3df28268f1 100644 --- a/src/ansiblelint/rules/meta_runtime.py +++ b/src/ansiblelint/rules/meta_runtime.py @@ -30,12 +30,8 @@ class CheckRequiresAnsibleVersion(AnsibleLintRule): tags = ["metadata"] version_added = "v6.11.0 (last update)" - # Refer to https://access.redhat.com/support/policy/updates/ansible-automation-platform - # Also add devel to this list - supported_ansible = ["2.15.", "2.16.", "2.17."] - supported_ansible_examples = [f">={x}0" for x in supported_ansible] _ids = { - "meta-runtime[unsupported-version]": f"'requires_ansible' key must refer to a currently supported version such as: {', '.join(supported_ansible_examples)}", + "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", } @@ -50,22 +46,26 @@ def matchyaml(self, file: Lintable) -> list[MatchError]: if file.kind != "meta-runtime": return [] - version_required = file.data.get("requires_ansible", None) + requires_ansible = file.data.get("requires_ansible", None) - if version_required: - if not any( - version in version_required for version in self.supported_ansible + 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=self._ids["meta-runtime[unsupported-version]"], + message=msg, tag="meta-runtime[unsupported-version]", filename=file, ), ) try: - SpecifierSet(version_required) + SpecifierSet(requires_ansible) except ValueError: results.append( self.create_matcherror( @@ -90,10 +90,10 @@ def matchyaml(self, file: Lintable) -> list[MatchError]: ("test_file", "failures", "tags"), ( pytest.param( - "examples/meta_runtime_version_checks/pass/meta/runtime.yml", + "examples/meta_runtime_version_checks/pass_0/meta/runtime.yml", 0, "meta-runtime[unsupported-version]", - id="pass", + id="pass0", ), pytest.param( "examples/meta_runtime_version_checks/fail_0/meta/runtime.yml", @@ -115,16 +115,37 @@ def matchyaml(self, file: Lintable) -> list[MatchError]: ), ), ) - def test_meta_supported_version( + def test_default_meta_supported_version( default_rules_collection: RulesCollection, test_file: str, failures: int, tags: str, ) -> None: - """Test rule matches.""" + """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 diff --git a/src/ansiblelint/rules/schema.py b/src/ansiblelint/rules/schema.py index f3e1b5ccfc..6997acdba9 100644 --- a/src/ansiblelint/rules/schema.py +++ b/src/ansiblelint/rules/schema.py @@ -293,6 +293,14 @@ def matchyaml(self, file: Lintable) -> list[MatchError]: ], id="ansible-lint-config-broken", ), + pytest.param( + "examples/broken_supported_ansible_also/.ansible-lint", + "ansible-lint-config", + [ + r".*supported_ansible_also True is not of type 'array'.*https://", + ], + id="ansible-lint-config-broken", + ), pytest.param( "examples/ansible-navigator.yml", "ansible-navigator-config", diff --git a/src/ansiblelint/schemas/ansible-lint-config.json b/src/ansiblelint/schemas/ansible-lint-config.json index 8c6128a3f1..7f53ffd197 100644 --- a/src/ansiblelint/schemas/ansible-lint-config.json +++ b/src/ansiblelint/schemas/ansible-lint-config.json @@ -248,6 +248,13 @@ "title": "Strict", "type": "boolean" }, + "supported_ansible_also": { + "items": { + "type": "string" + }, + "title": "Add supported ansible versions", + "type": "array" + }, "tags": { "items": { "type": "string"