* 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>
... | ... |
@@ -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 |