From 5a63533967a052a5727872cba878f8232d8e973a Mon Sep 17 00:00:00 2001 From: John Westcott IV <32551173+john-westcott-iv@users.noreply.github.com> Date: Thu, 27 Jul 2023 09:22:41 -0400 Subject: [PATCH] Added support to collection for named urls (#14205) --- awx_collection/README.md | 1 + .../plugins/module_utils/controller_api.py | 65 +++++++++++------ .../targets/module_utils/tasks/main.yml | 8 ++ .../tasks/test_named_reference.yml | 73 +++++++++++++++++++ .../template_galaxy/templates/README.md.j2 | 1 + 5 files changed, 126 insertions(+), 22 deletions(-) create mode 100644 awx_collection/tests/integration/targets/module_utils/tasks/main.yml create mode 100644 awx_collection/tests/integration/targets/module_utils/tasks/test_named_reference.yml diff --git a/awx_collection/README.md b/awx_collection/README.md index 2be1ad624a..179ee2b9ea 100644 --- a/awx_collection/README.md +++ b/awx_collection/README.md @@ -68,6 +68,7 @@ Notable releases of the `awx.awx` collection: - 7.0.0 is intended to be identical to the content prior to the migration, aside from changes necessary to function as a collection. - 11.0.0 has no non-deprecated modules that depend on the deprecated `tower-cli` [PyPI](https://pypi.org/project/ansible-tower-cli/). - 19.2.1 large renaming purged "tower" names (like options and module names), adding redirects for old names + - X.X.X added support of named URLs to all modules. Anywhere that previously accepted name or id can also support named URLs - 0.0.1-devel is the version you should see if installing from source, which is intended for development and expected to be unstable. The following notes are changes that may require changes to playbooks: diff --git a/awx_collection/plugins/module_utils/controller_api.py b/awx_collection/plugins/module_utils/controller_api.py index 5f59548ba9..19ce497852 100644 --- a/awx_collection/plugins/module_utils/controller_api.py +++ b/awx_collection/plugins/module_utils/controller_api.py @@ -10,7 +10,7 @@ from ansible.module_utils.six import raise_from, string_types from ansible.module_utils.six.moves import StringIO from ansible.module_utils.six.moves.urllib.error import HTTPError from ansible.module_utils.six.moves.http_cookiejar import CookieJar -from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode +from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode, quote from ansible.module_utils.six.moves.configparser import ConfigParser, NoOptionError from socket import getaddrinfo, IPPROTO_TCP import time @@ -383,29 +383,51 @@ class ControllerAPIModule(ControllerModule): def get_one(self, endpoint, name_or_id=None, allow_none=True, check_exists=False, **kwargs): new_kwargs = kwargs.copy() - if name_or_id: - name_field = self.get_name_field_from_endpoint(endpoint) - new_data = kwargs.get('data', {}).copy() - if name_field in new_data: - self.fail_json(msg="You can't specify the field {0} in your search data if using the name_or_id field".format(name_field)) + response = None - try: - new_data['or__id'] = int(name_or_id) - new_data['or__{0}'.format(name_field)] = name_or_id - except ValueError: - # If we get a value error, then we didn't have an integer so we can just pass and fall down to the fail - new_data[name_field] = name_or_id - new_kwargs['data'] = new_data + # A named URL is pretty unique so if we have a ++ in the name then lets start by looking for that + # This also needs to go first because if there was data passed in kwargs and we do the next lookup first there may be results + if name_or_id is not None and '++' in name_or_id: + # Maybe someone gave us a named URL so lets see if we get anything from that. + url_quoted_name = quote(name_or_id, safe="+") + named_endpoint = '{0}/{1}/'.format(endpoint, url_quoted_name) + named_response = self.get_endpoint(named_endpoint) - response = self.get_endpoint(endpoint, **new_kwargs) - if response['status_code'] != 200: - fail_msg = "Got a {0} response when trying to get one from {1}".format(response['status_code'], endpoint) - if 'detail' in response.get('json', {}): - fail_msg += ', detail: {0}'.format(response['json']['detail']) - self.fail_json(msg=fail_msg) + if named_response['status_code'] == 200 and 'json' in named_response: + # We found a named item but we expect to deal with a list view so mock that up + response = { + 'json': { + 'count': 1, + 'results': [named_response['json']], + } + } - if 'count' not in response['json'] or 'results' not in response['json']: - self.fail_json(msg="The endpoint did not provide count and results") + # Since we didn't have a named URL, lets try and find it with a general search + if response is None: + if name_or_id: + name_field = self.get_name_field_from_endpoint(endpoint) + new_data = kwargs.get('data', {}).copy() + if name_field in new_data: + self.fail_json(msg="You can't specify the field {0} in your search data if using the name_or_id field".format(name_field)) + + try: + new_data['or__id'] = int(name_or_id) + new_data['or__{0}'.format(name_field)] = name_or_id + except ValueError: + # If we get a value error, then we didn't have an integer so we can just pass and fall down to the fail + new_data[name_field] = name_or_id + new_kwargs['data'] = new_data + + response = self.get_endpoint(endpoint, **new_kwargs) + + if response['status_code'] != 200: + fail_msg = "Got a {0} response when trying to get one from {1}".format(response['status_code'], endpoint) + if 'detail' in response.get('json', {}): + fail_msg += ', detail: {0}'.format(response['json']['detail']) + self.fail_json(msg=fail_msg) + + if 'count' not in response['json'] or 'results' not in response['json']: + self.fail_json(msg="The endpoint did not provide count and results") if response['json']['count'] == 0: if allow_none: @@ -423,7 +445,6 @@ class ControllerAPIModule(ControllerModule): self.fail_wanted_one(response, endpoint, new_kwargs.get('data')) if check_exists: - name_field = self.get_name_field_from_endpoint(endpoint) self.json_output['id'] = response['json']['results'][0]['id'] self.exit_json(**self.json_output) diff --git a/awx_collection/tests/integration/targets/module_utils/tasks/main.yml b/awx_collection/tests/integration/targets/module_utils/tasks/main.yml new file mode 100644 index 0000000000..2c0908b481 --- /dev/null +++ b/awx_collection/tests/integration/targets/module_utils/tasks/main.yml @@ -0,0 +1,8 @@ +--- +- name: Generate a random string for test + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + +- include_tasks: + file: test_named_reference.yml diff --git a/awx_collection/tests/integration/targets/module_utils/tasks/test_named_reference.yml b/awx_collection/tests/integration/targets/module_utils/tasks/test_named_reference.yml new file mode 100644 index 0000000000..3ae913a571 --- /dev/null +++ b/awx_collection/tests/integration/targets/module_utils/tasks/test_named_reference.yml @@ -0,0 +1,73 @@ +--- +- block: + - name: generate random string for project + set_fact: + org_name: "AWX-Collection-tests-organization-org-{{ test_id }}" + cred: "AWX-Collection-tests-job_template-cred-{{ test_id }}" + inv: "AWX-Collection-tests-job_template-inv-{{ test_id }}" + proj: "AWX-Collection-tests-job_template-proj-{{ test_id }}" + jt: "AWX-Collection-tests-job_template-jt-{{ test_id }}" + + - name: "Create a new organization" + organization: + name: "{{ org_name }}" + galaxy_credentials: + - Ansible Galaxy + + - name: Create an inventory + inventory: + name: "{{ inv }}" + organization: "{{ org_name }}" + + - name: Create a Demo Project + project: + name: "{{ proj }}" + organization: "{{ org_name }}" + state: present + scm_type: git + scm_url: https://github.com/ansible/ansible-tower-samples.git + + - name: Create Credential + credential: + name: "{{ cred }}" + organization: "{{ org_name }}" + credential_type: Machine + + - name: Create Job Template + job_template: + name: "{{ jt }}" + project: "{{ proj }}++{{ org_name }}" + inventory: "{{ inv }}++{{ org_name }}" + playbook: hello_world.yml + credentials: + - "{{ cred }}++Machine+ssh++" + job_type: run + state: present + + always: + - name: Delete the Job Template + job_template: + name: "{{ jt }}" + state: absent + + - name: Delete the Demo Project + project: + name: "{{ proj }}++{{ org_name }}" + state: absent + + - name: Delete Credential + credential: + name: "{{ cred }}++Machine+ssh++{{ org_name }}" + credential_type: Machine + state: absent + + - name: Delete the inventory + inventory: + name: "{{ inv }}++{{ org_name }}" + organization: "{{ org_name }}" + state: absent + + - name: Remove the organization + organization: + name: "{{ org_name }}" + state: absent diff --git a/awx_collection/tools/roles/template_galaxy/templates/README.md.j2 b/awx_collection/tools/roles/template_galaxy/templates/README.md.j2 index c1e8f8d567..94c42b701a 100644 --- a/awx_collection/tools/roles/template_galaxy/templates/README.md.j2 +++ b/awx_collection/tools/roles/template_galaxy/templates/README.md.j2 @@ -75,6 +75,7 @@ Notable releases of the `{{ collection_namespace }}.{{ collection_package }}` co - 11.0.0 has no non-deprecated modules that depend on the deprecated `tower-cli` [PyPI](https://pypi.org/project/ansible-tower-cli/). - 19.2.1 large renaming purged "tower" names (like options and module names), adding redirects for old names - 21.11.0 "tower" modules deprecated and symlinks removed. + - X.X.X added support of named URLs to all modules. Anywhere that previously accepted name or id can also support named URLs - 0.0.1-devel is the version you should see if installing from source, which is intended for development and expected to be unstable. {% else %} - 3.7.0 initial release