diff --git a/awx/api/generics.py b/awx/api/generics.py index 7d21d74ee6..361506c605 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -28,7 +28,7 @@ from rest_framework import generics from rest_framework.response import Response from rest_framework import status from rest_framework import views -from rest_framework.permissions import AllowAny +from rest_framework.permissions import IsAuthenticated from rest_framework.renderers import StaticHTMLRenderer from rest_framework.negotiation import DefaultContentNegotiation @@ -822,7 +822,7 @@ def trigger_delayed_deep_copy(*args, **kwargs): class CopyAPIView(GenericAPIView): serializer_class = CopySerializer - permission_classes = (AllowAny,) + permission_classes = (IsAuthenticated,) copy_return_serializer_class = None new_in_330 = True new_in_api_v2 = True diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index e81a6ebbde..609e88e155 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -4288,7 +4288,7 @@ class WorkflowApprovalTemplateJobsList(SubListAPIView): parent_key = 'workflow_approval_template' -class WorkflowApprovalList(ListCreateAPIView): +class WorkflowApprovalList(ListAPIView): model = models.WorkflowApproval serializer_class = serializers.WorkflowApprovalListSerializer diff --git a/awx/playbooks/project_update.yml b/awx/playbooks/project_update.yml index 2067e76043..7334547f97 100644 --- a/awx/playbooks/project_update.yml +++ b/awx/playbooks/project_update.yml @@ -25,42 +25,47 @@ connection: local name: Update source tree if necessary tasks: - - - name: delete project directory before update - command: "find -delete" # volume mounted, cannot delete folder itself + - name: Delete project directory before update + ansible.builtin.shell: set -o pipefail && find . -delete -print | head -2 # volume mounted, cannot delete folder itself + register: reg + changed_when: reg.stdout_lines | length > 1 args: chdir: "{{ project_path }}" tags: - delete - - block: - - name: update project using git - git: - dest: "{{project_path|quote}}" - repo: "{{scm_url}}" - version: "{{scm_branch|quote}}" - refspec: "{{scm_refspec|default(omit)}}" - force: "{{scm_clean}}" - track_submodules: "{{scm_track_submodules|default(omit)}}" - accept_hostkey: "{{scm_accept_hostkey|default(omit)}}" + - name: Update project using git + tags: + - update_git + block: + - name: Update project using git + ansible.builtin.git: + dest: "{{ project_path | quote }}" + repo: "{{ scm_url }}" + version: "{{ scm_branch | quote }}" + refspec: "{{ scm_refspec | default(omit) }}" + force: "{{ scm_clean }}" + track_submodules: "{{ scm_track_submodules | default(omit) }}" + accept_hostkey: "{{ scm_accept_hostkey | default(omit) }}" register: git_result - name: Set the git repository version - set_fact: + ansible.builtin.set_fact: scm_version: "{{ git_result['after'] }}" when: "'after' in git_result" - tags: - - update_git - - block: - - name: update project using svn - subversion: - dest: "{{project_path|quote}}" - repo: "{{scm_url|quote}}" - revision: "{{scm_branch|quote}}" - force: "{{scm_clean}}" - username: "{{scm_username|default(omit)}}" - password: "{{scm_password|default(omit)}}" + - name: Update project using svn + tags: + - update_svn + block: + - name: Update project using svn + ansible.builtin.subversion: + dest: "{{ project_path | quote }}" + repo: "{{ scm_url | quote }}" + revision: "{{ scm_branch | quote }}" + force: "{{ scm_clean }}" + username: "{{ scm_username | default(omit) }}" + password: "{{ scm_password | default(omit) }}" # must be in_place because folder pre-existing, because it is mounted in_place: true environment: @@ -68,85 +73,90 @@ register: svn_result - name: Set the svn repository version - set_fact: + ansible.builtin.set_fact: scm_version: "{{ svn_result['after'] }}" when: "'after' in svn_result" - - name: parse subversion version string properly - set_fact: - scm_version: "{{scm_version|regex_replace('^.*Revision: ([0-9]+).*$', '\\1')}}" - tags: - - update_svn + - name: Parse subversion version string properly + ansible.builtin.set_fact: + scm_version: "{{ scm_version | regex_replace('^.*Revision: ([0-9]+).*$', '\\1') }}" - - block: + + - name: Project update for Insights + tags: + - update_insights + block: - name: Ensure the project directory is present - file: - dest: "{{project_path|quote}}" + ansible.builtin.file: + dest: "{{ project_path | quote }}" state: directory + mode: '0755' - name: Fetch Insights Playbook(s) insights: - insights_url: "{{insights_url}}" - username: "{{scm_username}}" - password: "{{scm_password}}" - project_path: "{{project_path}}" - awx_license_type: "{{awx_license_type}}" - awx_version: "{{awx_version}}" + insights_url: "{{ insights_url }}" + username: "{{ scm_username }}" + password: "{{ scm_password }}" + project_path: "{{ project_path }}" + awx_license_type: "{{ awx_license_type }}" + awx_version: "{{ awx_version }}" register: results - name: Save Insights Version - set_fact: - scm_version: "{{results.version}}" + ansible.builtin.set_fact: + scm_version: "{{ results.version }}" when: results is defined - tags: - - update_insights - - block: + + - name: Update project using archive + tags: + - update_archive + block: - name: Ensure the project archive directory is present - file: - dest: "{{ project_path|quote }}/.archive" + ansible.builtin.file: + dest: "{{ project_path | quote }}/.archive" state: directory + mode: '0755' - name: Get archive from url - get_url: - url: "{{ scm_url|quote }}" - dest: "{{ project_path|quote }}/.archive/" - url_username: "{{ scm_username|default(omit) }}" - url_password: "{{ scm_password|default(omit) }}" + ansible.builtin.get_url: + url: "{{ scm_url | quote }}" + dest: "{{ project_path | quote }}/.archive/" + url_username: "{{ scm_username | default(omit) }}" + url_password: "{{ scm_password | default(omit) }}" force_basic_auth: true + mode: '0755' register: get_archive - name: Unpack archive project_archive: src: "{{ get_archive.dest }}" - project_path: "{{ project_path|quote }}" + project_path: "{{ project_path | quote }}" force: "{{ scm_clean }}" when: get_archive.changed or scm_clean register: unarchived - name: Find previous archives - find: - paths: "{{ project_path|quote }}/.archive/" + ansible.builtin.find: + paths: "{{ project_path | quote }}/.archive/" excludes: - - "{{ get_archive.dest|basename }}" + - "{{ get_archive.dest | basename }}" when: unarchived.changed register: previous_archive - name: Remove previous archives - file: + ansible.builtin.file: path: "{{ item.path }}" state: absent loop: "{{ previous_archive.files }}" - when: previous_archive.files|default([]) + when: previous_archive.files | default([]) - name: Set scm_version to archive sha1 checksum - set_fact: + ansible.builtin.set_fact: scm_version: "{{ get_archive.checksum_src }}" - tags: - - update_archive - name: Repository Version - debug: + ansible.builtin.debug: msg: "Repository Version {{ scm_version }}" tags: - update_git @@ -183,60 +193,59 @@ additional_collections_env: # These environment variables are used for installing collections, in addition to galaxy_task_env # setting the collections paths silences warnings - ANSIBLE_COLLECTIONS_PATHS: "{{projects_root}}/.__awx_cache/{{local_path}}/stage/requirements_collections" + ANSIBLE_COLLECTIONS_PATHS: "{{ projects_root }}/.__awx_cache/{{ local_path }}/stage/requirements_collections" # Put the local tmp directory in same volume as collection destination # otherwise, files cannot be moved accross volumes and will cause error - ANSIBLE_LOCAL_TEMP: "{{projects_root}}/.__awx_cache/{{local_path}}/stage/tmp" + ANSIBLE_LOCAL_TEMP: "{{ projects_root }}/.__awx_cache/{{ local_path }}/stage/tmp" tasks: - - name: Check content sync settings - block: - - debug: - msg: > - Collection and role syncing disabled. Check the AWX_ROLES_ENABLED and - AWX_COLLECTIONS_ENABLED settings and Galaxy credentials on the project's organization. - - - meta: end_play - - when: not roles_enabled|bool and not collections_enabled|bool + when: not roles_enabled | bool and not collections_enabled | bool tags: - install_roles - install_collections + block: + - name: Warn about disabled content sync + ansible.builtin.debug: + msg: > + Collection and role syncing disabled. Check the AWX_ROLES_ENABLED and + AWX_COLLECTIONS_ENABLED settings and Galaxy credentials on the project's organization. + - name: End play due to disabled content sync + ansible.builtin.meta: end_play - - name: fetch galaxy roles from requirements.(yml/yaml) - command: > + - name: Fetch galaxy roles from requirements.(yml/yaml) + ansible.builtin.command: > ansible-galaxy role install -r {{ item }} - --roles-path {{projects_root}}/.__awx_cache/{{local_path}}/stage/requirements_roles + --roles-path {{ projects_root }}/.__awx_cache/{{ local_path }}/stage/requirements_roles {{ ' -' + 'v' * ansible_verbosity if ansible_verbosity else '' }} args: - chdir: "{{project_path|quote}}" + chdir: "{{ project_path | quote }}" register: galaxy_result with_fileglob: - - "{{project_path|quote}}/roles/requirements.yaml" - - "{{project_path|quote}}/roles/requirements.yml" + - "{{ project_path | quote }}/roles/requirements.yaml" + - "{{ project_path | quote }}/roles/requirements.yml" changed_when: "'was installed successfully' in galaxy_result.stdout" environment: "{{ galaxy_task_env }}" - when: roles_enabled|bool + when: roles_enabled | bool tags: - install_roles - - name: fetch galaxy collections from collections/requirements.(yml/yaml) - command: > + - name: Fetch galaxy collections from collections/requirements.(yml/yaml) + ansible.builtin.command: > ansible-galaxy collection install -r {{ item }} - --collections-path {{projects_root}}/.__awx_cache/{{local_path}}/stage/requirements_collections + --collections-path {{ projects_root }}/.__awx_cache/{{ local_path }}/stage/requirements_collections {{ ' -' + 'v' * ansible_verbosity if ansible_verbosity else '' }} args: - chdir: "{{project_path|quote}}" + chdir: "{{ project_path | quote }}" register: galaxy_collection_result with_fileglob: - - "{{project_path|quote}}/collections/requirements.yaml" - - "{{project_path|quote}}/collections/requirements.yml" - - "{{project_path|quote}}/requirements.yaml" - - "{{project_path|quote}}/requirements.yml" + - "{{ project_path | quote }}/collections/requirements.yaml" + - "{{ project_path | quote }}/collections/requirements.yml" + - "{{ project_path | quote }}/requirements.yaml" + - "{{ project_path | quote }}/requirements.yml" changed_when: "'Installing ' in galaxy_collection_result.stdout" environment: "{{ additional_collections_env | combine(galaxy_task_env) }}" when: - "ansible_version.full is version_compare('2.9', '>=')" - - collections_enabled|bool + - collections_enabled | bool tags: - install_collections diff --git a/awx_collection/plugins/modules/workflow_approval.py b/awx_collection/plugins/modules/workflow_approval.py index bb8c24dfb8..dd81eeb93c 100644 --- a/awx_collection/plugins/modules/workflow_approval.py +++ b/awx_collection/plugins/modules/workflow_approval.py @@ -57,7 +57,15 @@ extends_documentation_fragment: awx.awx.auth EXAMPLES = """ -- name: Launch a workflow with a timeout of 10 seconds +- name: Create a workflow approval node + workflow_job_template_node: + identifier: approval_test + approval_node: + name: approval_jt_name + timeout: 900 + workflow: "Test Workflow" + +- name: Launch the workflow with a timeout of 10 seconds workflow_launch: workflow_template: "Test Workflow" wait: False @@ -66,7 +74,7 @@ EXAMPLES = """ - name: Wait for approval node to activate and approve workflow_approval: workflow_job_id: "{{ workflow.id }}" - name: Approve Me + name: approval_jt_name interval: 10 timeout: 20 action: deny diff --git a/awx_collection/plugins/modules/workflow_job_template.py b/awx_collection/plugins/modules/workflow_job_template.py index 41c5b66573..19954877b7 100644 --- a/awx_collection/plugins/modules/workflow_job_template.py +++ b/awx_collection/plugins/modules/workflow_job_template.py @@ -183,7 +183,21 @@ options: inventory: description: - Inventory applied as a prompt, if job template prompts for inventory - type: str + type: dict + suboptions: + name: + description: + - Name Inventory to be applied to job as launch-time prompts. + type: str + organization: + description: + - Name of key for use in model for organizational reference + type: dict + suboptions: + name: + description: + - The organization of the credentials exists in. + type: str scm_branch: description: - SCM branch applied as a prompt, if job template prompts for SCM branch @@ -544,6 +558,10 @@ EXAMPLES = ''' type: job_template execution_environment: name: My EE + inventory: + name: Test inventory + organization: + name: Default related: credentials: - name: cyberark @@ -613,10 +631,6 @@ def create_workflow_nodes(module, response, workflow_nodes, workflow_id): if workflow_node['unified_job_template']['type'] != 'workflow_approval': module.fail_json(msg="Unable to Find unified_job_template: {0}".format(search_fields)) - inventory = workflow_node.get('inventory') - if inventory: - workflow_node_fields['inventory'] = module.resolve_name_to_id('inventories', inventory) - # Lookup Values for other fields for field_name in ( @@ -645,6 +659,17 @@ def create_workflow_nodes(module, response, workflow_nodes, workflow_id): 'execution_environments', name_or_id=workflow_node['execution_environment']['name'] )['id'] + # Two lookup methods are used based on a fix added in 21.11.0, and the awx export model + if 'inventory' in workflow_node: + if 'name' in workflow_node['inventory']: + inv_lookup_data = {} + if 'organization' in workflow_node['inventory']: + inv_lookup_data['organization'] = module.resolve_name_to_id('organizations', workflow_node['inventory']['organization']['name']) + workflow_node_fields['inventory'] = module.get_one( + 'inventories', name_or_id=workflow_node['inventory']['name'], data=inv_lookup_data)['id'] + else: + workflow_node_fields['inventory'] = module.get_one('inventories', name_or_id=workflow_node['inventory'])['id'] + # Set Search fields search_fields['workflow_job_template'] = workflow_node_fields['workflow_job_template'] = workflow_id diff --git a/awx_collection/test/awx/test_completeness.py b/awx_collection/test/awx/test_completeness.py index 43e225e4b8..451c1a61d3 100644 --- a/awx_collection/test/awx/test_completeness.py +++ b/awx_collection/test/awx/test_completeness.py @@ -16,7 +16,7 @@ import glob # Normally a read-only endpoint should not have a module (i.e. /api/v2/me) but sometimes we reuse a name # For example, we have a role module but /api/v2/roles is a read only endpoint. # This list indicates which read-only endpoints have associated modules with them. -read_only_endpoints_with_modules = ['settings', 'role', 'project_update'] +read_only_endpoints_with_modules = ['settings', 'role', 'project_update', 'workflow_approval'] # If a module should not be created for an endpoint and the endpoint is not read-only add it here # THINK HARD ABOUT DOING THIS diff --git a/awx_collection/tests/integration/targets/workflow_approval/tasks/main.yml b/awx_collection/tests/integration/targets/workflow_approval/tasks/main.yml new file mode 100644 index 0000000000..eaf1b3bf8d --- /dev/null +++ b/awx_collection/tests/integration/targets/workflow_approval/tasks/main.yml @@ -0,0 +1,57 @@ +--- +- name: Generate a random string for names + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + test_prefix: AWX-Collection-tests-workflow_approval + +- name: Generate random names for test objects + set_fact: + org_name: "{{ test_prefix }}-org-{{ test_id }}" + approval_node_name: "{{ test_prefix }}-node-{{ test_id }}" + wfjt_name: "{{ test_prefix }}-wfjt-{{ test_id }}" + +- block: + - name: Create a new organization for test isolation + organization: + name: "{{ org_name }}" + + - name: Create a workflow job template + workflow_job_template: + name: "{{ wfjt_name }}" + organization: "{{ org_name }}" + + - name: Create approval node + workflow_job_template_node: + identifier: approval_test + approval_node: + name: "{{ approval_node_name }}" # Referenced later on + timeout: 900 + workflow: "{{ wfjt_name }}" + + # Launch and approve the workflow + - name: Launch the workflow + workflow_launch: + workflow_template: "{{ wfjt_name }}" + wait: False + register: workflow_job + + - name: Wait for approval node to activate and approve + workflow_approval: + workflow_job_id: "{{ workflow_job.id }}" + name: "{{ approval_node_name }}" + interval: 10 + timeout: 20 + action: approve + register: result + + - assert: + that: + - "result is changed" + - "result is not failed" + + always: + - name: Delete the workflow job template + workflow_job_template: + name: "{{ wfjt_name }}" + state: absent + ignore_errors: True diff --git a/awx_collection/tests/integration/targets/workflow_job_template/tasks/main.yml b/awx_collection/tests/integration/targets/workflow_job_template/tasks/main.yml index 1477193e6c..e5f3366cd3 100644 --- a/awx_collection/tests/integration/targets/workflow_job_template/tasks/main.yml +++ b/awx_collection/tests/integration/targets/workflow_job_template/tasks/main.yml @@ -493,6 +493,7 @@ workflow_job_template: name: "copy_{{ wfjt_name }}" organization: Default + ask_inventory_on_launch: true survey_spec: name: Basic Survey description: Basic Survey @@ -737,6 +738,10 @@ timeout: 23 execution_environment: name: "{{ ee1 }}" + inventory: + name: Test inventory + organization: + name: Default related: credentials: - name: "{{ scm_cred_name }}" diff --git a/docs/development/kind.md b/docs/development/kind.md index 820abae182..d3a11cc4cd 100644 --- a/docs/development/kind.md +++ b/docs/development/kind.md @@ -75,7 +75,8 @@ In the root of awx-operator: -e image_version=devel \ -e image_pull_policy=Always \ -e service_type=nodeport \ - -e namespace=awx + -e namespace=awx \ + -e nodeport_port=30080 ``` Check the operator with the following commands: