Browse code

[stable-2.9] Galaxy publish fix (#63580)

* Handle galaxy v2/v3 API diffs for artifact publish response

For publishing a collection artifact
(POST /v3/collections/artifacts/), the response
format is different between v2 and v3.

For v2 galaxy, the 'task' url returned is
a full url with scheme:

{"task": "https://galaxy-dev.ansible.com/api/v2/collection-imports/35573/"}

For v3 galaxy, the task url is relative:

{"task": "/api/automation-hub/v3/imports/collections/838d1308-a8f4-402c-95cb-7823f3806cd8/"}

So check which API we are using and update the task url approriately.

* Use full url for all wait_for_import messages

Update unit tests to parameterize the expected
responses and urls.

* update explanatory comment

* Rename n_url to full_url.

* Fix issue with overwrite of the complete path

* Fixes overwrite of the complete path in case there's extra path stored
in self.api_sever
* Normalizes the input to the wait_import_task function so it receives
the same value on both v2 and v3

Builds on #63523

* Update unittests for new call signature

* Add changelog for ansible-galaxy publish API fixes.
(cherry picked from commit 4cad7e4)

Co-authored-by: Toshio Kuratomi <a.badger@gmail.com>

Toshio Kuratomi authored on 2019/10/17 07:23:12
Showing 5 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,3 @@
0
+bugfixes:
1
+  - ansible-galaxy - Handle the different task resource urls in API responses from publishing
2
+    collection artifacts to galaxy servers using v2 and v3 APIs.
... ...
@@ -15,7 +15,7 @@ from ansible import context
15 15
 from ansible.errors import AnsibleError
16 16
 from ansible.module_utils.six import string_types
17 17
 from ansible.module_utils.six.moves.urllib.error import HTTPError
18
-from ansible.module_utils.six.moves.urllib.parse import quote as urlquote, urlencode
18
+from ansible.module_utils.six.moves.urllib.parse import quote as urlquote, urlencode, urlparse
19 19
 from ansible.module_utils._text import to_bytes, to_native, to_text
20 20
 from ansible.module_utils.urls import open_url
21 21
 from ansible.utils.display import Display
... ...
@@ -439,24 +439,35 @@ class GalaxyAPI:
439 439
         return resp['task']
440 440
 
441 441
     @g_connect(['v2', 'v3'])
442
-    def wait_import_task(self, task_url, timeout=0):
442
+    def wait_import_task(self, task_id, timeout=0):
443 443
         """
444 444
         Waits until the import process on the Galaxy server has completed or the timeout is reached.
445 445
 
446
-        :param task_url: The full URI of the import task to wait for, this is returned by publish_collection.
446
+        :param task_id: The id of the import task to wait for. This can be parsed out of the return
447
+            value for GalaxyAPI.publish_collection.
447 448
         :param timeout: The timeout in seconds, 0 is no timeout.
448 449
         """
449 450
         # TODO: actually verify that v3 returns the same structure as v2, right now this is just an assumption.
450 451
         state = 'waiting'
451 452
         data = None
452 453
 
453
-        display.display("Waiting until Galaxy import task %s has completed" % task_url)
454
+        # Construct the appropriate URL per version
455
+        if 'v3' in self.available_api_versions:
456
+            full_url = _urljoin(self.api_server, 'automation-hub', self.available_api_versions['v3'],
457
+                                'imports/collections', task_id, '/')
458
+        else:
459
+            # TODO: Should we have a trailing slash here?  I'm working with what the unittests ask
460
+            # for but a trailing slash may be more correct
461
+            full_url = _urljoin(self.api_server, self.available_api_versions['v2'],
462
+                                'collection-imports', task_id)
463
+
464
+        display.display("Waiting until Galaxy import task %s has completed" % full_url)
454 465
         start = time.time()
455 466
         wait = 2
456 467
 
457 468
         while timeout == 0 or (time.time() - start) < timeout:
458
-            data = self._call_galaxy(task_url, method='GET', auth_required=True,
459
-                                     error_context_msg='Error when getting import task results at %s' % task_url)
469
+            data = self._call_galaxy(full_url, method='GET', auth_required=True,
470
+                                     error_context_msg='Error when getting import task results at %s' % full_url)
460 471
 
461 472
             state = data.get('state', 'waiting')
462 473
 
... ...
@@ -471,7 +482,7 @@ class GalaxyAPI:
471 471
             wait = min(30, wait * 1.5)
472 472
         if state == 'waiting':
473 473
             raise AnsibleError("Timeout while waiting for the Galaxy import process to finish, check progress at '%s'"
474
-                               % to_native(task_url))
474
+                               % to_native(full_url))
475 475
 
476 476
         for message in data.get('messages', []):
477 477
             level = message['level']
... ...
@@ -485,7 +496,7 @@ class GalaxyAPI:
485 485
         if state == 'failed':
486 486
             code = to_native(data['error'].get('code', 'UNKNOWN'))
487 487
             description = to_native(
488
-                data['error'].get('description', "Unknown error, see %s for more details" % task_url))
488
+                data['error'].get('description', "Unknown error, see %s for more details" % full_url))
489 489
             raise AnsibleError("Galaxy import process failed: %s (Code: %s)" % (description, code))
490 490
 
491 491
     @g_connect(['v2', 'v3'])
... ...
@@ -381,10 +381,24 @@ def publish_collection(collection_path, api, wait, timeout):
381 381
     :param timeout: The time in seconds to wait for the import process to finish, 0 is indefinite.
382 382
     """
383 383
     import_uri = api.publish_collection(collection_path)
384
+
384 385
     if wait:
386
+        # Galaxy returns a url fragment which differs between v2 and v3.  The second to last entry is
387
+        # always the task_id, though.
388
+        # v2: {"task": "https://galaxy-dev.ansible.com/api/v2/collection-imports/35573/"}
389
+        # v3: {"task": "/api/automation-hub/v3/imports/collections/838d1308-a8f4-402c-95cb-7823f3806cd8/"}
390
+        task_id = None
391
+        for path_segment in reversed(import_uri.split('/')):
392
+            if path_segment:
393
+                task_id = path_segment
394
+                break
395
+
396
+        if not task_id:
397
+            raise AnsibleError("Publishing the collection did not return valid task info. Cannot wait for task status. Returned task info: '%s'" % import_uri)
398
+
385 399
         display.display("Collection has been published to the Galaxy server %s %s" % (api.name, api.api_server))
386 400
         with _display_progress():
387
-            api.wait_import_task(import_uri, timeout)
401
+            api.wait_import_task(task_id, timeout)
388 402
         display.display("Collection has been successfully published and imported to the Galaxy server %s %s"
389 403
                         % (api.name, api.api_server))
390 404
     else:
... ...
@@ -332,13 +332,16 @@ def test_publish_failure(api_version, collection_url, response, expected, collec
332 332
         api.publish_collection(collection_artifact)
333 333
 
334 334
 
335
-@pytest.mark.parametrize('api_version, token_type, token_ins', [
336
-    ('v2', 'Token', GalaxyToken('my token')),
337
-    ('v3', 'Bearer', KeycloakToken(auth_url='https://api.test/')),
335
+@pytest.mark.parametrize('api_version, token_type, token_ins, import_uri, full_import_uri', [
336
+    ('v2', 'Token', GalaxyToken('my token'),
337
+     '1234',
338
+     'https://galaxy.server.com/api/v2/collection-imports/1234'),
339
+    ('v3', 'Bearer', KeycloakToken(auth_url='https://api.test/'),
340
+     '1234',
341
+     'https://galaxy.server.com/api/automation-hub/v3/imports/collections/1234/'),
338 342
 ])
339
-def test_wait_import_task(api_version, token_type, token_ins, monkeypatch):
343
+def test_wait_import_task(api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch):
340 344
     api = get_test_galaxy_api('https://galaxy.server.com/api/', api_version, token_ins=token_ins)
341
-    import_uri = 'https://galaxy.server.com/api/%s/task/1234' % api_version
342 345
 
343 346
     if token_ins:
344 347
         mock_token_get = MagicMock()
... ...
@@ -355,20 +358,23 @@ def test_wait_import_task(api_version, token_type, token_ins, monkeypatch):
355 355
     api.wait_import_task(import_uri)
356 356
 
357 357
     assert mock_open.call_count == 1
358
-    assert mock_open.mock_calls[0][1][0] == import_uri
358
+    assert mock_open.mock_calls[0][1][0] == full_import_uri
359 359
     assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
360 360
 
361 361
     assert mock_display.call_count == 1
362
-    assert mock_display.mock_calls[0][1][0] == 'Waiting until Galaxy import task %s has completed' % import_uri
362
+    assert mock_display.mock_calls[0][1][0] == 'Waiting until Galaxy import task %s has completed' % full_import_uri
363 363
 
364 364
 
365
-@pytest.mark.parametrize('api_version, token_type, token_ins', [
366
-    ('v2', 'Token', GalaxyToken('my token')),
367
-    ('v3', 'Bearer', KeycloakToken(auth_url='https://api.test/')),
365
+@pytest.mark.parametrize('api_version, token_type, token_ins, import_uri, full_import_uri', [
366
+    ('v2', 'Token', GalaxyToken('my token'),
367
+     '1234',
368
+     'https://galaxy.server.com/api/v2/collection-imports/1234'),
369
+    ('v3', 'Bearer', KeycloakToken(auth_url='https://api.test/'),
370
+     '1234',
371
+     'https://galaxy.server.com/api/automation-hub/v3/imports/collections/1234/'),
368 372
 ])
369
-def test_wait_import_task_multiple_requests(api_version, token_type, token_ins, monkeypatch):
373
+def test_wait_import_task_multiple_requests(api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch):
370 374
     api = get_test_galaxy_api('https://galaxy.server.com/api/', api_version, token_ins=token_ins)
371
-    import_uri = 'https://galaxy.server.com/api/%s/task/1234' % api_version
372 375
 
373 376
     if token_ins:
374 377
         mock_token_get = MagicMock()
... ...
@@ -393,26 +399,29 @@ def test_wait_import_task_multiple_requests(api_version, token_type, token_ins,
393 393
     api.wait_import_task(import_uri)
394 394
 
395 395
     assert mock_open.call_count == 2
396
-    assert mock_open.mock_calls[0][1][0] == import_uri
396
+    assert mock_open.mock_calls[0][1][0] == full_import_uri
397 397
     assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
398
-    assert mock_open.mock_calls[1][1][0] == import_uri
398
+    assert mock_open.mock_calls[1][1][0] == full_import_uri
399 399
     assert mock_open.mock_calls[1][2]['headers']['Authorization'] == '%s my token' % token_type
400 400
 
401 401
     assert mock_display.call_count == 1
402
-    assert mock_display.mock_calls[0][1][0] == 'Waiting until Galaxy import task %s has completed' % import_uri
402
+    assert mock_display.mock_calls[0][1][0] == 'Waiting until Galaxy import task %s has completed' % full_import_uri
403 403
 
404 404
     assert mock_vvv.call_count == 1
405 405
     assert mock_vvv.mock_calls[0][1][0] == \
406 406
         'Galaxy import process has a status of test, wait 2 seconds before trying again'
407 407
 
408 408
 
409
-@pytest.mark.parametrize('api_version, token_type, token_ins', [
410
-    ('v2', 'Token', GalaxyToken('my token')),
411
-    ('v3', 'Bearer', KeycloakToken(auth_url='https://api.test/')),
409
+@pytest.mark.parametrize('api_version, token_type, token_ins, import_uri, full_import_uri,', [
410
+    ('v2', 'Token', GalaxyToken('my token'),
411
+     '1234',
412
+     'https://galaxy.server.com/api/v2/collection-imports/1234'),
413
+    ('v3', 'Bearer', KeycloakToken(auth_url='https://api.test/'),
414
+     '1234',
415
+     'https://galaxy.server.com/api/automation-hub/v3/imports/collections/1234/'),
412 416
 ])
413
-def test_wait_import_task_with_failure(api_version, token_type, token_ins, monkeypatch):
417
+def test_wait_import_task_with_failure(api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch):
414 418
     api = get_test_galaxy_api('https://galaxy.server.com/api/', api_version, token_ins=token_ins)
415
-    import_uri = 'https://galaxy.server.com/api/%s/task/1234' % api_version
416 419
 
417 420
     if token_ins:
418 421
         mock_token_get = MagicMock()
... ...
@@ -464,11 +473,11 @@ def test_wait_import_task_with_failure(api_version, token_type, token_ins, monke
464 464
         api.wait_import_task(import_uri)
465 465
 
466 466
     assert mock_open.call_count == 1
467
-    assert mock_open.mock_calls[0][1][0] == import_uri
467
+    assert mock_open.mock_calls[0][1][0] == full_import_uri
468 468
     assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
469 469
 
470 470
     assert mock_display.call_count == 1
471
-    assert mock_display.mock_calls[0][1][0] == 'Waiting until Galaxy import task %s has completed' % import_uri
471
+    assert mock_display.mock_calls[0][1][0] == 'Waiting until Galaxy import task %s has completed' % full_import_uri
472 472
 
473 473
     assert mock_vvv.call_count == 1
474 474
     assert mock_vvv.mock_calls[0][1][0] == u'Galaxy import message: info - Somé info'
... ...
@@ -480,13 +489,16 @@ def test_wait_import_task_with_failure(api_version, token_type, token_ins, monke
480 480
     assert mock_err.mock_calls[0][1][0] == u'Galaxy import error message: Somé error'
481 481
 
482 482
 
483
-@pytest.mark.parametrize('api_version, token_type, token_ins', [
484
-    ('v2', 'Token', GalaxyToken('my_token')),
485
-    ('v3', 'Bearer', KeycloakToken(auth_url='https://api.test/')),
483
+@pytest.mark.parametrize('api_version, token_type, token_ins, import_uri, full_import_uri', [
484
+    ('v2', 'Token', GalaxyToken('my_token'),
485
+     '1234',
486
+     'https://galaxy.server.com/api/v2/collection-imports/1234'),
487
+    ('v3', 'Bearer', KeycloakToken(auth_url='https://api.test/'),
488
+     '1234',
489
+     'https://galaxy.server.com/api/automation-hub/v3/imports/collections/1234/'),
486 490
 ])
487
-def test_wait_import_task_with_failure_no_error(api_version, token_type, token_ins, monkeypatch):
491
+def test_wait_import_task_with_failure_no_error(api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch):
488 492
     api = get_test_galaxy_api('https://galaxy.server.com/api/', api_version, token_ins=token_ins)
489
-    import_uri = 'https://galaxy.server.com/api/%s/task/1234' % api_version
490 493
 
491 494
     if token_ins:
492 495
         mock_token_get = MagicMock()
... ...
@@ -529,16 +541,16 @@ def test_wait_import_task_with_failure_no_error(api_version, token_type, token_i
529 529
     mock_err = MagicMock()
530 530
     monkeypatch.setattr(Display, 'error', mock_err)
531 531
 
532
-    expected = 'Galaxy import process failed: Unknown error, see %s for more details (Code: UNKNOWN)' % import_uri
533
-    with pytest.raises(AnsibleError, match=re.escape(expected)):
532
+    expected = 'Galaxy import process failed: Unknown error, see %s for more details \\(Code: UNKNOWN\\)' % full_import_uri
533
+    with pytest.raises(AnsibleError, match=expected):
534 534
         api.wait_import_task(import_uri)
535 535
 
536 536
     assert mock_open.call_count == 1
537
-    assert mock_open.mock_calls[0][1][0] == import_uri
537
+    assert mock_open.mock_calls[0][1][0] == full_import_uri
538 538
     assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
539 539
 
540 540
     assert mock_display.call_count == 1
541
-    assert mock_display.mock_calls[0][1][0] == 'Waiting until Galaxy import task %s has completed' % import_uri
541
+    assert mock_display.mock_calls[0][1][0] == 'Waiting until Galaxy import task %s has completed' % full_import_uri
542 542
 
543 543
     assert mock_vvv.call_count == 1
544 544
     assert mock_vvv.mock_calls[0][1][0] == u'Galaxy import message: info - Somé info'
... ...
@@ -550,13 +562,16 @@ def test_wait_import_task_with_failure_no_error(api_version, token_type, token_i
550 550
     assert mock_err.mock_calls[0][1][0] == u'Galaxy import error message: Somé error'
551 551
 
552 552
 
553
-@pytest.mark.parametrize('api_version, token_type, token_ins', [
554
-    ('v2', 'Token', GalaxyToken('my token')),
555
-    ('v3', 'Bearer', KeycloakToken(auth_url='https://api.test/')),
553
+@pytest.mark.parametrize('api_version, token_type, token_ins, import_uri, full_import_uri', [
554
+    ('v2', 'Token', GalaxyToken('my token'),
555
+     '1234',
556
+     'https://galaxy.server.com/api/v2/collection-imports/1234'),
557
+    ('v3', 'Bearer', KeycloakToken(auth_url='https://api.test/'),
558
+     '1234',
559
+     'https://galaxy.server.com/api/automation-hub/v3/imports/collections/1234/'),
556 560
 ])
557
-def test_wait_import_task_timeout(api_version, token_type, token_ins, monkeypatch):
561
+def test_wait_import_task_timeout(api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch):
558 562
     api = get_test_galaxy_api('https://galaxy.server.com/api/', api_version, token_ins=token_ins)
559
-    import_uri = 'https://galaxy.server.com/api/%s/task/1234' % api_version
560 563
 
561 564
     if token_ins:
562 565
         mock_token_get = MagicMock()
... ...
@@ -578,18 +593,18 @@ def test_wait_import_task_timeout(api_version, token_type, token_ins, monkeypatc
578 578
 
579 579
     monkeypatch.setattr(time, 'sleep', MagicMock())
580 580
 
581
-    expected = "Timeout while waiting for the Galaxy import process to finish, check progress at '%s'" % import_uri
581
+    expected = "Timeout while waiting for the Galaxy import process to finish, check progress at '%s'" % full_import_uri
582 582
     with pytest.raises(AnsibleError, match=expected):
583 583
         api.wait_import_task(import_uri, 1)
584 584
 
585 585
     assert mock_open.call_count > 1
586
-    assert mock_open.mock_calls[0][1][0] == import_uri
586
+    assert mock_open.mock_calls[0][1][0] == full_import_uri
587 587
     assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
588
-    assert mock_open.mock_calls[1][1][0] == import_uri
588
+    assert mock_open.mock_calls[1][1][0] == full_import_uri
589 589
     assert mock_open.mock_calls[1][2]['headers']['Authorization'] == '%s my token' % token_type
590 590
 
591 591
     assert mock_display.call_count == 1
592
-    assert mock_display.mock_calls[0][1][0] == 'Waiting until Galaxy import task %s has completed' % import_uri
592
+    assert mock_display.mock_calls[0][1][0] == 'Waiting until Galaxy import task %s has completed' % full_import_uri
593 593
 
594 594
     # expected_wait_msg = 'Galaxy import process has a status of waiting, wait {0} seconds before trying again'
595 595
     assert mock_vvv.call_count > 9  # 1st is opening Galaxy token file.
... ...
@@ -439,7 +439,7 @@ def test_publish_with_wait(galaxy_server, collection_artifact, monkeypatch):
439 439
     assert mock_publish.mock_calls[0][1][0] == artifact_path
440 440
 
441 441
     assert mock_wait.call_count == 1
442
-    assert mock_wait.mock_calls[0][1][0] == fake_import_uri
442
+    assert mock_wait.mock_calls[0][1][0] == '1234'
443 443
 
444 444
     assert mock_display.mock_calls[0][1][0] == "Collection has been published to the Galaxy server test_server %s" \
445 445
         % galaxy_server.api_server