diff --git a/changelogs/fragments/77630-ansible-galaxy-fix-unsupported-resolvelib-version.yml b/changelogs/fragments/77630-ansible-galaxy-fix-unsupported-resolvelib-version.yml new file mode 100644 index 00000000000..22f512674a5 --- /dev/null +++ b/changelogs/fragments/77630-ansible-galaxy-fix-unsupported-resolvelib-version.yml @@ -0,0 +1,2 @@ +bugfixes: + - ansible-galaxy - handle unsupported versions of resolvelib gracefully. diff --git a/lib/ansible/galaxy/collection/__init__.py b/lib/ansible/galaxy/collection/__init__.py index 0e53339eef1..db952f12dc7 100644 --- a/lib/ansible/galaxy/collection/__init__.py +++ b/lib/ansible/galaxy/collection/__init__.py @@ -27,8 +27,19 @@ from collections import namedtuple from contextlib import contextmanager from hashlib import sha256 from io import BytesIO +from importlib.metadata import distribution, PackageNotFoundError from itertools import chain +try: + from packaging.requirements import Requirement as PkgReq +except ImportError: + class PkgReq: # type: ignore[no-redef] + pass + + HAS_PACKAGING = False +else: + HAS_PACKAGING = True + if t.TYPE_CHECKING: from ansible.galaxy.collection.concrete_artifact_manager import ( ConcreteArtifactsManager, @@ -79,16 +90,27 @@ from ansible.galaxy.collection.gpg import ( get_signature_from_source, GPG_ERROR_MAP, ) -from ansible.galaxy.dependency_resolution import ( - build_collection_dependency_resolver, -) +try: + from ansible.galaxy.dependency_resolution import ( + build_collection_dependency_resolver, + ) + from ansible.galaxy.dependency_resolution.errors import ( + CollectionDependencyResolutionImpossible, + CollectionDependencyInconsistentCandidate, + ) + from ansible.galaxy.dependency_resolution.providers import ( + RESOLVELIB_VERSION, + RESOLVELIB_LOWERBOUND, + RESOLVELIB_UPPERBOUND, + ) +except ImportError: + HAS_RESOLVELIB = False +else: + HAS_RESOLVELIB = True + from ansible.galaxy.dependency_resolution.dataclasses import ( Candidate, Requirement, _is_installed_collection_dir, ) -from ansible.galaxy.dependency_resolution.errors import ( - CollectionDependencyResolutionImpossible, - CollectionDependencyInconsistentCandidate, -) from ansible.galaxy.dependency_resolution.versioning import meets_requirements from ansible.module_utils.six import raise_from from ansible.module_utils._text import to_bytes, to_native, to_text @@ -1566,6 +1588,27 @@ def _resolve_depenency_map( include_signatures, # type: bool ): # type: (...) -> dict[str, Candidate] """Return the resolved dependency map.""" + if not HAS_RESOLVELIB: + raise AnsibleError("Failed to import resolvelib, check that a supported version is installed") + if not HAS_PACKAGING: + raise AnsibleError("Failed to import packaging, check that a supported version is installed") + try: + dist = distribution('ansible-core') + except PackageNotFoundError: + req = None + else: + req = next((rr for r in (dist.requires or []) if (rr := PkgReq(r)).name == 'resolvelib'), None) + finally: + if req is None: + # TODO: replace the hardcoded versions with a warning if the dist info is missing + # display.warning("Unable to find 'ansible-core' distribution requirements to verify the resolvelib version is supported.") + if not RESOLVELIB_LOWERBOUND <= RESOLVELIB_VERSION < RESOLVELIB_UPPERBOUND: + raise AnsibleError( + f"ansible-galaxy requires resolvelib<{RESOLVELIB_UPPERBOUND.vstring},>={RESOLVELIB_LOWERBOUND.vstring}" + ) + elif req.specifier.contains(RESOLVELIB_VERSION.vstring): + raise AnsibleError(f"ansible-galaxy requires {req.name}{req.specifier}") + collection_dep_resolver = build_collection_dependency_resolver( galaxy_apis=galaxy_apis, concrete_artifacts_manager=concrete_artifacts_manager, diff --git a/lib/ansible/galaxy/dependency_resolution/errors.py b/lib/ansible/galaxy/dependency_resolution/errors.py index f5339a4d519..ae3b4396d70 100644 --- a/lib/ansible/galaxy/dependency_resolution/errors.py +++ b/lib/ansible/galaxy/dependency_resolution/errors.py @@ -6,7 +6,14 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from resolvelib.resolvers import ( - ResolutionImpossible as CollectionDependencyResolutionImpossible, - InconsistentCandidate as CollectionDependencyInconsistentCandidate, -) +try: + from resolvelib.resolvers import ( + ResolutionImpossible as CollectionDependencyResolutionImpossible, + InconsistentCandidate as CollectionDependencyInconsistentCandidate, + ) +except ImportError: + class CollectionDependencyResolutionImpossible(Exception): # type: ignore[no-redef] + pass + + class CollectionDependencyInconsistentCandidate(Exception): # type: ignore[no-redef] + pass diff --git a/lib/ansible/galaxy/dependency_resolution/providers.py b/lib/ansible/galaxy/dependency_resolution/providers.py index 9d82513c268..5a6fcacfd47 100644 --- a/lib/ansible/galaxy/dependency_resolution/providers.py +++ b/lib/ansible/galaxy/dependency_resolution/providers.py @@ -25,10 +25,24 @@ from ansible.galaxy.dependency_resolution.versioning import ( meets_requirements, ) from ansible.module_utils.six import string_types -from ansible.utils.version import SemanticVersion +from ansible.utils.version import SemanticVersion, LooseVersion from collections.abc import Set -from resolvelib import AbstractProvider + +try: + from resolvelib import AbstractProvider + from resolvelib import __version__ as resolvelib_version +except ImportError: + class AbstractProvider: # type: ignore[no-redef] + pass + + resolvelib_version = '0.0.0' + + +# TODO: add python requirements to ansible-test's ansible-core distribution info and remove the hardcoded lowerbound/upperbound fallback +RESOLVELIB_LOWERBOUND = SemanticVersion("0.5.3") +RESOLVELIB_UPPERBOUND = SemanticVersion("0.6.0") +RESOLVELIB_VERSION = SemanticVersion.from_loose_version(LooseVersion(resolvelib_version)) class PinnedCandidateRequests(Set): diff --git a/lib/ansible/galaxy/dependency_resolution/reporters.py b/lib/ansible/galaxy/dependency_resolution/reporters.py index d8eacb70df1..69908b22435 100644 --- a/lib/ansible/galaxy/dependency_resolution/reporters.py +++ b/lib/ansible/galaxy/dependency_resolution/reporters.py @@ -6,7 +6,11 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from resolvelib import BaseReporter +try: + from resolvelib import BaseReporter +except ImportError: + class BaseReporter: # type: ignore[no-redef] + pass class CollectionDependencyReporter(BaseReporter): diff --git a/lib/ansible/galaxy/dependency_resolution/resolvers.py b/lib/ansible/galaxy/dependency_resolution/resolvers.py index 1b3e30ff86f..87ca38d5d42 100644 --- a/lib/ansible/galaxy/dependency_resolution/resolvers.py +++ b/lib/ansible/galaxy/dependency_resolution/resolvers.py @@ -6,7 +6,11 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from resolvelib import Resolver +try: + from resolvelib import Resolver +except ImportError: + class Resolver: # type: ignore[no-redef] + pass class CollectionDependencyResolver(Resolver): diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/main.yml b/test/integration/targets/ansible-galaxy-collection/tasks/main.yml index 598784d3ad3..c3b124e9951 100644 --- a/test/integration/targets/ansible-galaxy-collection/tasks/main.yml +++ b/test/integration/targets/ansible-galaxy-collection/tasks/main.yml @@ -50,6 +50,12 @@ src: ansible.cfg.j2 dest: '{{ galaxy_dir }}/ansible.cfg' +- name: test install command using an unsupported version of resolvelib + include_tasks: unsupported_resolvelib.yml + loop: "{{ unsupported_resolvelib_versions }}" + loop_control: + loop_var: resolvelib_version + - name: run ansible-galaxy collection publish tests for {{ test_name }} include_tasks: publish.yml args: diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/unsupported_resolvelib.yml b/test/integration/targets/ansible-galaxy-collection/tasks/unsupported_resolvelib.yml new file mode 100644 index 00000000000..d9667b93316 --- /dev/null +++ b/test/integration/targets/ansible-galaxy-collection/tasks/unsupported_resolvelib.yml @@ -0,0 +1,42 @@ +- vars: + venv_cmd: "{{ ansible_python_interpreter ~ ' -m venv' }}" + venv_dest: "{{ galaxy_dir }}/test_resolvelib_{{ resolvelib_version }}" + block: + - name: install another version of resolvelib that is unsupported by ansible-galaxy + pip: + name: resolvelib + version: "{{ resolvelib_version }}" + state: present + virtualenv_command: "{{ venv_cmd }}" + virtualenv: "{{ venv_dest }}" + virtualenv_site_packages: True + + - name: create test collection install directory - {{ test_name }} + file: + path: '{{ galaxy_dir }}/ansible_collections' + state: directory + + - name: install simple collection from first accessible server (expected failure) + command: "ansible-galaxy collection install namespace1.name1 {{ galaxy_verbosity }}" + environment: + ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections' + PATH: "{{ venv_dest }}/bin:{{ ansible_env.PATH }}" + register: resolvelib_version_error + ignore_errors: yes + + - assert: + that: + - resolvelib_version_error is failed + - compat_error in resolvelib_version_error.stderr or import_error in resolvelib_version_error.stderr + vars: + import_error: "Failed to import resolvelib" + compat_error: "ansible-galaxy requires resolvelib<0.6.0,>=0.5.3" + + always: + - name: cleanup venv and install directory + file: + path: '{{ galaxy_dir }}/ansible_collections' + state: absent + loop: + - '{{ galaxy_dir }}/ansible_collections' + - '{{ venv_dest }}' diff --git a/test/integration/targets/ansible-galaxy-collection/vars/main.yml b/test/integration/targets/ansible-galaxy-collection/vars/main.yml index 604ff1ab6a7..260f90e70f2 100644 --- a/test/integration/targets/ansible-galaxy-collection/vars/main.yml +++ b/test/integration/targets/ansible-galaxy-collection/vars/main.yml @@ -2,6 +2,11 @@ galaxy_verbosity: "{{ '' if not ansible_verbosity else '-' ~ ('v' * ansible_verb gpg_homedir: "{{ galaxy_dir }}/gpg" +unsupported_resolvelib_versions: + - "0.2.0" # Fails on import + - "0.5.1" + - "0.6.0" # Fails on dependency resolution + pulp_repositories: - published - secondary diff --git a/test/sanity/ignore.txt b/test/sanity/ignore.txt index 4c4e43669ea..873fddd9a18 100644 --- a/test/sanity/ignore.txt +++ b/test/sanity/ignore.txt @@ -4,54 +4,6 @@ docs/docsite/rst/locales/ja/LC_MESSAGES/dev_guide.po no-smart-quotes # Translat examples/scripts/ConfigureRemotingForAnsible.ps1 pslint:PSCustomUseLiteralPath examples/scripts/upgrade_to_ps3.ps1 pslint:PSCustomUseLiteralPath examples/scripts/upgrade_to_ps3.ps1 pslint:PSUseApprovedVerbs -lib/ansible/cli/galaxy.py import-3.8 # unguarded indirect resolvelib import -lib/ansible/galaxy/collection/__init__.py import-3.8 # unguarded resolvelib import -lib/ansible/galaxy/collection/concrete_artifact_manager.py import-3.8 # unguarded resolvelib import -lib/ansible/galaxy/collection/galaxy_api_proxy.py import-3.8 # unguarded resolvelib imports -lib/ansible/galaxy/collection/gpg.py import-3.8 # unguarded resolvelib imports -lib/ansible/galaxy/dependency_resolution/__init__.py import-3.8 # circular imports -lib/ansible/galaxy/dependency_resolution/dataclasses.py import-3.8 # circular imports -lib/ansible/galaxy/dependency_resolution/errors.py import-3.8 # circular imports -lib/ansible/galaxy/dependency_resolution/providers.py import-3.8 # circular imports -lib/ansible/galaxy/dependency_resolution/reporters.py import-3.8 # circular imports -lib/ansible/galaxy/dependency_resolution/resolvers.py import-3.8 # circular imports -lib/ansible/galaxy/dependency_resolution/versioning.py import-3.8 # circular imports -lib/ansible/cli/galaxy.py import-3.9 # unguarded indirect resolvelib import -lib/ansible/galaxy/collection/__init__.py import-3.9 # unguarded resolvelib import -lib/ansible/galaxy/collection/concrete_artifact_manager.py import-3.9 # unguarded resolvelib import -lib/ansible/galaxy/collection/galaxy_api_proxy.py import-3.9 # unguarded resolvelib imports -lib/ansible/galaxy/collection/gpg.py import-3.9 # unguarded resolvelib imports -lib/ansible/galaxy/dependency_resolution/__init__.py import-3.9 # circular imports -lib/ansible/galaxy/dependency_resolution/dataclasses.py import-3.9 # circular imports -lib/ansible/galaxy/dependency_resolution/errors.py import-3.9 # circular imports -lib/ansible/galaxy/dependency_resolution/providers.py import-3.9 # circular imports -lib/ansible/galaxy/dependency_resolution/reporters.py import-3.9 # circular imports -lib/ansible/galaxy/dependency_resolution/resolvers.py import-3.9 # circular imports -lib/ansible/galaxy/dependency_resolution/versioning.py import-3.9 # circular imports -lib/ansible/cli/galaxy.py import-3.10 # unguarded indirect resolvelib import -lib/ansible/galaxy/collection/__init__.py import-3.10 # unguarded resolvelib import -lib/ansible/galaxy/collection/concrete_artifact_manager.py import-3.10 # unguarded resolvelib import -lib/ansible/galaxy/collection/galaxy_api_proxy.py import-3.10 # unguarded resolvelib imports -lib/ansible/galaxy/collection/gpg.py import-3.10 # unguarded resolvelib imports -lib/ansible/galaxy/dependency_resolution/__init__.py import-3.10 # circular imports -lib/ansible/galaxy/dependency_resolution/dataclasses.py import-3.10 # circular imports -lib/ansible/galaxy/dependency_resolution/errors.py import-3.10 # circular imports -lib/ansible/galaxy/dependency_resolution/providers.py import-3.10 # circular imports -lib/ansible/galaxy/dependency_resolution/reporters.py import-3.10 # circular imports -lib/ansible/galaxy/dependency_resolution/resolvers.py import-3.10 # circular imports -lib/ansible/galaxy/dependency_resolution/versioning.py import-3.10 # circular imports -lib/ansible/cli/galaxy.py import-3.11 # unguarded indirect resolvelib import -lib/ansible/galaxy/collection/__init__.py import-3.11 # unguarded resolvelib import -lib/ansible/galaxy/collection/concrete_artifact_manager.py import-3.11 # unguarded resolvelib import -lib/ansible/galaxy/collection/galaxy_api_proxy.py import-3.11 # unguarded resolvelib imports -lib/ansible/galaxy/collection/gpg.py import-3.11 # unguarded resolvelib imports -lib/ansible/galaxy/dependency_resolution/__init__.py import-3.11 # circular imports -lib/ansible/galaxy/dependency_resolution/dataclasses.py import-3.11 # circular imports -lib/ansible/galaxy/dependency_resolution/errors.py import-3.11 # circular imports -lib/ansible/galaxy/dependency_resolution/providers.py import-3.11 # circular imports -lib/ansible/galaxy/dependency_resolution/reporters.py import-3.11 # circular imports -lib/ansible/galaxy/dependency_resolution/resolvers.py import-3.11 # circular imports -lib/ansible/galaxy/dependency_resolution/versioning.py import-3.11 # circular imports lib/ansible/cli/scripts/ansible_connection_cli_stub.py shebang lib/ansible/config/base.yml no-unwanted-files lib/ansible/executor/playbook_executor.py pylint:disallowed-name