summaryrefslogtreecommitdiffstats
path: root/src/ansiblelint/rules/galaxy.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/ansiblelint/rules/galaxy.py')
-rw-r--r--src/ansiblelint/rules/galaxy.py251
1 files changed, 251 insertions, 0 deletions
diff --git a/src/ansiblelint/rules/galaxy.py b/src/ansiblelint/rules/galaxy.py
new file mode 100644
index 0000000..2f627f5
--- /dev/null
+++ b/src/ansiblelint/rules/galaxy.py
@@ -0,0 +1,251 @@
+"""Implementation of GalaxyRule."""
+from __future__ import annotations
+
+import sys
+from functools import total_ordering
+from typing import TYPE_CHECKING, Any
+
+from ansiblelint.constants import LINE_NUMBER_KEY
+from ansiblelint.rules import AnsibleLintRule
+
+if TYPE_CHECKING:
+ from ansiblelint.errors import MatchError
+ from ansiblelint.file_utils import Lintable
+
+
+class GalaxyRule(AnsibleLintRule):
+ """Rule for checking collection version is greater than 1.0.0 and checking for changelog."""
+
+ id = "galaxy"
+ description = "Confirm via galaxy.yml file if collection version is greater than or equal to 1.0.0 and check for changelog."
+ severity = "MEDIUM"
+ tags = ["metadata"]
+ version_added = "v6.11.0 (last update)"
+ _ids = {
+ "galaxy[tags]": "galaxy.yaml must have one of the required tags",
+ "galaxy[no-changelog]": "No changelog found. Please add a changelog file. Refer to the galaxy.md file for more info.",
+ "galaxy[version-missing]": "galaxy.yaml should have version tag.",
+ "galaxy[version-incorrect]": "collection version should be greater than or equal to 1.0.0",
+ "galaxy[no-runtime]": "meta/runtime.yml file not found.",
+ }
+
+ def matchplay(self, file: Lintable, data: dict[str, Any]) -> list[MatchError]:
+ """Return matches found for a specific play (entry in playbook)."""
+ if file.kind != "galaxy": # type: ignore[comparison-overlap]
+ return []
+
+ # Defined by Automation Hub Team and Partner Engineering
+ required_tag_list = [
+ "application",
+ "cloud",
+ "database",
+ "infrastructure",
+ "linux",
+ "monitoring",
+ "networking",
+ "security",
+ "storage",
+ "tools",
+ "windows",
+ ]
+
+ results = []
+
+ base_path = file.path.parent.resolve()
+ changelog_found = 0
+ changelog_paths = [
+ base_path / "changelogs" / "changelog.yaml",
+ base_path / "CHANGELOG.rst",
+ base_path / "CHANGELOG.md",
+ ]
+
+ for path in changelog_paths:
+ if path.is_file():
+ changelog_found = 1
+
+ galaxy_tag_list = data.get("tags", None)
+
+ # Changelog Check - building off Galaxy rule as there is no current way to check
+ # for a nonexistent file
+ if not changelog_found:
+ results.append(
+ self.create_matcherror(
+ message="No changelog found. Please add a changelog file. Refer to the galaxy.md file for more info.",
+ tag="galaxy[no-changelog]",
+ filename=file,
+ ),
+ )
+
+ # Checking if galaxy.yml contains one or more required tags for certification
+ if not galaxy_tag_list or not any(
+ tag in required_tag_list for tag in galaxy_tag_list
+ ):
+ results.append(
+ self.create_matcherror(
+ message=(
+ f"galaxy.yaml must have one of the required tags: {required_tag_list}"
+ ),
+ tag="galaxy[tags]",
+ filename=file,
+ ),
+ )
+
+ if "version" not in data:
+ results.append(
+ self.create_matcherror(
+ message="galaxy.yaml should have version tag.",
+ lineno=data[LINE_NUMBER_KEY],
+ tag="galaxy[version-missing]",
+ filename=file,
+ ),
+ )
+ return results
+ # returning here as it does not make sense
+ # to continue for version check below
+
+ version = data.get("version")
+ if Version(version) < Version("1.0.0"):
+ results.append(
+ self.create_matcherror(
+ message="collection version should be greater than or equal to 1.0.0",
+ # pylint: disable=protected-access
+ lineno=version._line_number, # noqa: SLF001
+ tag="galaxy[version-incorrect]",
+ filename=file,
+ ),
+ )
+
+ if not (base_path / "meta" / "runtime.yml").is_file():
+ results.append(
+ self.create_matcherror(
+ message="meta/runtime.yml file not found.",
+ tag="galaxy[no-runtime]",
+ filename=file,
+ ),
+ )
+
+ return results
+
+
+@total_ordering
+class Version:
+ """Simple class to compare arbitrary versions."""
+
+ def __init__(self, version_string: str):
+ """Construct a Version object."""
+ self.components = version_string.split(".")
+
+ def __eq__(self, other: object) -> bool:
+ """Implement equality comparison."""
+ try:
+ other = _coerce(other)
+ except NotImplementedError:
+ return NotImplemented
+
+ return self.components == other.components
+
+ def __lt__(self, other: Version) -> bool:
+ """Implement lower-than operation."""
+ other = _coerce(other)
+
+ return self.components < other.components
+
+
+def _coerce(other: object) -> Version:
+ if isinstance(other, str):
+ other = Version(other)
+ if isinstance(other, (int, float)):
+ other = Version(str(other))
+ if isinstance(other, Version):
+ return other
+ msg = f"Unable to coerce object type {type(other)} to Version"
+ raise NotImplementedError(msg)
+
+
+if "pytest" in sys.modules:
+ import pytest
+
+ from ansiblelint.rules import RulesCollection # pylint: disable=ungrouped-imports
+ from ansiblelint.runner import Runner
+
+ def test_galaxy_collection_version_positive() -> None:
+ """Positive test for collection version in galaxy."""
+ collection = RulesCollection()
+ collection.register(GalaxyRule())
+ success = "examples/collection/galaxy.yml"
+ good_runner = Runner(success, rules=collection)
+ assert [] == good_runner.run()
+
+ def test_galaxy_collection_version_negative() -> None:
+ """Negative test for collection version in galaxy."""
+ collection = RulesCollection()
+ collection.register(GalaxyRule())
+ failure = "examples/meta/galaxy.yml"
+ bad_runner = Runner(failure, rules=collection)
+ errs = bad_runner.run()
+ assert len(errs) == 1
+
+ def test_galaxy_no_collection_version() -> None:
+ """Test for no collection version in galaxy."""
+ collection = RulesCollection()
+ collection.register(GalaxyRule())
+ failure = "examples/no_collection_version/galaxy.yml"
+ bad_runner = Runner(failure, rules=collection)
+ errs = bad_runner.run()
+ assert len(errs) == 1
+
+ def test_version_class() -> None:
+ """Test for version class."""
+ v = Version("1.0.0")
+ assert v == Version("1.0.0")
+ assert v != NotImplemented
+
+ def test_coerce() -> None:
+ """Test for _coerce function."""
+ assert _coerce("1.0") == Version("1.0")
+ assert _coerce(1.0) == Version("1.0")
+ expected = "Unable to coerce object type"
+ with pytest.raises(NotImplementedError, match=expected):
+ _coerce(type(Version))
+
+ @pytest.mark.parametrize(
+ ("file", "expected"),
+ (
+ pytest.param(
+ "examples/galaxy_no_required_tags/fail/galaxy.yml",
+ ["galaxy[tags]"],
+ id="tags",
+ ),
+ pytest.param(
+ "examples/galaxy_no_required_tags/pass/galaxy.yml",
+ [],
+ id="pass",
+ ),
+ pytest.param(
+ "examples/collection/galaxy.yml",
+ ["schema[galaxy]"],
+ id="schema",
+ ),
+ pytest.param(
+ "examples/no_changelog/galaxy.yml",
+ ["galaxy[no-changelog]"],
+ id="no-changelog",
+ ),
+ pytest.param(
+ "examples/no_collection_version/galaxy.yml",
+ ["schema[galaxy]", "galaxy[version-missing]"],
+ id="no-collection-version",
+ ),
+ ),
+ )
+ def test_galaxy_rule(
+ default_rules_collection: RulesCollection,
+ file: str,
+ expected: list[str],
+ ) -> None:
+ """Validate that rule works as intended."""
+ results = Runner(file, rules=default_rules_collection).run()
+
+ assert len(results) == len(expected)
+ for index, result in enumerate(results):
+ assert result.tag == expected[index]