diff --git a/changelogs/fragments/handle-role-dependency-type-error.yml b/changelogs/fragments/handle-role-dependency-type-error.yml new file mode 100644 index 00000000000..7b2ac45b2d3 --- /dev/null +++ b/changelogs/fragments/handle-role-dependency-type-error.yml @@ -0,0 +1,2 @@ +bugfixes: + - ansible-galaxy - Fix unhandled traceback if a role's dependencies in meta/main.yml or meta/requirements.yml are not lists. diff --git a/lib/ansible/cli/galaxy.py b/lib/ansible/cli/galaxy.py index 6742b66f8ec..cb37291eeec 100755 --- a/lib/ansible/cli/galaxy.py +++ b/lib/ansible/cli/galaxy.py @@ -1376,9 +1376,10 @@ class GalaxyCLI(CLI): # install dependencies, if we want them if not no_deps and installed: if not role.metadata: + # NOTE: the meta file is also required for installing the role, not just dependencies display.warning("Meta file %s is empty. Skipping dependencies." % role.path) else: - role_dependencies = (role.metadata.get('dependencies') or []) + role.requirements + role_dependencies = role.metadata_dependencies + role.requirements for dep in role_dependencies: display.debug('Installing dep %s' % dep) dep_req = RoleRequirement() diff --git a/lib/ansible/galaxy/role.py b/lib/ansible/galaxy/role.py index 058174c931e..7a13c971575 100644 --- a/lib/ansible/galaxy/role.py +++ b/lib/ansible/galaxy/role.py @@ -27,14 +27,16 @@ import datetime import os import tarfile import tempfile -from ansible.module_utils.compat.version import LooseVersion + +from collections.abc import MutableSequence from shutil import rmtree from ansible import context -from ansible.errors import AnsibleError +from ansible.errors import AnsibleError, AnsibleParserError from ansible.galaxy.user_agent import user_agent from ansible.module_utils._text import to_native, to_text from ansible.module_utils.common.yaml import yaml_dump, yaml_load +from ansible.module_utils.compat.version import LooseVersion from ansible.module_utils.urls import open_url from ansible.playbook.role.requirement import RoleRequirement from ansible.utils.display import Display @@ -53,6 +55,7 @@ class GalaxyRole(object): def __init__(self, galaxy, api, name, src=None, version=None, scm=None, path=None): self._metadata = None + self._metadata_dependencies = None self._requirements = None self._install_info = None self._validate_certs = not context.CLIARGS['ignore_certs'] @@ -120,6 +123,24 @@ class GalaxyRole(object): return self._metadata + @property + def metadata_dependencies(self): + """ + Returns a list of dependencies from role metadata + """ + if self._metadata_dependencies is None: + self._metadata_dependencies = [] + + if self.metadata is not None: + self._metadata_dependencies = self.metadata.get('dependencies') or [] + + if not isinstance(self._metadata_dependencies, MutableSequence): + raise AnsibleParserError( + f"Expected role dependencies to be a list. Role {self} has meta/main.yml with dependencies {self._metadata_dependencies}" + ) + + return self._metadata_dependencies + @property def install_info(self): """ @@ -405,4 +426,7 @@ class GalaxyRole(object): break + if not isinstance(self._requirements, MutableSequence): + raise AnsibleParserError(f"Expected role dependencies to be a list. Role {self} has meta/requirements.yml {self._requirements}") + return self._requirements diff --git a/lib/ansible/playbook/role/metadata.py b/lib/ansible/playbook/role/metadata.py index eac71ee4e39..edcbc661710 100644 --- a/lib/ansible/playbook/role/metadata.py +++ b/lib/ansible/playbook/role/metadata.py @@ -72,6 +72,7 @@ class RoleMetadata(Base, CollectionSearch): raise AnsibleParserError("Expected role dependencies to be a list.", obj=self._ds) for role_def in ds: + # FIXME: consolidate with ansible-galaxy to keep this in sync if isinstance(role_def, string_types) or 'role' in role_def or 'name' in role_def: roles.append(role_def) continue diff --git a/test/integration/targets/ansible-galaxy/runme.sh b/test/integration/targets/ansible-galaxy/runme.sh index 4bb459588ad..4005531a712 100755 --- a/test/integration/targets/ansible-galaxy/runme.sh +++ b/test/integration/targets/ansible-galaxy/runme.sh @@ -194,6 +194,70 @@ popd # ${galaxy_testdir} rm -fr "${galaxy_testdir}" rm -fr "${HOME}/.ansible/roles/${galaxy_local_test_role}" +# Galaxy install test case (expected failure) +# +# Install role with a meta/requirements.yml that is not a list of roles +mkdir -p "${role_testdir}" +pushd "${role_testdir}" + + ansible-galaxy role init --init-path . unsupported_requirements_format + cat < ./unsupported_requirements_format/meta/requirements.yml +roles: + - src: git+file:///${galaxy_local_test_role_git_repo} +EOF + tar czvf unsupported_requirements_format.tar.gz unsupported_requirements_format + + set +e + ansible-galaxy role install -p ./roles unsupported_requirements_format.tar.gz 2>&1 | tee out.txt + rc="$?" + set -e + + # Test that installing the role was an error + [[ ! "$rc" == 0 ]] + grep out.txt -qe 'Expected role dependencies to be a list.' + + # Test that the role was not installed to the expected directory + [[ ! -d "${HOME}/.ansible/roles/unsupported_requirements_format" ]] + +popd # ${role_testdir} +rm -rf "${role_testdir}" + +# Galaxy install test case (expected failure) +# +# Install role with meta/main.yml dependencies that is not a list of roles +mkdir -p "${role_testdir}" +pushd "${role_testdir}" + + ansible-galaxy role init --init-path . unsupported_requirements_format + cat < ./unsupported_requirements_format/meta/main.yml +galaxy_info: + author: Ansible + description: test unknown dependency format (expected failure) + company: your company (optional) + license: license (GPL-2.0-or-later, MIT, etc) + min_ansible_version: 2.1 + galaxy_tags: [] +dependencies: + roles: + - src: git+file:///${galaxy_local_test_role_git_repo} +EOF + tar czvf unsupported_requirements_format.tar.gz unsupported_requirements_format + + set +e + ansible-galaxy role install -p ./roles unsupported_requirements_format.tar.gz 2>&1 | tee out.txt + rc="$?" + set -e + + # Test that installing the role was an error + [[ ! "$rc" == 0 ]] + grep out.txt -qe 'Expected role dependencies to be a list.' + + # Test that the role was not installed to the expected directory + [[ ! -d "${HOME}/.ansible/roles/unsupported_requirements_format" ]] + +popd # ${role_testdir} +rm -rf "${role_testdir}" + # Galaxy install test case # # Ensure that if both a role_file and role_path is provided, they are both