diff --git a/changelogs/fragments/galaxy-role-version.yaml b/changelogs/fragments/galaxy-role-version.yaml new file mode 100644 index 00000000000..ab236128722 --- /dev/null +++ b/changelogs/fragments/galaxy-role-version.yaml @@ -0,0 +1,2 @@ +bugfixes: +- ansible-galaxy - Fix pagination issue when retrieving role versions for install - https://github.com/ansible/ansible/issues/64355 diff --git a/lib/ansible/galaxy/api.py b/lib/ansible/galaxy/api.py index da53f2e70c4..ed9be32af3d 100644 --- a/lib/ansible/galaxy/api.py +++ b/lib/ansible/galaxy/api.py @@ -21,6 +21,12 @@ from ansible.module_utils.urls import open_url from ansible.utils.display import Display from ansible.utils.hashing import secure_hash_s +try: + from urllib.parse import urlparse +except ImportError: + # Python 2 + from urlparse import urlparse + display = Display() @@ -289,14 +295,20 @@ class GalaxyAPI: data = self._call_galaxy(url) results = data['results'] done = (data.get('next_link', None) is None) + + # https://github.com/ansible/ansible/issues/64355 + # api_server contains part of the API path but next_link includes the the /api part so strip it out. + url_info = urlparse(self.api_server) + base_url = "%s://%s/" % (url_info.scheme, url_info.netloc) + while not done: - url = _urljoin(self.api_server, data['next_link']) + url = _urljoin(base_url, data['next_link']) data = self._call_galaxy(url) results += data['results'] done = (data.get('next_link', None) is None) except Exception as e: - display.vvvv("Unable to retrive role (id=%s) data (%s), but this is not fatal so we continue: %s" - % (role_id, related, to_text(e))) + display.warning("Unable to retrieve role (id=%s) data (%s), but this is not fatal so we continue: %s" + % (role_id, related, to_text(e))) return results @g_connect(['v1']) diff --git a/test/units/galaxy/test_api.py b/test/units/galaxy/test_api.py index 11fd435f0e0..7464a7d7f4e 100644 --- a/test/units/galaxy/test_api.py +++ b/test/units/galaxy/test_api.py @@ -860,3 +860,50 @@ def test_get_collection_versions_pagination(api_version, token_type, token_ins, assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type assert mock_open.mock_calls[1][2]['headers']['Authorization'] == '%s my token' % token_type assert mock_open.mock_calls[2][2]['headers']['Authorization'] == '%s my token' % token_type + + +@pytest.mark.parametrize('responses', [ + [ + { + 'count': 2, + 'results': [{'name': '3.5.1', }, {'name': '3.5.2'}], + 'next_link': None, + 'next': None, + 'previous_link': None, + 'previous': None + }, + ], + [ + { + 'count': 2, + 'results': [{'name': '3.5.1'}], + 'next_link': '/api/v1/roles/432/versions/?page=2&page_size=50', + 'next': '/roles/432/versions/?page=2&page_size=50', + 'previous_link': None, + 'previous': None + }, + { + 'count': 2, + 'results': [{'name': '3.5.2'}], + 'next_link': None, + 'next': None, + 'previous_link': '/api/v1/roles/432/versions/?&page_size=50', + 'previous': '/roles/432/versions/?page_size=50', + }, + ] +]) +def test_get_role_versions_pagination(monkeypatch, responses): + api = get_test_galaxy_api('https://galaxy.com/api/', 'v1') + + mock_open = MagicMock() + mock_open.side_effect = [StringIO(to_text(json.dumps(r))) for r in responses] + monkeypatch.setattr(galaxy_api, 'open_url', mock_open) + + actual = api.fetch_role_related('versions', 432) + assert actual == [{'name': '3.5.1'}, {'name': '3.5.2'}] + + assert mock_open.call_count == len(responses) + + assert mock_open.mock_calls[0][1][0] == 'https://galaxy.com/api/v1/roles/432/versions/?page_size=50' + if len(responses) == 2: + assert mock_open.mock_calls[1][1][0] == 'https://galaxy.com/api/v1/roles/432/versions/?page=2&page_size=50'