Fix ansible-galaxy traceback when unexpected version of resolvelib is installed (#77630)

* Fix traceback when a supported version of resolvelib is not installed

Try to read the supported version range from the package distribution info and fall back to a hardcoded lowerbound/upperbound (>=0.5.3,<0.6.0).

* Add tests for unsupported resolvelib versions

* Resolve remaining import sanity test issues.

Co-authored-by: Matt Clay <matt@mystile.com>
Co-authored-by: Matt Martz <matt@sivel.net>
pull/77906/head
Sloane Hertel 2 years ago committed by GitHub
parent 621e782ed0
commit 82f3a57bee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,2 @@
bugfixes:
- ansible-galaxy - handle unsupported versions of resolvelib gracefully.

@ -27,8 +27,19 @@ from collections import namedtuple
from contextlib import contextmanager from contextlib import contextmanager
from hashlib import sha256 from hashlib import sha256
from io import BytesIO from io import BytesIO
from importlib.metadata import distribution, PackageNotFoundError
from itertools import chain 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: if t.TYPE_CHECKING:
from ansible.galaxy.collection.concrete_artifact_manager import ( from ansible.galaxy.collection.concrete_artifact_manager import (
ConcreteArtifactsManager, ConcreteArtifactsManager,
@ -79,16 +90,27 @@ from ansible.galaxy.collection.gpg import (
get_signature_from_source, get_signature_from_source,
GPG_ERROR_MAP, GPG_ERROR_MAP,
) )
from ansible.galaxy.dependency_resolution import ( try:
build_collection_dependency_resolver, 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 ( from ansible.galaxy.dependency_resolution.dataclasses import (
Candidate, Requirement, _is_installed_collection_dir, 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.galaxy.dependency_resolution.versioning import meets_requirements
from ansible.module_utils.six import raise_from from ansible.module_utils.six import raise_from
from ansible.module_utils._text import to_bytes, to_native, to_text from ansible.module_utils._text import to_bytes, to_native, to_text
@ -1566,6 +1588,27 @@ def _resolve_depenency_map(
include_signatures, # type: bool include_signatures, # type: bool
): # type: (...) -> dict[str, Candidate] ): # type: (...) -> dict[str, Candidate]
"""Return the resolved dependency map.""" """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( collection_dep_resolver = build_collection_dependency_resolver(
galaxy_apis=galaxy_apis, galaxy_apis=galaxy_apis,
concrete_artifacts_manager=concrete_artifacts_manager, concrete_artifacts_manager=concrete_artifacts_manager,

@ -6,7 +6,14 @@
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
from resolvelib.resolvers import ( try:
ResolutionImpossible as CollectionDependencyResolutionImpossible, from resolvelib.resolvers import (
InconsistentCandidate as CollectionDependencyInconsistentCandidate, ResolutionImpossible as CollectionDependencyResolutionImpossible,
) InconsistentCandidate as CollectionDependencyInconsistentCandidate,
)
except ImportError:
class CollectionDependencyResolutionImpossible(Exception): # type: ignore[no-redef]
pass
class CollectionDependencyInconsistentCandidate(Exception): # type: ignore[no-redef]
pass

@ -25,10 +25,24 @@ from ansible.galaxy.dependency_resolution.versioning import (
meets_requirements, meets_requirements,
) )
from ansible.module_utils.six import string_types 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 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): class PinnedCandidateRequests(Set):

@ -6,7 +6,11 @@
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
from resolvelib import BaseReporter try:
from resolvelib import BaseReporter
except ImportError:
class BaseReporter: # type: ignore[no-redef]
pass
class CollectionDependencyReporter(BaseReporter): class CollectionDependencyReporter(BaseReporter):

@ -6,7 +6,11 @@
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
from resolvelib import Resolver try:
from resolvelib import Resolver
except ImportError:
class Resolver: # type: ignore[no-redef]
pass
class CollectionDependencyResolver(Resolver): class CollectionDependencyResolver(Resolver):

@ -50,6 +50,12 @@
src: ansible.cfg.j2 src: ansible.cfg.j2
dest: '{{ galaxy_dir }}/ansible.cfg' 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 }} - name: run ansible-galaxy collection publish tests for {{ test_name }}
include_tasks: publish.yml include_tasks: publish.yml
args: args:

@ -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 }}'

@ -2,6 +2,11 @@ galaxy_verbosity: "{{ '' if not ansible_verbosity else '-' ~ ('v' * ansible_verb
gpg_homedir: "{{ galaxy_dir }}/gpg" 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: pulp_repositories:
- published - published
- secondary - secondary

@ -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/ConfigureRemotingForAnsible.ps1 pslint:PSCustomUseLiteralPath
examples/scripts/upgrade_to_ps3.ps1 pslint:PSCustomUseLiteralPath examples/scripts/upgrade_to_ps3.ps1 pslint:PSCustomUseLiteralPath
examples/scripts/upgrade_to_ps3.ps1 pslint:PSUseApprovedVerbs 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/cli/scripts/ansible_connection_cli_stub.py shebang
lib/ansible/config/base.yml no-unwanted-files lib/ansible/config/base.yml no-unwanted-files
lib/ansible/executor/playbook_executor.py pylint:disallowed-name lib/ansible/executor/playbook_executor.py pylint:disallowed-name

Loading…
Cancel
Save