From 0ff2a0d09bc96c60c790eafc4ffd52b622fcdf92 Mon Sep 17 00:00:00 2001 From: Brian McLaughlin Date: Tue, 6 Aug 2024 13:58:45 -0400 Subject: [PATCH] Fix collection uploads --- plugins/module_utils/ah_module.py | 78 ++++++------------- plugins/modules/ah_collection.py | 12 ++- plugins/modules/ah_collection_upload.py | 10 ++- roles/collection/tasks/main.yml | 1 + .../testing_collections_playbook.yml | 15 +--- 5 files changed, 44 insertions(+), 72 deletions(-) diff --git a/plugins/module_utils/ah_module.py b/plugins/module_utils/ah_module.py index fb84662c..7e57129f 100644 --- a/plugins/module_utils/ah_module.py +++ b/plugins/module_utils/ah_module.py @@ -155,13 +155,16 @@ def __init__(self, argument_spec=None, direct_params=None, error_callback=None, def build_url(self, endpoint, query_params=None): # Make sure we start with /api/vX - if not endpoint.startswith("/"): + upload_endpoint = "content" in endpoint and "v3" in endpoint and "artifacts" in endpoint + if upload_endpoint and not endpoint.startswith("/api"): + endpoint = "/api/{0}".format(endpoint) + if not endpoint.startswith("/") and not upload_endpoint: endpoint = "/{0}".format(endpoint) - if not endpoint.startswith("/api/") and not self.path_prefix.startswith("/api/"): + if not endpoint.startswith("/api/") and not self.path_prefix.startswith("/api/") and not upload_endpoint: endpoint = "api/{0}/v3{1}".format(self.path_prefix, endpoint) - if not endpoint.startswith("/api/") and self.path_prefix.startswith("/api/"): + if not endpoint.startswith("/api/") and self.path_prefix.startswith("/api/") and not upload_endpoint: endpoint = "{0}/v3{1}".format(self.path_prefix, endpoint) - if not endpoint.endswith("/") and "?" not in endpoint: + if not endpoint.endswith("/") and "?" not in endpoint and not upload_endpoint: endpoint = "{0}/".format(endpoint) # Update the URL path with the endpoint @@ -379,56 +382,19 @@ def get_only(self, endpoint, name_or_id=None, allow_none=True, key="url", **kwar def authenticate(self, **kwargs): if self.username and self.password: - # Attempt to get a token from /v3/auth/token/ by giving it our username/password combo - # If we have a username and password, we need to get a session cookie - api_token_url = self.build_url("auth/token").geturl() - try: - try: - response = self.session.open( - "POST", - api_token_url, - validate_certs=self.verify_ssl, - timeout=self.request_timeout, - follow_redirects=True, - force_basic_auth=True, - url_username=self.username, - url_password=self.password, - headers={"Content-Type": "application/json"}, - ) - except HTTPError: - test_url = self.build_url("namespaces").geturl() - self.basic_auth = True - basic_str = base64.b64encode("{0}:{1}".format(self.username, self.password).encode("ascii")) - response = self.session.open( - "GET", - test_url, - validate_certs=self.verify_ssl, - timeout=self.request_timeout, - headers={ - "Content-Type": "application/json", - "Authorization": "Basic {0}".format(basic_str.decode("ascii")), - }, - ) - except HTTPError as he: - try: - resp = he.read() - except Exception as e: - resp = "unknown {0}".format(e) - self.fail_json(msg="Failed to get token: {0}".format(he), response=resp) - except (Exception) as e: - # Sanity check: Did the server send back some kind of internal error? - self.fail_json(msg="Failed to get token: {0}".format(e)) - - token_response = None - if not self.basic_auth: - try: - token_response = response.read() - response_json = loads(token_response) - self.oauth_token = response_json["token"] - except (Exception) as e: - self.fail_json(msg="Failed to extract token information from login response: {0}".format(e), **{"response": token_response}) - - # If we have neither of these, then we can try un-authenticated access + test_url = self.build_url("namespaces").geturl() + self.basic_auth = True + basic_str = base64.b64encode("{0}:{1}".format(self.username, self.password).encode("ascii")) + self.session.open( + "GET", + test_url, + validate_certs=self.verify_ssl, + timeout=self.request_timeout, + headers={ + "Content-Type": "application/json", + "Authorization": "Basic {0}".format(basic_str.decode("ascii")), + }, + ) self.authenticated = True def existing_item_add_url(self, existing_item, endpoint, key="url"): @@ -730,7 +696,7 @@ def wait_for_complete(self, task_url): time.sleep(1) return - def upload(self, path, endpoint, wait=True, item_type="unknown"): + def upload(self, path, endpoint, wait=True, repository="staging", item_type="unknown"): if "://" in path: tmppath = fetch_file(self, path) path = path.split("/")[-1] @@ -739,7 +705,7 @@ def upload(self, path, endpoint, wait=True, item_type="unknown"): ct, body = self.prepare_multipart(path) response = self.make_request( "POST", - endpoint, + "{0}/content/{1}/v3/{2}/".format(self.path_prefix, repository, endpoint), **{ "data": body, "headers": {"Content-Type": str(ct)}, diff --git a/plugins/modules/ah_collection.py b/plugins/modules/ah_collection.py index fa184347..ba2fc703 100644 --- a/plugins/modules/ah_collection.py +++ b/plugins/modules/ah_collection.py @@ -44,6 +44,12 @@ - Collection artifact file path. - If version is not specified, version will be derived from file name. type: str + repository: + description: + - Name of the destination collection for the repository (staging for uploads). + required: False + type: str + default: 'staging' wait: description: - Waits for the collection to be uploaded @@ -114,6 +120,7 @@ def main(): namespace=dict(required=True), name=dict(required=True), path=dict(), + repository=dict(default='staging'), wait=dict(type="bool", default=True), interval=dict(default=10.0, type="float"), timeout=dict(default=None, type="int"), @@ -130,6 +137,7 @@ def main(): namespace = module.params.get("namespace") name = module.params.get("name") path = module.params.get("path") + repository = module.params.get("repository") wait = module.params.get("wait") interval = module.params.get("interval") timeout = module.params.get("timeout") @@ -176,7 +184,7 @@ def main(): module.json_output["deleted"] = True module.wait_for_complete(module.json_output["task"]) # Upload new collection - module.upload(path, "artifacts/collections", wait, item_type="collections") + module.upload(path, "artifacts/collections", wait, repository, item_type="collections") module.json_output["changed"] = True # Get new collection version existing_item = module.get_endpoint(collection_endpoint, **{"return_none_on_404": True}) @@ -187,7 +195,7 @@ def main(): interval=interval ) elif existing_item is None: - module.upload(path, "artifacts/collections", wait, item_type="collections") + module.upload(path, "artifacts/collections", wait, repository, item_type="collections") module.json_output["changed"] = True if auto_approve: module.approve( diff --git a/plugins/modules/ah_collection_upload.py b/plugins/modules/ah_collection_upload.py index 3151a01a..3008d3c1 100644 --- a/plugins/modules/ah_collection_upload.py +++ b/plugins/modules/ah_collection_upload.py @@ -31,6 +31,12 @@ - Can be a URL required: True type: str + repository: + description: + - Name of the collection's repository + - Defaults to 'staging' + required: False + type: str wait: description: - Wait for the collection to be uploaded. @@ -59,6 +65,7 @@ def main(): # Any additional arguments that are not fields of the item can be added here argument_spec = dict( path=dict(required=True), + repository=dict(required=False), wait=dict(type="bool", default=True), ) @@ -67,9 +74,10 @@ def main(): # Extract our parameters path = module.params.get("path") + repository = module.params.get("repository") or "staging" wait = module.params.get("wait") - module.upload(path, "artifacts/collections", wait, item_type="collections") + module.upload(path, "artifacts/collections", wait, repository, item_type="collections") module.exit_json(**module.json_output) diff --git a/roles/collection/tasks/main.yml b/roles/collection/tasks/main.yml index 491b4dbe..670900df 100644 --- a/roles/collection/tasks/main.yml +++ b/roles/collection/tasks/main.yml @@ -19,6 +19,7 @@ name: "{{ __collection.name }}" version: "{{ __collection.version | default(omit) }}" path: "{{ __collection.path | default(omit) }}" + repository: "{{ __collection.repository | default(omit) }}" wait: "{{ __collection.wait | default(omit) }}" auto_approve: "{{ __collection.auto_approve | default(omit) }}" timeout: "{{ __collection.timeout | default(omit) }}" diff --git a/tests/playbooks/testing_collections_playbook.yml b/tests/playbooks/testing_collections_playbook.yml index c6ab6700..8b6cdf83 100644 --- a/tests/playbooks/testing_collections_playbook.yml +++ b/tests/playbooks/testing_collections_playbook.yml @@ -6,7 +6,7 @@ collections: - galaxy.galaxy vars: - ah_path_prefix: 'galaxy' + ah_path_prefix: '/api/galaxy' ah_hostname: "{{ lookup('ansible.builtin.env', 'AH_HOST') }}" ah_username: "{{ lookup('ansible.builtin.env', 'AH_USERNAME') }}" ah_password: "{{ lookup('ansible.builtin.env', 'AH_PASSWORD') }}" @@ -47,7 +47,7 @@ - name: community_test company: Community Test email: user@example.com - avatar_url: https://github.com/ansible/awx-logos/blob/master/awx/ui/client/assets/logo-header.svg + avatar_url: https://github.com/ansible/awx-logos/blob/master/awx/ui/client/assets/192.png description: string resources: "# Community\nA Namespace test with changes" links: @@ -57,7 +57,6 @@ - name: test_namespace - name: galaxy - - name: Rename namespace ansible.builtin.include_role: name: namespace @@ -120,16 +119,6 @@ ah_path_prefix: "{{ ah_path_prefix }}" validate_certs: "{{ ah_validate_certs }}" - - name: Add EE Registry - ah_ee_registry: - name: myreg - url: https://registry.redhat.io - ah_host: "{{ ah_hostname }}" - ah_username: "{{ ah_username }}" - ah_password: "{{ ah_password }}" - ah_path_prefix: "{{ ah_path_prefix }}" - validate_certs: "{{ ah_validate_certs }}" - - name: Download Tower tarball ansible.builtin.get_url: url: https://galaxy.ansible.com/download/infra-controller_configuration-2.4.0.tar.gz