diff --git a/changelogs/fragments/77561-ansible-galaxy-coll-install-null-dependencies.yml b/changelogs/fragments/77561-ansible-galaxy-coll-install-null-dependencies.yml new file mode 100644 index 00000000000..c148e4b0551 --- /dev/null +++ b/changelogs/fragments/77561-ansible-galaxy-coll-install-null-dependencies.yml @@ -0,0 +1,4 @@ +bugfixes: + - >- + ansible-galaxy - fix installing collections that have dependencies in the metadata set to + null instead of an empty dictionary (https://github.com/ansible/ansible/issues/77560). diff --git a/lib/ansible/galaxy/collection/concrete_artifact_manager.py b/lib/ansible/galaxy/collection/concrete_artifact_manager.py index 5d2e61c949d..b63cf8fec1a 100644 --- a/lib/ansible/galaxy/collection/concrete_artifact_manager.py +++ b/lib/ansible/galaxy/collection/concrete_artifact_manager.py @@ -268,7 +268,10 @@ class ConcreteArtifactsManager: def get_direct_collection_dependencies(self, collection): # type: (Candidate | Requirement) -> dict[str, str] """Extract deps from the given on-disk collection artifact.""" - return self.get_direct_collection_meta(collection)['dependencies'] # type: ignore[return-value] + collection_dependencies = self.get_direct_collection_meta(collection)['dependencies'] + if collection_dependencies is None: + collection_dependencies = {} + return collection_dependencies # type: ignore[return-value] def get_direct_collection_meta(self, collection): # type: (Candidate | Requirement) -> dict[str, str | dict[str, str] | list[str] | None] diff --git a/test/units/galaxy/test_collection_install.py b/test/units/galaxy/test_collection_install.py index e34472f2e68..7134e73ce16 100644 --- a/test/units/galaxy/test_collection_install.py +++ b/test/units/galaxy/test_collection_install.py @@ -143,16 +143,16 @@ def collection_artifact(request, tmp_path_factory): call_galaxy_cli(['init', '%s.%s' % (namespace, collection), '-c', '--init-path', test_dir, '--collection-skeleton', skeleton_path]) - dependencies = getattr(request, 'param', None) - if dependencies: - galaxy_yml = os.path.join(collection_path, 'galaxy.yml') - with open(galaxy_yml, 'rb+') as galaxy_obj: - existing_yaml = yaml.safe_load(galaxy_obj) - existing_yaml['dependencies'] = dependencies + dependencies = getattr(request, 'param', {}) - galaxy_obj.seek(0) - galaxy_obj.write(to_bytes(yaml.safe_dump(existing_yaml))) - galaxy_obj.truncate() + galaxy_yml = os.path.join(collection_path, 'galaxy.yml') + with open(galaxy_yml, 'rb+') as galaxy_obj: + existing_yaml = yaml.safe_load(galaxy_obj) + existing_yaml['dependencies'] = dependencies + + galaxy_obj.seek(0) + galaxy_obj.write(to_bytes(yaml.safe_dump(existing_yaml))) + galaxy_obj.truncate() # Create a file with +x in the collection so we can test the permissions execute_path = os.path.join(collection_path, 'runme.sh') @@ -995,6 +995,7 @@ def test_install_collection_with_circular_dependency(collection_artifact, monkey assert actual_manifest['collection_info']['namespace'] == 'ansible_namespace' assert actual_manifest['collection_info']['name'] == 'collection' assert actual_manifest['collection_info']['version'] == '0.1.0' + assert actual_manifest['collection_info']['dependencies'] == {'ansible_namespace.collection': '>=0.0.1'} # Filter out the progress cursor display calls. display_msgs = [m[1][0] for m in mock_display.mock_calls if 'newline' not in m[2] and len(m[1]) == 1] @@ -1005,6 +1006,30 @@ def test_install_collection_with_circular_dependency(collection_artifact, monkey assert display_msgs[3] == "ansible_namespace.collection:0.1.0 was installed successfully" +@pytest.mark.parametrize('collection_artifact', [ + None, + {}, +], indirect=True) +def test_install_collection_with_no_dependency(collection_artifact, monkeypatch): + collection_path, collection_tar = collection_artifact + temp_path = os.path.split(collection_tar)[0] + shutil.rmtree(collection_path) + + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False) + requirements = [Requirement('ansible_namespace.collection', '0.1.0', to_text(collection_tar), 'file', None)] + collection.install_collections(requirements, to_text(temp_path), [], False, False, False, False, False, False, concrete_artifact_cm, True) + + assert os.path.isdir(collection_path) + + with open(os.path.join(collection_path, b'MANIFEST.json'), 'rb') as manifest_obj: + actual_manifest = json.loads(to_text(manifest_obj.read())) + + assert not actual_manifest['collection_info']['dependencies'] + assert actual_manifest['collection_info']['namespace'] == 'ansible_namespace' + assert actual_manifest['collection_info']['name'] == 'collection' + assert actual_manifest['collection_info']['version'] == '0.1.0' + + @pytest.mark.parametrize( "signatures,required_successful_count,ignore_errors,expected_success", [