From 78145922854d00e24733b05a42726e6ea41057ae Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Fri, 9 Aug 2019 17:55:42 -0400 Subject: [PATCH] when copying workflows w/ pause nodes, copy the WorkflowApprovalTemplate --- awx/api/generics.py | 18 ++++++- awx/main/access.py | 9 +++- awx/main/models/workflow.py | 3 ++ awx/main/tests/functional/test_copy.py | 74 +++++++++++++++++++++++++- 4 files changed, 101 insertions(+), 3 deletions(-) diff --git a/awx/api/generics.py b/awx/api/generics.py index c66c9b7348..06f80887ae 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -34,7 +34,8 @@ from rest_framework.negotiation import DefaultContentNegotiation # AWX from awx.api.filters import FieldLookupBackend from awx.main.models import ( - UnifiedJob, UnifiedJobTemplate, User, Role, Credential + UnifiedJob, UnifiedJobTemplate, User, Role, Credential, + WorkflowJobTemplateNode, WorkflowApprovalTemplate ) from awx.main.access import access_registry from awx.main.utils import ( @@ -882,6 +883,21 @@ class CopyAPIView(GenericAPIView): create_kwargs[field.name] = CopyAPIView._decrypt_model_field_if_needed( obj, field.name, field_val ) + + # WorkflowJobTemplateNodes that represent an approval are *special*; + # when we copy them, we actually want to *copy* the UJT they point at + # rather than share the template reference between nodes in disparate + # workflows + if ( + isinstance(obj, WorkflowJobTemplateNode) and + isinstance(getattr(obj, 'unified_job_template'), WorkflowApprovalTemplate) + ): + new_approval_template, sub_objs = CopyAPIView.copy_model_obj( + None, None, WorkflowApprovalTemplate, + obj.unified_job_template, creater + ) + create_kwargs['unified_job_template'] = new_approval_template + new_obj = model.objects.create(**create_kwargs) logger.debug('Deep copy: Created new object {}({})'.format( new_obj, model diff --git a/awx/main/access.py b/awx/main/access.py index 270cb9b420..936d80efab 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -2016,7 +2016,7 @@ class WorkflowJobTemplateAccess(NotificationAttachMixin, BaseAccess): if self.user not in cred.use_role: missing_credentials.append(cred.name) ujt = node.unified_job_template - if ujt and not isinstance(ujt, WorkflowApprovalTemplate) and not self.user.can_access(UnifiedJobTemplate, 'start', ujt, validate_license=False): + if ujt and not self.user.can_access(UnifiedJobTemplate, 'start', ujt, validate_license=False): missing_ujt.append(ujt.name) if missing_ujt: self.messages['templates_unable_to_copy'] = missing_ujt @@ -2829,6 +2829,13 @@ class WorkflowApprovalTemplateAccess(BaseAccess): else: return (self.check_related('workflow_approval_template', UnifiedJobTemplate, role_field='admin_role')) + def can_start(self, obj, validate_license=False): + # Super users can start any job + if self.user.is_superuser: + return True + + return self.user in obj.workflow_job_template.execute_role + def filtered_queryset(self): return self.model.objects.filter( workflowjobtemplatenodes__workflow_job_template__in=WorkflowJobTemplate.accessible_pk_qs( diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index 65f03abe95..5c0f277cfb 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -619,6 +619,9 @@ class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificatio class WorkflowApprovalTemplate(UnifiedJobTemplate): + + FIELDS_TO_PRESERVE_AT_COPY = ['description', 'timeout',] + class Meta: app_label = 'main' diff --git a/awx/main/tests/functional/test_copy.py b/awx/main/tests/functional/test_copy.py index a4d2859110..7be582d6c8 100644 --- a/awx/main/tests/functional/test_copy.py +++ b/awx/main/tests/functional/test_copy.py @@ -3,7 +3,9 @@ from unittest import mock from awx.api.versioning import reverse from awx.main.utils import decrypt_field -from awx.main.models.workflow import WorkflowJobTemplateNode +from awx.main.models.workflow import ( + WorkflowJobTemplate, WorkflowJobTemplateNode, WorkflowApprovalTemplate +) from awx.main.models.jobs import JobTemplate from awx.main.tasks import deep_copy_model_obj @@ -175,6 +177,76 @@ def test_workflow_job_template_copy(workflow_job_template, post, get, admin, org assert copied_node_list[4] in copied_node_list[3].failure_nodes.all() +@pytest.mark.django_db +def test_workflow_approval_node_copy(workflow_job_template, post, get, admin, organization): + workflow_job_template.organization = organization + workflow_job_template.save() + ajts = [ + WorkflowApprovalTemplate.objects.create( + name='test-approval-{}'.format(i), + description='description-{}'.format(i), + timeout=30 + ) + for i in range(0, 5) + ] + nodes = [ + WorkflowJobTemplateNode.objects.create( + workflow_job_template=workflow_job_template, unified_job_template=ajts[i] + ) for i in range(0, 5) + ] + nodes[0].success_nodes.add(nodes[1]) + nodes[1].success_nodes.add(nodes[2]) + nodes[0].failure_nodes.add(nodes[3]) + nodes[3].failure_nodes.add(nodes[4]) + assert WorkflowJobTemplate.objects.count() == 1 + assert WorkflowJobTemplateNode.objects.count() == 5 + assert WorkflowApprovalTemplate.objects.count() == 5 + + with mock.patch('awx.api.generics.trigger_delayed_deep_copy') as deep_copy_mock: + wfjt_copy_id = post( + reverse('api:workflow_job_template_copy', kwargs={'pk': workflow_job_template.pk}), + {'name': 'new wfjt name'}, admin, expect=201 + ).data['id'] + wfjt_copy = type(workflow_job_template).objects.get(pk=wfjt_copy_id) + args, kwargs = deep_copy_mock.call_args + deep_copy_model_obj(*args, **kwargs) + assert wfjt_copy.organization == organization + assert wfjt_copy.created_by == admin + assert wfjt_copy.name == 'new wfjt name' + + assert WorkflowJobTemplate.objects.count() == 2 + assert WorkflowJobTemplateNode.objects.count() == 10 + assert WorkflowApprovalTemplate.objects.count() == 10 + original_templates = [ + x.unified_job_template for x in workflow_job_template.workflow_job_template_nodes.all() + ] + copied_templates = [ + x.unified_job_template for x in wfjt_copy.workflow_job_template_nodes.all() + ] + + # make sure shallow fields like `timeout` are copied properly + for i, t in enumerate(original_templates): + assert t.timeout == 30 + assert t.description == 'description-{}'.format(i) + + for i, t in enumerate(copied_templates): + assert t.timeout == 30 + assert t.description == 'description-{}'.format(i) + + # the Approval Template IDs on the *original* WFJT should not match *any* + # of the Approval Template IDs on the *copied* WFJT + assert not set([x.id for x in original_templates]).intersection( + set([x.id for x in copied_templates]) + ) + + # if you remove the " copy" suffix from the copied template names, they + # should match the original templates + assert ( + set([x.name for x in original_templates]) == + set([x.name.replace(' copy', '') for x in copied_templates]) + ) + + @pytest.mark.django_db def test_credential_copy(post, get, machine_credential, credentialtype_ssh, admin): assert get(