diff --git a/src/ansible_builder/constants.py b/src/ansible_builder/constants.py index 87088b32..1b4a1cf0 100644 --- a/src/ansible_builder/constants.py +++ b/src/ansible_builder/constants.py @@ -48,3 +48,5 @@ DEFAULT_EE_BASENAME = "execution-environment" YAML_FILENAME_EXTENSIONS = ('yml', 'yaml') + +REQUIRE_ANSIBLE_CORE_PIN = False diff --git a/src/ansible_builder/user_definition.py b/src/ansible_builder/user_definition.py index eb78b521..2bb40958 100644 --- a/src/ansible_builder/user_definition.py +++ b/src/ansible_builder/user_definition.py @@ -9,6 +9,7 @@ from typing import Callable import yaml +from packaging.requirements import InvalidRequirement, Requirement from . import constants from .exceptions import DefinitionError @@ -239,6 +240,30 @@ def _validate_additional_build_files(self): if dest.is_absolute() or '..' in dest.parts: raise DefinitionError(f"'dest' must not be an absolute path or contain '..': {dest}") + def _validate_ansible_core_ref(self): + """ + If a downstream has patched REQUIRE_ANSIBLE_CORE_PIN validate + that the 'ansible_core' ref contains a version constraint. + + :raises: DefinitionError exception if version constraint is invalid or missing + """ + if not constants.REQUIRE_ANSIBLE_CORE_PIN: + return + + ref = self.ansible_core_ref + if not ref: + return + + try: + req = Requirement(ref) + except InvalidRequirement as e: + raise DefinitionError("Invalid package requirement specified for 'ansible_core'") from e + + if not req.specifier: + raise DefinitionError( + "Value for 'ansible_core' must contain a version constraint, such as 'ansible-core==2.16.*'" + ) + def validate(self): """ Check that all specified keys in the definition file are valid. @@ -247,6 +272,8 @@ def validate(self): """ validate_schema(self.raw) + self._validate_ansible_core_ref() + for item in constants.CONTEXT_FILES: for exclude in (False, True): requirement_path = self.get_dep_abs_path(item, exclude=exclude) diff --git a/test/unit/test_user_definition.py b/test/unit/test_user_definition.py index 3928f862..d651f104 100644 --- a/test/unit/test_user_definition.py +++ b/test/unit/test_user_definition.py @@ -173,7 +173,7 @@ def test_v3_ansible_install_refs(self, exec_env_definition_file): {'version': 3, 'images': { 'base_image': {'name': 'base_image:latest'}}, 'dependencies': { - 'ansible_core': {'package_pip': 'ansible-core==2.13'}, + 'ansible_core': {'package_pip': 'ansible-core'}, 'ansible_runner': { 'package_pip': 'ansible-runner==2.3.1'} } } @@ -181,9 +181,45 @@ def test_v3_ansible_install_refs(self, exec_env_definition_file): ) definition = UserDefinition(path) definition.validate() - assert definition.ansible_core_ref == "ansible-core==2.13" + assert definition.ansible_core_ref == "ansible-core" assert definition.ansible_runner_ref == "ansible-runner==2.3.1" - assert definition.ansible_ref_install_list == "ansible-core==2.13 ansible-runner==2.3.1" + assert definition.ansible_ref_install_list == "ansible-core ansible-runner==2.3.1" + + def test_v3_ansible_install_ref_pin_required(self, monkeypatch, exec_env_definition_file): + path = exec_env_definition_file( + """ + {'version': 3, + 'images': { 'base_image': {'name': 'base_image:latest'}}, + 'dependencies': { + 'ansible_core': {'package_pip': 'ansible-core'}, + 'ansible_runner': { 'package_pip': 'ansible-runner==2.3.1'} + } + } + """ + ) + monkeypatch.setattr(constants, 'REQUIRE_ANSIBLE_CORE_PIN', True) + definition = UserDefinition(path) + with pytest.raises(DefinitionError) as error: + definition.validate() + assert "Value for 'ansible_core' must contain a version constraint" in str(error.value.args[0]) + + def test_v3_ansible_install_ref_bad_req(self, monkeypatch, exec_env_definition_file): + path = exec_env_definition_file( + """ + {'version': 3, + 'images': { 'base_image': {'name': 'base_image:latest'}}, + 'dependencies': { + 'ansible_core': {'package_pip': 'ansible-core=2.6.0'}, + 'ansible_runner': { 'package_pip': 'ansible-runner==2.3.1'} + } + } + """ + ) + monkeypatch.setattr(constants, 'REQUIRE_ANSIBLE_CORE_PIN', True) + definition = UserDefinition(path) + with pytest.raises(DefinitionError) as error: + definition.validate() + assert "Invalid package requirement specified for 'ansible_core'" in str(error.value.args[0]) def test_v3_inline_python(self, exec_env_definition_file): """