From 7acae62fa849481b2a5e2e2d56961c5e1dcea96c Mon Sep 17 00:00:00 2001
From: Jordan Borean <jborean93@gmail.com>
Date: Wed, 6 Nov 2019 01:34:50 +1000
Subject: [PATCH] Fix up role version pagination for Galaxy install (#64373)

* Fix up role version pagination for Galaxy install

* Fix sanity issue
---
 changelogs/fragments/galaxy-role-version.yaml |  2 +
 lib/ansible/galaxy/api.py                     | 18 +++++--
 test/units/galaxy/test_api.py                 | 47 +++++++++++++++++++
 3 files changed, 64 insertions(+), 3 deletions(-)
 create mode 100644 changelogs/fragments/galaxy-role-version.yaml

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'