Skip to content

Commit

Permalink
Merge pull request #4264 from beeankha/workflow_pause_approve
Browse files Browse the repository at this point in the history
Workflow Approval Nodes

Reviewed-by: Ryan Petrello
             https://github.com/ryanpetrello
  • Loading branch information
softwarefactory-project-zuul[bot] authored Aug 28, 2019
2 parents 3d4cd1b + 97d9c26 commit 2918b6c
Show file tree
Hide file tree
Showing 78 changed files with 3,177 additions and 1,806 deletions.
18 changes: 17 additions & 1 deletion awx/api/generics.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
# 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 (
Expand Down Expand Up @@ -882,6 +883,21 @@ def copy_model_obj(old_parent, new_parent, model, obj, creater, copy_name='', cr
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
Expand Down
14 changes: 12 additions & 2 deletions awx/api/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

__all__ = ['ModelAccessPermission', 'JobTemplateCallbackPermission', 'VariableDataPermission',
'TaskPermission', 'ProjectUpdatePermission', 'InventoryInventorySourcesUpdatePermission',
'UserPermission', 'IsSuperUser', 'InstanceGroupTowerPermission',]
'UserPermission', 'IsSuperUser', 'InstanceGroupTowerPermission', 'WorkflowApprovalPermission']


class ModelAccessPermission(permissions.BasePermission):
Expand Down Expand Up @@ -196,6 +196,17 @@ def has_permission(self, request, view, obj=None):
return False


class WorkflowApprovalPermission(ModelAccessPermission):
'''
Permission check used by workflow `approval` and `deny` views to determine
who has access to approve and deny paused workflow nodes
'''

def check_post_permissions(self, request, view, obj=None):
approval = get_object_or_400(view.model, pk=view.kwargs['pk'])
return check_user_access(request.user, view.model, 'approve_or_deny', approval)


class ProjectUpdatePermission(ModelAccessPermission):
'''
Permission check used by ProjectUpdateView to determine who can update projects
Expand Down Expand Up @@ -238,4 +249,3 @@ def has_object_permission(self, request, view, obj):
if request.method == 'DELETE' and obj.name == "tower":
return False
return super(InstanceGroupTowerPermission, self).has_object_permission(request, view, obj)

130 changes: 119 additions & 11 deletions awx/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,16 @@
CENSOR_VALUE,
)
from awx.main.models import (
ActivityStream, AdHocCommand, AdHocCommandEvent, Credential,
CredentialInputSource, CredentialType, CustomInventoryScript,
Group, Host, Instance, InstanceGroup, Inventory, InventorySource,
InventoryUpdate, InventoryUpdateEvent, Job, JobEvent, JobHostSummary,
JobLaunchConfig, JobNotificationMixin, JobTemplate, Label, Notification,
NotificationTemplate, OAuth2AccessToken, OAuth2Application, Organization,
Project, ProjectUpdate, ProjectUpdateEvent, RefreshToken, Role, Schedule,
StdoutMaxBytesExceeded, SystemJob, SystemJobEvent, SystemJobTemplate,
Team, UnifiedJob, UnifiedJobTemplate, WorkflowJob, WorkflowJobNode,
WorkflowJobTemplate, WorkflowJobTemplateNode
ActivityStream, AdHocCommand, AdHocCommandEvent, Credential, CredentialInputSource,
CredentialType, CustomInventoryScript, Group, Host, Instance,
InstanceGroup, Inventory, InventorySource, InventoryUpdate,
InventoryUpdateEvent, Job, JobEvent, JobHostSummary, JobLaunchConfig,
JobNotificationMixin, JobTemplate, Label, Notification, NotificationTemplate,
OAuth2AccessToken, OAuth2Application, Organization, Project,
ProjectUpdate, ProjectUpdateEvent, RefreshToken, Role, Schedule,
SystemJob, SystemJobEvent, SystemJobTemplate, Team, UnifiedJob,
UnifiedJobTemplate, WorkflowApproval, WorkflowApprovalTemplate, WorkflowJob,
WorkflowJobNode, WorkflowJobTemplate, WorkflowJobTemplateNode, StdoutMaxBytesExceeded
)
from awx.main.models.base import VERBOSITY_CHOICES, NEW_JOB_TYPE_CHOICES
from awx.main.models.rbac import (
Expand Down Expand Up @@ -121,6 +121,8 @@
'job_template': DEFAULT_SUMMARY_FIELDS,
'workflow_job_template': DEFAULT_SUMMARY_FIELDS,
'workflow_job': DEFAULT_SUMMARY_FIELDS,
'workflow_approval_template': DEFAULT_SUMMARY_FIELDS + ('timeout',),
'workflow_approval': DEFAULT_SUMMARY_FIELDS + ('timeout',),
'schedule': DEFAULT_SUMMARY_FIELDS + ('next_run',),
'unified_job_template': DEFAULT_SUMMARY_FIELDS + ('unified_job_type',),
'last_job': DEFAULT_SUMMARY_FIELDS + ('finished', 'status', 'failed', 'license_error'),
Expand Down Expand Up @@ -681,6 +683,8 @@ def get_sub_serializer(self, obj):
serializer_class = SystemJobTemplateSerializer
elif isinstance(obj, WorkflowJobTemplate):
serializer_class = WorkflowJobTemplateSerializer
elif isinstance(obj, WorkflowApprovalTemplate):
serializer_class = WorkflowApprovalTemplateSerializer
return serializer_class

def to_representation(self, obj):
Expand Down Expand Up @@ -782,6 +786,8 @@ def get_sub_serializer(self, obj):
serializer_class = SystemJobSerializer
elif isinstance(obj, WorkflowJob):
serializer_class = WorkflowJobSerializer
elif isinstance(obj, WorkflowApproval):
serializer_class = WorkflowApprovalSerializer
return serializer_class

def to_representation(self, obj):
Expand Down Expand Up @@ -838,6 +844,8 @@ def get_sub_serializer(self, obj):
serializer_class = SystemJobListSerializer
elif isinstance(obj, WorkflowJob):
serializer_class = WorkflowJobListSerializer
elif isinstance(obj, WorkflowApproval):
serializer_class = WorkflowApprovalListSerializer
return serializer_class

def to_representation(self, obj):
Expand Down Expand Up @@ -3395,6 +3403,76 @@ class Meta:
fields = ('can_cancel',)


class WorkflowApprovalViewSerializer(UnifiedJobSerializer):

class Meta:
model = WorkflowApproval
fields = []


class WorkflowApprovalSerializer(UnifiedJobSerializer):

can_approve_or_deny = serializers.SerializerMethodField()
approval_expiration = serializers.SerializerMethodField()
timed_out = serializers.ReadOnlyField()

class Meta:
model = WorkflowApproval
fields = ('*', '-controller_node', '-execution_node', 'can_approve_or_deny', 'approval_expiration', 'timed_out',)

def get_approval_expiration(self, obj):
if obj.status != 'pending' or obj.timeout == 0:
return None
return obj.created + timedelta(seconds=obj.timeout)

def get_can_approve_or_deny(self, obj):
request = self.context.get('request', None)
allowed = request.user.can_access(WorkflowApproval, 'approve_or_deny', obj)
return allowed is True and obj.status == 'pending'

def get_related(self, obj):
res = super(WorkflowApprovalSerializer, self).get_related(obj)

if obj.workflow_approval_template:
res['workflow_approval_template'] = self.reverse('api:workflow_approval_template_detail',
kwargs={'pk': obj.workflow_approval_template.pk})
res['approve'] = self.reverse('api:workflow_approval_approve', kwargs={'pk': obj.pk})
res['deny'] = self.reverse('api:workflow_approval_deny', kwargs={'pk': obj.pk})
return res


class WorkflowApprovalActivityStreamSerializer(WorkflowApprovalSerializer):
"""
timed_out and status are usually read-only fields
However, when we generate an activity stream record, we *want* to record
these types of changes. This serializer allows us to do so.
"""
status = serializers.ChoiceField(choices=JobTemplate.JOB_TEMPLATE_STATUS_CHOICES)
timed_out = serializers.BooleanField()



class WorkflowApprovalListSerializer(WorkflowApprovalSerializer, UnifiedJobListSerializer):

class Meta:
fields = ('*', '-controller_node', '-execution_node', 'can_approve_or_deny', 'approval_expiration', 'timed_out',)


class WorkflowApprovalTemplateSerializer(UnifiedJobTemplateSerializer):

class Meta:
model = WorkflowApprovalTemplate
fields = ('*', 'timeout', 'name',)

def get_related(self, obj):
res = super(WorkflowApprovalTemplateSerializer, self).get_related(obj)
if 'last_job' in res:
del res['last_job']

res.update(dict(jobs = self.reverse('api:workflow_approval_template_jobs_list', kwargs={'pk': obj.pk}),))
return res


class LaunchConfigurationBaseSerializer(BaseSerializer):
scm_branch = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None)
job_type = serializers.ChoiceField(allow_blank=True, allow_null=True, required=False, default=None,
Expand Down Expand Up @@ -3453,6 +3531,10 @@ def validate(self, attrs):
ujt = attrs['unified_job_template']
elif self.instance:
ujt = self.instance.unified_job_template
if ujt is None:
if 'workflow_job_template' in attrs:
return {'workflow_job_template': attrs['workflow_job_template']}
return {}

# build additional field survey_passwords to track redacted variables
password_dict = {}
Expand Down Expand Up @@ -3534,6 +3616,7 @@ class Meta:

def get_related(self, obj):
res = super(WorkflowJobTemplateNodeSerializer, self).get_related(obj)
res['create_approval_template'] = self.reverse('api:workflow_job_template_node_create_approval', kwargs={'pk': obj.pk})
res['success_nodes'] = self.reverse('api:workflow_job_template_node_success_nodes_list', kwargs={'pk': obj.pk})
res['failure_nodes'] = self.reverse('api:workflow_job_template_node_failure_nodes_list', kwargs={'pk': obj.pk})
res['always_nodes'] = self.reverse('api:workflow_job_template_node_always_nodes_list', kwargs={'pk': obj.pk})
Expand All @@ -3553,6 +3636,12 @@ def build_relational_field(self, field_name, relation_info):
field_kwargs.pop('queryset', None)
return field_class, field_kwargs

def get_summary_fields(self, obj):
summary_fields = super(WorkflowJobTemplateNodeSerializer, self).get_summary_fields(obj)
if isinstance(obj.unified_job_template, WorkflowApprovalTemplate):
summary_fields['unified_job_template']['timeout'] = obj.unified_job_template.timeout
return summary_fields


class WorkflowJobNodeSerializer(LaunchConfigurationBaseSerializer):
success_nodes = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
Expand All @@ -3578,6 +3667,12 @@ def get_related(self, obj):
res['workflow_job'] = self.reverse('api:workflow_job_detail', kwargs={'pk': obj.workflow_job.pk})
return res

def get_summary_fields(self, obj):
summary_fields = super(WorkflowJobNodeSerializer, self).get_summary_fields(obj)
if isinstance(obj.job, WorkflowApproval):
summary_fields['job']['timed_out'] = obj.job.timed_out
return summary_fields


class WorkflowJobNodeListSerializer(WorkflowJobNodeSerializer):
pass
Expand All @@ -3603,6 +3698,16 @@ def build_relational_field(self, field_name, relation_info):
return field_class, field_kwargs


class WorkflowJobTemplateNodeCreateApprovalSerializer(BaseSerializer):

class Meta:
model = WorkflowApprovalTemplate
fields = ('timeout', 'name', 'description',)

def to_representation(self, obj):
return {}


class JobListSerializer(JobSerializer, UnifiedJobListSerializer):
pass

Expand Down Expand Up @@ -4663,7 +4768,8 @@ def _local_summarizable_fk_fields(self):
('o_auth2_access_token', ('id', 'user_id', 'description', 'application_id', 'scope')),
('o_auth2_application', ('id', 'name', 'description')),
('credential_type', ('id', 'name', 'description', 'kind', 'managed_by_tower')),
('ad_hoc_command', ('id', 'name', 'status', 'limit'))
('ad_hoc_command', ('id', 'name', 'status', 'limit')),
('workflow_approval', ('id', 'name', 'unified_job_id')),
]
return field_list

Expand Down Expand Up @@ -4772,6 +4878,8 @@ def _get_related_objects(self, obj, fk):
def _summarize_parent_ujt(self, obj, fk, summary_fields):
summary_keys = {'job': 'job_template',
'workflow_job_template_node': 'workflow_job_template',
'workflow_approval_template': 'workflow_job_template',
'workflow_approval': 'workflow_job',
'schedule': 'unified_job_template'}
if fk not in summary_keys:
return
Expand Down
5 changes: 5 additions & 0 deletions awx/api/urls/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
from .instance_group import urls as instance_group_urls
from .oauth2 import urls as oauth2_urls
from .oauth2_root import urls as oauth2_root_urls
from .workflow_approval_template import urls as workflow_approval_template_urls
from .workflow_approval import urls as workflow_approval_urls


v2_urls = [
Expand Down Expand Up @@ -131,8 +133,11 @@
url(r'^unified_job_templates/$', UnifiedJobTemplateList.as_view(), name='unified_job_template_list'),
url(r'^unified_jobs/$', UnifiedJobList.as_view(), name='unified_job_list'),
url(r'^activity_stream/', include(activity_stream_urls)),
url(r'^workflow_approval_templates/', include(workflow_approval_template_urls)),
url(r'^workflow_approvals/', include(workflow_approval_urls)),
]


app_name = 'api'
urlpatterns = [
url(r'^$', ApiRootView.as_view(), name='api_root_view'),
Expand Down
21 changes: 21 additions & 0 deletions awx/api/urls/workflow_approval.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright (c) 2017 Ansible, Inc.
# All Rights Reserved.

from django.conf.urls import url

from awx.api.views import (
WorkflowApprovalList,
WorkflowApprovalDetail,
WorkflowApprovalApprove,
WorkflowApprovalDeny,
)


urls = [
url(r'^$', WorkflowApprovalList.as_view(), name='workflow_approval_list'),
url(r'^(?P<pk>[0-9]+)/$', WorkflowApprovalDetail.as_view(), name='workflow_approval_detail'),
url(r'^(?P<pk>[0-9]+)/approve/$', WorkflowApprovalApprove.as_view(), name='workflow_approval_approve'),
url(r'^(?P<pk>[0-9]+)/deny/$', WorkflowApprovalDeny.as_view(), name='workflow_approval_deny'),
]

__all__ = ['urls']
17 changes: 17 additions & 0 deletions awx/api/urls/workflow_approval_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright (c) 2017 Ansible, Inc.
# All Rights Reserved.

from django.conf.urls import url

from awx.api.views import (
WorkflowApprovalTemplateDetail,
WorkflowApprovalTemplateJobsList,
)


urls = [
url(r'^(?P<pk>[0-9]+)/$', WorkflowApprovalTemplateDetail.as_view(), name='workflow_approval_template_detail'),
url(r'^(?P<pk>[0-9]+)/approvals/$', WorkflowApprovalTemplateJobsList.as_view(), name='workflow_approval_template_jobs_list'),
]

__all__ = ['urls']
2 changes: 2 additions & 0 deletions awx/api/urls/workflow_job_template_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
WorkflowJobTemplateNodeFailureNodesList,
WorkflowJobTemplateNodeAlwaysNodesList,
WorkflowJobTemplateNodeCredentialsList,
WorkflowJobTemplateNodeCreateApproval,
)


Expand All @@ -20,6 +21,7 @@
url(r'^(?P<pk>[0-9]+)/failure_nodes/$', WorkflowJobTemplateNodeFailureNodesList.as_view(), name='workflow_job_template_node_failure_nodes_list'),
url(r'^(?P<pk>[0-9]+)/always_nodes/$', WorkflowJobTemplateNodeAlwaysNodesList.as_view(), name='workflow_job_template_node_always_nodes_list'),
url(r'^(?P<pk>[0-9]+)/credentials/$', WorkflowJobTemplateNodeCredentialsList.as_view(), name='workflow_job_template_node_credentials_list'),
url(r'^(?P<pk>[0-9]+)/create_approval_template/$', WorkflowJobTemplateNodeCreateApproval.as_view(), name='workflow_job_template_node_create_approval'),
]

__all__ = ['urls']
Loading

0 comments on commit 2918b6c

Please sign in to comment.