diff --git a/changelogs/fragments/73114-fix-ansible-role-download.yaml b/changelogs/fragments/73114-fix-ansible-role-download.yaml new file mode 100644 index 00000000000..1c1d3afdabd --- /dev/null +++ b/changelogs/fragments/73114-fix-ansible-role-download.yaml @@ -0,0 +1,2 @@ +minor_changes: + - galaxy - support role artifact download from API response ``download_url`` location (https://github.com/ansible/ansible/issues/73103). diff --git a/lib/ansible/galaxy/role.py b/lib/ansible/galaxy/role.py index 9868f9ff54c..d0703b56711 100644 --- a/lib/ansible/galaxy/role.py +++ b/lib/ansible/galaxy/role.py @@ -65,6 +65,7 @@ class GalaxyRole(object): self.name = name self.version = version self.src = src or name + self.download_url = None self.scm = scm self.paths = [os.path.join(x, self.name) for x in galaxy.roles_paths] @@ -190,7 +191,9 @@ class GalaxyRole(object): if role_data: # first grab the file and save it to a temp location - if "github_user" in role_data and "github_repo" in role_data: + if self.download_url is not None: + archive_url = self.download_url + elif "github_user" in role_data and "github_repo" in role_data: archive_url = 'https://github.com/%s/%s/archive/%s.tar.gz' % (role_data["github_user"], role_data["github_repo"], self.version) else: archive_url = self.src @@ -259,10 +262,12 @@ class GalaxyRole(object): self.name, role_versions)) - # check if there's a source link for our role_version + # check if there's a source link/url for our role_version for role_version in role_versions: if role_version['name'] == self.version and 'source' in role_version: self.src = role_version['source'] + if role_version['name'] == self.version and 'download_url' in role_version: + self.download_url = role_version['download_url'] tmp_file = self.fetch(role_data) diff --git a/test/units/galaxy/test_role_install.py b/test/units/galaxy/test_role_install.py new file mode 100644 index 00000000000..cf990b55a6d --- /dev/null +++ b/test/units/galaxy/test_role_install.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2019, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +import os +import pytest +import tempfile + +from io import StringIO +from ansible import context +from ansible.cli.galaxy import GalaxyCLI +from ansible.galaxy import api, role, Galaxy +from ansible.module_utils._text import to_text +from ansible.utils import context_objects as co + + +def call_galaxy_cli(args): + orig = co.GlobalCLIArgs._Singleton__instance + co.GlobalCLIArgs._Singleton__instance = None + try: + GalaxyCLI(args=['ansible-galaxy', 'role'] + args).run() + finally: + co.GlobalCLIArgs._Singleton__instance = orig + + +@pytest.fixture(autouse='function') +def reset_cli_args(): + co.GlobalCLIArgs._Singleton__instance = None + yield + co.GlobalCLIArgs._Singleton__instance = None + + +@pytest.fixture(autouse=True) +def galaxy_server(): + context.CLIARGS._store = {'ignore_certs': False} + galaxy_api = api.GalaxyAPI(None, 'test_server', 'https://galaxy.ansible.com') + return galaxy_api + + +@pytest.fixture(autouse=True) +def init_role_dir(tmp_path_factory): + test_dir = to_text(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Roles Input')) + namespace = 'ansible_namespace' + role = 'role' + skeleton_path = os.path.join(os.path.dirname(os.path.split(__file__)[0]), 'cli', 'test_data', 'role_skeleton') + call_galaxy_cli(['init', '%s.%s' % (namespace, role), '-c', '--init-path', test_dir, '--role-skeleton', skeleton_path]) + + +def mock_NamedTemporaryFile(mocker, **args): + mock_ntf = mocker.MagicMock() + mock_ntf.write = mocker.MagicMock() + mock_ntf.close = mocker.MagicMock() + mock_ntf.name = None + return mock_ntf + + +@pytest.fixture(autouse=True) +def init_test(monkeypatch): + monkeypatch.setattr(tempfile, 'NamedTemporaryFile', mock_NamedTemporaryFile) + + +@pytest.fixture(autouse=True) +def mock_role_download_api(mocker, monkeypatch): + mock_role_api = mocker.MagicMock() + mock_role_api.side_effect = [ + StringIO(u''), + ] + monkeypatch.setattr(role, 'open_url', mock_role_api) + return mock_role_api + + +def test_role_download_github(mocker, galaxy_server, mock_role_download_api, monkeypatch): + mock_api = mocker.MagicMock() + mock_api.side_effect = [ + StringIO(u'{"available_versions":{"v1":"v1/"}}'), + StringIO(u'{"results":[{"id":"123","github_user":"test_owner","github_repo": "test_role"}]}'), + StringIO(u'{"results":[{"name": "0.0.1"},{"name": "0.0.2"}]}'), + ] + monkeypatch.setattr(api, 'open_url', mock_api) + + role.GalaxyRole(Galaxy(), galaxy_server, 'test_owner.test_role', version="0.0.1").install() + + assert mock_role_download_api.call_count == 1 + assert mock_role_download_api.mock_calls[0][1][0] == 'https://github.com/test_owner/test_role/archive/0.0.1.tar.gz' + + +def test_role_download_github_default_version(mocker, galaxy_server, mock_role_download_api, monkeypatch): + mock_api = mocker.MagicMock() + mock_api.side_effect = [ + StringIO(u'{"available_versions":{"v1":"v1/"}}'), + StringIO(u'{"results":[{"id":"123","github_user":"test_owner","github_repo": "test_role"}]}'), + StringIO(u'{"results":[{"name": "0.0.1"},{"name": "0.0.2"}]}'), + ] + monkeypatch.setattr(api, 'open_url', mock_api) + + role.GalaxyRole(Galaxy(), galaxy_server, 'test_owner.test_role').install() + + assert mock_role_download_api.call_count == 1 + assert mock_role_download_api.mock_calls[0][1][0] == 'https://github.com/test_owner/test_role/archive/0.0.2.tar.gz' + + +def test_role_download_github_no_download_url_for_version(mocker, galaxy_server, mock_role_download_api, monkeypatch): + mock_api = mocker.MagicMock() + mock_api.side_effect = [ + StringIO(u'{"available_versions":{"v1":"v1/"}}'), + StringIO(u'{"results":[{"id":"123","github_user":"test_owner","github_repo": "test_role"}]}'), + StringIO(u'{"results":[{"name": "0.0.1"},{"name": "0.0.2","download_url":"http://localhost:8080/test_owner/test_role/0.0.2.tar.gz"}]}'), + ] + monkeypatch.setattr(api, 'open_url', mock_api) + + role.GalaxyRole(Galaxy(), galaxy_server, 'test_owner.test_role', version="0.0.1").install() + + assert mock_role_download_api.call_count == 1 + assert mock_role_download_api.mock_calls[0][1][0] == 'https://github.com/test_owner/test_role/archive/0.0.1.tar.gz' + + +def test_role_download_url(mocker, galaxy_server, mock_role_download_api, monkeypatch): + mock_api = mocker.MagicMock() + mock_api.side_effect = [ + StringIO(u'{"available_versions":{"v1":"v1/"}}'), + StringIO(u'{"results":[{"id":"123","github_user":"test_owner","github_repo": "test_role"}]}'), + StringIO(u'{"results":[{"name": "0.0.1","download_url":"http://localhost:8080/test_owner/test_role/0.0.1.tar.gz"},' + u'{"name": "0.0.2","download_url":"http://localhost:8080/test_owner/test_role/0.0.2.tar.gz"}]}'), + ] + monkeypatch.setattr(api, 'open_url', mock_api) + + role.GalaxyRole(Galaxy(), galaxy_server, 'test_owner.test_role', version="0.0.1").install() + + assert mock_role_download_api.call_count == 1 + assert mock_role_download_api.mock_calls[0][1][0] == 'http://localhost:8080/test_owner/test_role/0.0.1.tar.gz' + + +def test_role_download_url_default_version(mocker, galaxy_server, mock_role_download_api, monkeypatch): + mock_api = mocker.MagicMock() + mock_api.side_effect = [ + StringIO(u'{"available_versions":{"v1":"v1/"}}'), + StringIO(u'{"results":[{"id":"123","github_user":"test_owner","github_repo": "test_role"}]}'), + StringIO(u'{"results":[{"name": "0.0.1","download_url":"http://localhost:8080/test_owner/test_role/0.0.1.tar.gz"},' + u'{"name": "0.0.2","download_url":"http://localhost:8080/test_owner/test_role/0.0.2.tar.gz"}]}'), + ] + monkeypatch.setattr(api, 'open_url', mock_api) + + role.GalaxyRole(Galaxy(), galaxy_server, 'test_owner.test_role').install() + + assert mock_role_download_api.call_count == 1 + assert mock_role_download_api.mock_calls[0][1][0] == 'http://localhost:8080/test_owner/test_role/0.0.2.tar.gz'