mirror of
https://github.com/ansible/awx.git
synced 2026-05-06 17:07:36 -02:30
Merge pull request #4436 from jbradberry/webhook-receivers
Webhook receivers Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -249,3 +249,8 @@ class InstanceGroupTowerPermission(ModelAccessPermission):
|
|||||||
if request.method == 'DELETE' and obj.name == "tower":
|
if request.method == 'DELETE' and obj.name == "tower":
|
||||||
return False
|
return False
|
||||||
return super(InstanceGroupTowerPermission, self).has_object_permission(request, view, obj)
|
return super(InstanceGroupTowerPermission, self).has_object_permission(request, view, obj)
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookKeyPermission(permissions.BasePermission):
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
return request.user.can_access(view.model, 'admin', obj, request.data)
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ SUMMARIZABLE_FK_FIELDS = {
|
|||||||
'insights_credential': DEFAULT_SUMMARY_FIELDS,
|
'insights_credential': DEFAULT_SUMMARY_FIELDS,
|
||||||
'source_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'credential_type_id'),
|
'source_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'credential_type_id'),
|
||||||
'target_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'credential_type_id'),
|
'target_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'credential_type_id'),
|
||||||
|
'webhook_credential': DEFAULT_SUMMARY_FIELDS,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2826,6 +2827,25 @@ class JobTemplateMixin(object):
|
|||||||
d['recent_jobs'] = self._recent_jobs(obj)
|
d['recent_jobs'] = self._recent_jobs(obj)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
webhook_service = attrs.get('webhook_service', getattr(self.instance, 'webhook_service', None))
|
||||||
|
webhook_credential = attrs.get('webhook_credential', getattr(self.instance, 'webhook_credential', None))
|
||||||
|
|
||||||
|
if webhook_credential:
|
||||||
|
if webhook_credential.credential_type.kind != 'token':
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'webhook_credential': _("Must be a Personal Access Token."),
|
||||||
|
})
|
||||||
|
|
||||||
|
msg = {'webhook_credential': _("Must match the selected webhook service.")}
|
||||||
|
if webhook_service:
|
||||||
|
if webhook_credential.credential_type.namespace != '{}_token'.format(webhook_service):
|
||||||
|
raise serializers.ValidationError(msg)
|
||||||
|
else:
|
||||||
|
raise serializers.ValidationError(msg)
|
||||||
|
|
||||||
|
return super().validate(attrs)
|
||||||
|
|
||||||
|
|
||||||
class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobOptionsSerializer):
|
class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobOptionsSerializer):
|
||||||
show_capabilities = ['start', 'schedule', 'copy', 'edit', 'delete']
|
show_capabilities = ['start', 'schedule', 'copy', 'edit', 'delete']
|
||||||
@@ -2838,30 +2858,39 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = JobTemplate
|
model = JobTemplate
|
||||||
fields = ('*', 'host_config_key', 'ask_scm_branch_on_launch', 'ask_diff_mode_on_launch', 'ask_variables_on_launch',
|
fields = (
|
||||||
'ask_limit_on_launch', 'ask_tags_on_launch',
|
'*', 'host_config_key', 'ask_scm_branch_on_launch', 'ask_diff_mode_on_launch',
|
||||||
'ask_skip_tags_on_launch', 'ask_job_type_on_launch', 'ask_verbosity_on_launch', 'ask_inventory_on_launch',
|
'ask_variables_on_launch', 'ask_limit_on_launch', 'ask_tags_on_launch',
|
||||||
'ask_credential_on_launch', 'survey_enabled', 'become_enabled', 'diff_mode',
|
'ask_skip_tags_on_launch', 'ask_job_type_on_launch', 'ask_verbosity_on_launch',
|
||||||
'allow_simultaneous', 'custom_virtualenv', 'job_slice_count')
|
'ask_inventory_on_launch', 'ask_credential_on_launch', 'survey_enabled',
|
||||||
|
'become_enabled', 'diff_mode', 'allow_simultaneous', 'custom_virtualenv',
|
||||||
|
'job_slice_count', 'webhook_service', 'webhook_credential',
|
||||||
|
)
|
||||||
|
|
||||||
def get_related(self, obj):
|
def get_related(self, obj):
|
||||||
res = super(JobTemplateSerializer, self).get_related(obj)
|
res = super(JobTemplateSerializer, self).get_related(obj)
|
||||||
res.update(dict(
|
res.update(
|
||||||
jobs = self.reverse('api:job_template_jobs_list', kwargs={'pk': obj.pk}),
|
jobs=self.reverse('api:job_template_jobs_list', kwargs={'pk': obj.pk}),
|
||||||
schedules = self.reverse('api:job_template_schedules_list', kwargs={'pk': obj.pk}),
|
schedules=self.reverse('api:job_template_schedules_list', kwargs={'pk': obj.pk}),
|
||||||
activity_stream = self.reverse('api:job_template_activity_stream_list', kwargs={'pk': obj.pk}),
|
activity_stream=self.reverse('api:job_template_activity_stream_list', kwargs={'pk': obj.pk}),
|
||||||
launch = self.reverse('api:job_template_launch', kwargs={'pk': obj.pk}),
|
launch=self.reverse('api:job_template_launch', kwargs={'pk': obj.pk}),
|
||||||
notification_templates_started = self.reverse('api:job_template_notification_templates_started_list', kwargs={'pk': obj.pk}),
|
webhook_key=self.reverse('api:webhook_key', kwargs={'model_kwarg': 'job_templates', 'pk': obj.pk}),
|
||||||
notification_templates_success = self.reverse('api:job_template_notification_templates_success_list', kwargs={'pk': obj.pk}),
|
webhook_receiver=(
|
||||||
notification_templates_error = self.reverse('api:job_template_notification_templates_error_list', kwargs={'pk': obj.pk}),
|
self.reverse('api:webhook_receiver_{}'.format(obj.webhook_service),
|
||||||
access_list = self.reverse('api:job_template_access_list', kwargs={'pk': obj.pk}),
|
kwargs={'model_kwarg': 'job_templates', 'pk': obj.pk})
|
||||||
survey_spec = self.reverse('api:job_template_survey_spec', kwargs={'pk': obj.pk}),
|
if obj.webhook_service else ''
|
||||||
labels = self.reverse('api:job_template_label_list', kwargs={'pk': obj.pk}),
|
),
|
||||||
object_roles = self.reverse('api:job_template_object_roles_list', kwargs={'pk': obj.pk}),
|
notification_templates_started=self.reverse('api:job_template_notification_templates_started_list', kwargs={'pk': obj.pk}),
|
||||||
instance_groups = self.reverse('api:job_template_instance_groups_list', kwargs={'pk': obj.pk}),
|
notification_templates_success=self.reverse('api:job_template_notification_templates_success_list', kwargs={'pk': obj.pk}),
|
||||||
slice_workflow_jobs = self.reverse('api:job_template_slice_workflow_jobs_list', kwargs={'pk': obj.pk}),
|
notification_templates_error=self.reverse('api:job_template_notification_templates_error_list', kwargs={'pk': obj.pk}),
|
||||||
copy = self.reverse('api:job_template_copy', kwargs={'pk': obj.pk}),
|
access_list=self.reverse('api:job_template_access_list', kwargs={'pk': obj.pk}),
|
||||||
))
|
survey_spec=self.reverse('api:job_template_survey_spec', kwargs={'pk': obj.pk}),
|
||||||
|
labels=self.reverse('api:job_template_label_list', kwargs={'pk': obj.pk}),
|
||||||
|
object_roles=self.reverse('api:job_template_object_roles_list', kwargs={'pk': obj.pk}),
|
||||||
|
instance_groups=self.reverse('api:job_template_instance_groups_list', kwargs={'pk': obj.pk}),
|
||||||
|
slice_workflow_jobs=self.reverse('api:job_template_slice_workflow_jobs_list', kwargs={'pk': obj.pk}),
|
||||||
|
copy=self.reverse('api:job_template_copy', kwargs={'pk': obj.pk}),
|
||||||
|
)
|
||||||
if obj.host_config_key:
|
if obj.host_config_key:
|
||||||
res['callback'] = self.reverse('api:job_template_callback', kwargs={'pk': obj.pk})
|
res['callback'] = self.reverse('api:job_template_callback', kwargs={'pk': obj.pk})
|
||||||
return res
|
return res
|
||||||
@@ -2889,7 +2918,6 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO
|
|||||||
def validate_extra_vars(self, value):
|
def validate_extra_vars(self, value):
|
||||||
return vars_validate_or_raise(value)
|
return vars_validate_or_raise(value)
|
||||||
|
|
||||||
|
|
||||||
def get_summary_fields(self, obj):
|
def get_summary_fields(self, obj):
|
||||||
summary_fields = super(JobTemplateSerializer, self).get_summary_fields(obj)
|
summary_fields = super(JobTemplateSerializer, self).get_summary_fields(obj)
|
||||||
all_creds = []
|
all_creds = []
|
||||||
@@ -2930,9 +2958,11 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Job
|
model = Job
|
||||||
fields = ('*', 'job_template', 'passwords_needed_to_start',
|
fields = (
|
||||||
'allow_simultaneous', 'artifacts', 'scm_revision',
|
'*', 'job_template', 'passwords_needed_to_start', 'allow_simultaneous',
|
||||||
'instance_group', 'diff_mode', 'job_slice_number', 'job_slice_count')
|
'artifacts', 'scm_revision', 'instance_group', 'diff_mode', 'job_slice_number',
|
||||||
|
'job_slice_count', 'webhook_service', 'webhook_credential', 'webhook_guid',
|
||||||
|
)
|
||||||
|
|
||||||
def get_related(self, obj):
|
def get_related(self, obj):
|
||||||
res = super(JobSerializer, self).get_related(obj)
|
res = super(JobSerializer, self).get_related(obj)
|
||||||
@@ -3320,16 +3350,25 @@ class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJo
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WorkflowJobTemplate
|
model = WorkflowJobTemplate
|
||||||
fields = ('*', 'extra_vars', 'organization', 'survey_enabled', 'allow_simultaneous',
|
fields = (
|
||||||
'ask_variables_on_launch', 'inventory', 'limit', 'scm_branch',
|
'*', 'extra_vars', 'organization', 'survey_enabled', 'allow_simultaneous',
|
||||||
'ask_inventory_on_launch', 'ask_scm_branch_on_launch', 'ask_limit_on_launch',)
|
'ask_variables_on_launch', 'inventory', 'limit', 'scm_branch',
|
||||||
|
'ask_inventory_on_launch', 'ask_scm_branch_on_launch', 'ask_limit_on_launch',
|
||||||
|
'webhook_service', 'webhook_credential',
|
||||||
|
)
|
||||||
|
|
||||||
def get_related(self, obj):
|
def get_related(self, obj):
|
||||||
res = super(WorkflowJobTemplateSerializer, self).get_related(obj)
|
res = super(WorkflowJobTemplateSerializer, self).get_related(obj)
|
||||||
res.update(dict(
|
res.update(
|
||||||
workflow_jobs = self.reverse('api:workflow_job_template_jobs_list', kwargs={'pk': obj.pk}),
|
workflow_jobs = self.reverse('api:workflow_job_template_jobs_list', kwargs={'pk': obj.pk}),
|
||||||
schedules = self.reverse('api:workflow_job_template_schedules_list', kwargs={'pk': obj.pk}),
|
schedules = self.reverse('api:workflow_job_template_schedules_list', kwargs={'pk': obj.pk}),
|
||||||
launch = self.reverse('api:workflow_job_template_launch', kwargs={'pk': obj.pk}),
|
launch = self.reverse('api:workflow_job_template_launch', kwargs={'pk': obj.pk}),
|
||||||
|
webhook_key=self.reverse('api:webhook_key', kwargs={'model_kwarg': 'workflow_job_templates', 'pk': obj.pk}),
|
||||||
|
webhook_receiver=(
|
||||||
|
self.reverse('api:webhook_receiver_{}'.format(obj.webhook_service),
|
||||||
|
kwargs={'model_kwarg': 'workflow_job_templates', 'pk': obj.pk})
|
||||||
|
if obj.webhook_service else ''
|
||||||
|
),
|
||||||
workflow_nodes = self.reverse('api:workflow_job_template_workflow_nodes_list', kwargs={'pk': obj.pk}),
|
workflow_nodes = self.reverse('api:workflow_job_template_workflow_nodes_list', kwargs={'pk': obj.pk}),
|
||||||
labels = self.reverse('api:workflow_job_template_label_list', kwargs={'pk': obj.pk}),
|
labels = self.reverse('api:workflow_job_template_label_list', kwargs={'pk': obj.pk}),
|
||||||
activity_stream = self.reverse('api:workflow_job_template_activity_stream_list', kwargs={'pk': obj.pk}),
|
activity_stream = self.reverse('api:workflow_job_template_activity_stream_list', kwargs={'pk': obj.pk}),
|
||||||
@@ -3341,7 +3380,7 @@ class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJo
|
|||||||
object_roles = self.reverse('api:workflow_job_template_object_roles_list', kwargs={'pk': obj.pk}),
|
object_roles = self.reverse('api:workflow_job_template_object_roles_list', kwargs={'pk': obj.pk}),
|
||||||
survey_spec = self.reverse('api:workflow_job_template_survey_spec', kwargs={'pk': obj.pk}),
|
survey_spec = self.reverse('api:workflow_job_template_survey_spec', kwargs={'pk': obj.pk}),
|
||||||
copy = self.reverse('api:workflow_job_template_copy', kwargs={'pk': obj.pk}),
|
copy = self.reverse('api:workflow_job_template_copy', kwargs={'pk': obj.pk}),
|
||||||
))
|
)
|
||||||
if obj.organization:
|
if obj.organization:
|
||||||
res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk})
|
res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk})
|
||||||
return res
|
return res
|
||||||
@@ -3382,10 +3421,11 @@ class WorkflowJobSerializer(LabelsListMixin, UnifiedJobSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WorkflowJob
|
model = WorkflowJob
|
||||||
fields = ('*', 'workflow_job_template', 'extra_vars', 'allow_simultaneous',
|
fields = (
|
||||||
'job_template', 'is_sliced_job',
|
'*', 'workflow_job_template', 'extra_vars', 'allow_simultaneous', 'job_template',
|
||||||
'-execution_node', '-event_processing_finished', '-controller_node',
|
'is_sliced_job', '-execution_node', '-event_processing_finished', '-controller_node',
|
||||||
'inventory', 'limit', 'scm_branch',)
|
'inventory', 'limit', 'scm_branch', 'webhook_service', 'webhook_credential', 'webhook_guid',
|
||||||
|
)
|
||||||
|
|
||||||
def get_related(self, obj):
|
def get_related(self, obj):
|
||||||
res = super(WorkflowJobSerializer, self).get_related(obj)
|
res = super(WorkflowJobSerializer, self).get_related(obj)
|
||||||
|
|||||||
12
awx/api/templates/api/webhook_key_view.md
Normal file
12
awx/api/templates/api/webhook_key_view.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Webhook Secret Key:
|
||||||
|
|
||||||
|
Make a GET request to this resource to obtain the secret key for a job
|
||||||
|
template or workflow job template configured to be triggered by
|
||||||
|
webhook events. The response will include the following fields:
|
||||||
|
|
||||||
|
* `webhook_key`: Secret key that needs to be copied and added to the
|
||||||
|
webhook configuration of the service this template will be receiving
|
||||||
|
webhook events from (string, read-only)
|
||||||
|
|
||||||
|
Make an empty POST request to this resource to generate a new
|
||||||
|
replacement `webhook_key`.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Copyright (c) 2017 Ansible, Inc.
|
# Copyright (c) 2017 Ansible, Inc.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import include, url
|
||||||
|
|
||||||
from awx.api.views import (
|
from awx.api.views import (
|
||||||
JobTemplateList,
|
JobTemplateList,
|
||||||
@@ -45,6 +45,7 @@ urls = [
|
|||||||
url(r'^(?P<pk>[0-9]+)/object_roles/$', JobTemplateObjectRolesList.as_view(), name='job_template_object_roles_list'),
|
url(r'^(?P<pk>[0-9]+)/object_roles/$', JobTemplateObjectRolesList.as_view(), name='job_template_object_roles_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/labels/$', JobTemplateLabelList.as_view(), name='job_template_label_list'),
|
url(r'^(?P<pk>[0-9]+)/labels/$', JobTemplateLabelList.as_view(), name='job_template_label_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/copy/$', JobTemplateCopy.as_view(), name='job_template_copy'),
|
url(r'^(?P<pk>[0-9]+)/copy/$', JobTemplateCopy.as_view(), name='job_template_copy'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/', include('awx.api.urls.webhooks'), {'model_kwarg': 'job_templates'}),
|
||||||
]
|
]
|
||||||
|
|
||||||
__all__ = ['urls']
|
__all__ = ['urls']
|
||||||
|
|||||||
14
awx/api/urls/webhooks.py
Normal file
14
awx/api/urls/webhooks.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from awx.api.views import (
|
||||||
|
WebhookKeyView,
|
||||||
|
GithubWebhookReceiver,
|
||||||
|
GitlabWebhookReceiver,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^webhook_key/$', WebhookKeyView.as_view(), name='webhook_key'),
|
||||||
|
url(r'^github/$', GithubWebhookReceiver.as_view(), name='webhook_receiver_github'),
|
||||||
|
url(r'^gitlab/$', GitlabWebhookReceiver.as_view(), name='webhook_receiver_gitlab'),
|
||||||
|
]
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Copyright (c) 2017 Ansible, Inc.
|
# Copyright (c) 2017 Ansible, Inc.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import include, url
|
||||||
|
|
||||||
from awx.api.views import (
|
from awx.api.views import (
|
||||||
WorkflowJobTemplateList,
|
WorkflowJobTemplateList,
|
||||||
@@ -44,6 +44,7 @@ urls = [
|
|||||||
url(r'^(?P<pk>[0-9]+)/access_list/$', WorkflowJobTemplateAccessList.as_view(), name='workflow_job_template_access_list'),
|
url(r'^(?P<pk>[0-9]+)/access_list/$', WorkflowJobTemplateAccessList.as_view(), name='workflow_job_template_access_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/object_roles/$', WorkflowJobTemplateObjectRolesList.as_view(), name='workflow_job_template_object_roles_list'),
|
url(r'^(?P<pk>[0-9]+)/object_roles/$', WorkflowJobTemplateObjectRolesList.as_view(), name='workflow_job_template_object_roles_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/labels/$', WorkflowJobTemplateLabelList.as_view(), name='workflow_job_template_label_list'),
|
url(r'^(?P<pk>[0-9]+)/labels/$', WorkflowJobTemplateLabelList.as_view(), name='workflow_job_template_label_list'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/', include('awx.api.urls.webhooks'), {'model_kwarg': 'workflow_job_templates'}),
|
||||||
]
|
]
|
||||||
|
|
||||||
__all__ = ['urls']
|
__all__ = ['urls']
|
||||||
|
|||||||
@@ -150,6 +150,11 @@ from awx.api.views.root import ( # noqa
|
|||||||
ApiV2ConfigView,
|
ApiV2ConfigView,
|
||||||
ApiV2SubscriptionView,
|
ApiV2SubscriptionView,
|
||||||
)
|
)
|
||||||
|
from awx.api.views.webhooks import ( # noqa
|
||||||
|
WebhookKeyView,
|
||||||
|
GithubWebhookReceiver,
|
||||||
|
GitlabWebhookReceiver,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('awx.api.views')
|
logger = logging.getLogger('awx.api.views')
|
||||||
|
|||||||
247
awx/api/views/webhooks.py
Normal file
247
awx/api/views/webhooks.py
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
from hashlib import sha1
|
||||||
|
import hmac
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
from django.utils.encoding import force_bytes
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
from rest_framework.permissions import AllowAny
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from awx.api import serializers
|
||||||
|
from awx.api.generics import APIView, GenericAPIView
|
||||||
|
from awx.api.permissions import WebhookKeyPermission
|
||||||
|
from awx.main.models import Job, JobTemplate, WorkflowJob, WorkflowJobTemplate
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('awx.api.views.webhooks')
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookKeyView(GenericAPIView):
|
||||||
|
serializer_class = serializers.EmptySerializer
|
||||||
|
permission_classes = (WebhookKeyPermission,)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
qs_models = {
|
||||||
|
'job_templates': JobTemplate,
|
||||||
|
'workflow_job_templates': WorkflowJobTemplate,
|
||||||
|
}
|
||||||
|
self.model = qs_models.get(self.kwargs['model_kwarg'])
|
||||||
|
|
||||||
|
return super().get_queryset()
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
obj = self.get_object()
|
||||||
|
|
||||||
|
return Response({'webhook_key': obj.webhook_key})
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
obj = self.get_object()
|
||||||
|
obj.rotate_webhook_key()
|
||||||
|
obj.save(update_fields=['webhook_key'])
|
||||||
|
|
||||||
|
return Response({'webhook_key': obj.webhook_key}, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookReceiverBase(APIView):
|
||||||
|
lookup_url_kwarg = None
|
||||||
|
lookup_field = 'pk'
|
||||||
|
|
||||||
|
permission_classes = (AllowAny,)
|
||||||
|
authentication_classes = ()
|
||||||
|
|
||||||
|
ref_keys = {}
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
qs_models = {
|
||||||
|
'job_templates': JobTemplate,
|
||||||
|
'workflow_job_templates': WorkflowJobTemplate,
|
||||||
|
}
|
||||||
|
model = qs_models.get(self.kwargs['model_kwarg'])
|
||||||
|
if model is None:
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
|
return model.objects.filter(webhook_service=self.service).exclude(webhook_key='')
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
queryset = self.get_queryset()
|
||||||
|
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
||||||
|
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
|
||||||
|
|
||||||
|
obj = queryset.filter(**filter_kwargs).first()
|
||||||
|
if obj is None:
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def get_event_type(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_event_guid(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_event_status_api(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_event_ref(self):
|
||||||
|
key = self.ref_keys.get(self.get_event_type(), '')
|
||||||
|
value = self.request.data
|
||||||
|
for element in key.split('.'):
|
||||||
|
try:
|
||||||
|
if element.isdigit():
|
||||||
|
value = value[int(element)]
|
||||||
|
else:
|
||||||
|
value = (value or {}).get(element)
|
||||||
|
except Exception:
|
||||||
|
value = None
|
||||||
|
if value == '0000000000000000000000000000000000000000': # a deleted ref
|
||||||
|
value = None
|
||||||
|
return value
|
||||||
|
|
||||||
|
def get_signature(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def check_signature(self, obj):
|
||||||
|
if not obj.webhook_key:
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
|
mac = hmac.new(force_bytes(obj.webhook_key), msg=force_bytes(self.request.body), digestmod=sha1)
|
||||||
|
logger.debug("header signature: %s", self.get_signature())
|
||||||
|
logger.debug("calculated signature: %s", force_bytes(mac.hexdigest()))
|
||||||
|
if not hmac.compare_digest(force_bytes(mac.hexdigest()), self.get_signature()):
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
# Ensure that the full contents of the request are captured for multiple uses.
|
||||||
|
request.body
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"headers: {}\n"
|
||||||
|
"data: {}\n".format(request.headers, request.data)
|
||||||
|
)
|
||||||
|
obj = self.get_object()
|
||||||
|
self.check_signature(obj)
|
||||||
|
|
||||||
|
event_type = self.get_event_type()
|
||||||
|
event_guid = self.get_event_guid()
|
||||||
|
event_ref = self.get_event_ref()
|
||||||
|
status_api = self.get_event_status_api()
|
||||||
|
|
||||||
|
kwargs = {
|
||||||
|
'unified_job_template_id': obj.id,
|
||||||
|
'webhook_service': obj.webhook_service,
|
||||||
|
'webhook_guid': event_guid,
|
||||||
|
}
|
||||||
|
if WorkflowJob.objects.filter(**kwargs).exists() or Job.objects.filter(**kwargs).exists():
|
||||||
|
# Short circuit if this webhook has already been received and acted upon.
|
||||||
|
logger.debug("Webhook previously received, returning without action.")
|
||||||
|
return Response({'message': _("Webhook previously received, aborting.")},
|
||||||
|
status=status.HTTP_202_ACCEPTED)
|
||||||
|
|
||||||
|
kwargs = {
|
||||||
|
'_eager_fields': {
|
||||||
|
'launch_type': 'webhook',
|
||||||
|
'webhook_service': obj.webhook_service,
|
||||||
|
'webhook_credential': obj.webhook_credential,
|
||||||
|
'webhook_guid': event_guid,
|
||||||
|
},
|
||||||
|
'extra_vars': json.dumps({
|
||||||
|
'tower_webhook_event_type': event_type,
|
||||||
|
'tower_webhook_event_guid': event_guid,
|
||||||
|
'tower_webhook_event_ref': event_ref,
|
||||||
|
'tower_webhook_status_api': status_api,
|
||||||
|
'tower_webhook_payload': request.data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
new_job = obj.create_unified_job(**kwargs)
|
||||||
|
new_job.signal_start()
|
||||||
|
|
||||||
|
return Response({'message': "Job queued."}, status=status.HTTP_202_ACCEPTED)
|
||||||
|
|
||||||
|
|
||||||
|
class GithubWebhookReceiver(WebhookReceiverBase):
|
||||||
|
service = 'github'
|
||||||
|
|
||||||
|
ref_keys = {
|
||||||
|
'pull_request': 'pull_request.head.sha',
|
||||||
|
'pull_request_review': 'pull_request.head.sha',
|
||||||
|
'pull_request_review_comment': 'pull_request.head.sha',
|
||||||
|
'push': 'after',
|
||||||
|
'release': 'release.tag_name',
|
||||||
|
'commit_comment': 'comment.commit_id',
|
||||||
|
'create': 'ref',
|
||||||
|
'page_build': 'build.commit',
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_event_type(self):
|
||||||
|
return self.request.META.get('HTTP_X_GITHUB_EVENT')
|
||||||
|
|
||||||
|
def get_event_guid(self):
|
||||||
|
return self.request.META.get('HTTP_X_GITHUB_DELIVERY')
|
||||||
|
|
||||||
|
def get_event_status_api(self):
|
||||||
|
if self.get_event_type() != 'pull_request':
|
||||||
|
return
|
||||||
|
return self.request.data.get('pull_request', {}).get('statuses_url')
|
||||||
|
|
||||||
|
def get_signature(self):
|
||||||
|
header_sig = self.request.META.get('HTTP_X_HUB_SIGNATURE')
|
||||||
|
if not header_sig:
|
||||||
|
logger.debug("Expected signature missing from header key HTTP_X_HUB_SIGNATURE")
|
||||||
|
raise PermissionDenied
|
||||||
|
hash_alg, signature = header_sig.split('=')
|
||||||
|
if hash_alg != 'sha1':
|
||||||
|
logger.debug("Unsupported signature type, expected: sha1, received: {}".format(hash_alg))
|
||||||
|
raise PermissionDenied
|
||||||
|
return force_bytes(signature)
|
||||||
|
|
||||||
|
|
||||||
|
class GitlabWebhookReceiver(WebhookReceiverBase):
|
||||||
|
service = 'gitlab'
|
||||||
|
|
||||||
|
ref_keys = {
|
||||||
|
'Push Hook': 'checkout_sha',
|
||||||
|
'Tag Push Hook': 'checkout_sha',
|
||||||
|
'Merge Request Hook': 'object_attributes.last_commit.id',
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_event_type(self):
|
||||||
|
return self.request.META.get('HTTP_X_GITLAB_EVENT')
|
||||||
|
|
||||||
|
def get_event_guid(self):
|
||||||
|
# GitLab does not provide a unique identifier on events, so construct one.
|
||||||
|
h = sha1()
|
||||||
|
h.update(force_bytes(self.request.body))
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
def get_event_status_api(self):
|
||||||
|
if self.get_event_type() != 'Merge Request Hook':
|
||||||
|
return
|
||||||
|
project = self.request.data.get('project', {})
|
||||||
|
repo_url = project.get('web_url')
|
||||||
|
if not repo_url:
|
||||||
|
return
|
||||||
|
parsed = urllib.parse.urlparse(repo_url)
|
||||||
|
|
||||||
|
return "{}://{}/api/v4/projects/{}/statuses/{}".format(
|
||||||
|
parsed.scheme, parsed.netloc, project['id'], self.get_event_ref())
|
||||||
|
|
||||||
|
def get_signature(self):
|
||||||
|
return force_bytes(self.request.META.get('HTTP_X_GITLAB_TOKEN') or '')
|
||||||
|
|
||||||
|
def check_signature(self, obj):
|
||||||
|
if not obj.webhook_key:
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
|
# GitLab only returns the secret token, not an hmac hash. Use
|
||||||
|
# the hmac `compare_digest` helper function to prevent timing
|
||||||
|
# analysis by attackers.
|
||||||
|
if not hmac.compare_digest(force_bytes(obj.webhook_key), self.get_signature()):
|
||||||
|
raise PermissionDenied
|
||||||
49
awx/main/migrations/0092_v360_webhook_mixin.py
Normal file
49
awx/main/migrations/0092_v360_webhook_mixin.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Generated by Django 2.2.4 on 2019-09-12 14:49
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0091_v360_approval_node_notifications'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='jobtemplate',
|
||||||
|
name='webhook_credential',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Personal Access Token for posting back the status to the service API', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='jobtemplates', to='main.Credential'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='jobtemplate',
|
||||||
|
name='webhook_key',
|
||||||
|
field=models.CharField(blank=True, help_text='Shared secret that the webhook service will use to sign requests', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='jobtemplate',
|
||||||
|
name='webhook_service',
|
||||||
|
field=models.CharField(blank=True, choices=[('github', 'GitHub'), ('gitlab', 'GitLab')], help_text='Service that webhook requests will be accepted from', max_length=16),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='workflowjobtemplate',
|
||||||
|
name='webhook_credential',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Personal Access Token for posting back the status to the service API', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workflowjobtemplates', to='main.Credential'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='workflowjobtemplate',
|
||||||
|
name='webhook_key',
|
||||||
|
field=models.CharField(blank=True, help_text='Shared secret that the webhook service will use to sign requests', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='workflowjobtemplate',
|
||||||
|
name='webhook_service',
|
||||||
|
field=models.CharField(blank=True, choices=[('github', 'GitHub'), ('gitlab', 'GitLab')], help_text='Service that webhook requests will be accepted from', max_length=16),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='unifiedjob',
|
||||||
|
name='launch_type',
|
||||||
|
field=models.CharField(choices=[('manual', 'Manual'), ('relaunch', 'Relaunch'), ('callback', 'Callback'), ('scheduled', 'Scheduled'), ('dependency', 'Dependency'), ('workflow', 'Workflow'), ('webhook', 'Webhook'), ('sync', 'Sync'), ('scm', 'SCM Update')], db_index=True, default='manual', editable=False, max_length=20),
|
||||||
|
),
|
||||||
|
]
|
||||||
27
awx/main/migrations/0093_v360_personal_access_tokens.py
Normal file
27
awx/main/migrations/0093_v360_personal_access_tokens.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 2.2.4 on 2019-09-12 14:50
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
from awx.main.models import CredentialType
|
||||||
|
from awx.main.utils.common import set_current_apps
|
||||||
|
|
||||||
|
|
||||||
|
def setup_tower_managed_defaults(apps, schema_editor):
|
||||||
|
set_current_apps(apps)
|
||||||
|
CredentialType.setup_tower_managed_defaults()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0092_v360_webhook_mixin'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='credentialtype',
|
||||||
|
name='kind',
|
||||||
|
field=models.CharField(choices=[('ssh', 'Machine'), ('vault', 'Vault'), ('net', 'Network'), ('scm', 'Source Control'), ('cloud', 'Cloud'), ('token', 'Personal Access Token'), ('insights', 'Insights'), ('external', 'External')], max_length=32),
|
||||||
|
),
|
||||||
|
migrations.RunPython(setup_tower_managed_defaults),
|
||||||
|
]
|
||||||
44
awx/main/migrations/0094_v360_webhook_mixin2.py
Normal file
44
awx/main/migrations/0094_v360_webhook_mixin2.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Generated by Django 2.2.4 on 2019-09-12 14:52
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0093_v360_personal_access_tokens'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='job',
|
||||||
|
name='webhook_credential',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Personal Access Token for posting back the status to the service API', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='jobs', to='main.Credential'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='job',
|
||||||
|
name='webhook_guid',
|
||||||
|
field=models.CharField(blank=True, help_text='Unique identifier of the event that triggered this webhook', max_length=128),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='job',
|
||||||
|
name='webhook_service',
|
||||||
|
field=models.CharField(blank=True, choices=[('github', 'GitHub'), ('gitlab', 'GitLab')], help_text='Service that webhook requests will be accepted from', max_length=16),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='workflowjob',
|
||||||
|
name='webhook_credential',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Personal Access Token for posting back the status to the service API', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workflowjobs', to='main.Credential'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='workflowjob',
|
||||||
|
name='webhook_guid',
|
||||||
|
field=models.CharField(blank=True, help_text='Unique identifier of the event that triggered this webhook', max_length=128),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='workflowjob',
|
||||||
|
name='webhook_service',
|
||||||
|
field=models.CharField(blank=True, choices=[('github', 'GitHub'), ('gitlab', 'GitLab')], help_text='Service that webhook requests will be accepted from', max_length=16),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -322,6 +322,7 @@ class CredentialType(CommonModelNameNotUnique):
|
|||||||
('net', _('Network')),
|
('net', _('Network')),
|
||||||
('scm', _('Source Control')),
|
('scm', _('Source Control')),
|
||||||
('cloud', _('Cloud')),
|
('cloud', _('Cloud')),
|
||||||
|
('token', _('Personal Access Token')),
|
||||||
('insights', _('Insights')),
|
('insights', _('Insights')),
|
||||||
('external', _('External')),
|
('external', _('External')),
|
||||||
)
|
)
|
||||||
@@ -968,6 +969,40 @@ ManagedCredentialType(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ManagedCredentialType(
|
||||||
|
namespace='github_token',
|
||||||
|
kind='token',
|
||||||
|
name=ugettext_noop('GitHub Personal Access Token'),
|
||||||
|
managed_by_tower=True,
|
||||||
|
inputs={
|
||||||
|
'fields': [{
|
||||||
|
'id': 'token',
|
||||||
|
'label': ugettext_noop('Token'),
|
||||||
|
'type': 'string',
|
||||||
|
'secret': True,
|
||||||
|
'help_text': ugettext_noop('This token needs to come from your profile settings in GitHub')
|
||||||
|
}],
|
||||||
|
'required': ['token'],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
ManagedCredentialType(
|
||||||
|
namespace='gitlab_token',
|
||||||
|
kind='token',
|
||||||
|
name=ugettext_noop('GitLab Personal Access Token'),
|
||||||
|
managed_by_tower=True,
|
||||||
|
inputs={
|
||||||
|
'fields': [{
|
||||||
|
'id': 'token',
|
||||||
|
'label': ugettext_noop('Token'),
|
||||||
|
'type': 'string',
|
||||||
|
'secret': True,
|
||||||
|
'help_text': ugettext_noop('This token needs to come from your profile settings in GitLab')
|
||||||
|
}],
|
||||||
|
'required': ['token'],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
ManagedCredentialType(
|
ManagedCredentialType(
|
||||||
namespace='insights',
|
namespace='insights',
|
||||||
kind='insights',
|
kind='insights',
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ from awx.main.models.mixins import (
|
|||||||
TaskManagerJobMixin,
|
TaskManagerJobMixin,
|
||||||
CustomVirtualEnvMixin,
|
CustomVirtualEnvMixin,
|
||||||
RelatedJobsMixin,
|
RelatedJobsMixin,
|
||||||
|
WebhookMixin,
|
||||||
|
WebhookTemplateMixin,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -187,7 +189,7 @@ class JobOptions(BaseModel):
|
|||||||
return needed
|
return needed
|
||||||
|
|
||||||
|
|
||||||
class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, ResourceMixin, CustomVirtualEnvMixin, RelatedJobsMixin):
|
class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, ResourceMixin, CustomVirtualEnvMixin, RelatedJobsMixin, WebhookTemplateMixin):
|
||||||
'''
|
'''
|
||||||
A job template is a reusable job definition for applying a project (with
|
A job template is a reusable job definition for applying a project (with
|
||||||
playbook) to an inventory source with a given credential.
|
playbook) to an inventory source with a given credential.
|
||||||
@@ -484,7 +486,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
|
|||||||
return UnifiedJob.objects.filter(unified_job_template=self)
|
return UnifiedJob.objects.filter(unified_job_template=self)
|
||||||
|
|
||||||
|
|
||||||
class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskManagerJobMixin, CustomVirtualEnvMixin):
|
class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskManagerJobMixin, CustomVirtualEnvMixin, WebhookMixin):
|
||||||
'''
|
'''
|
||||||
A job applies a project (with playbook) to an inventory source with a given
|
A job applies a project (with playbook) to an inventory source with a given
|
||||||
credential. It represents a single invocation of ansible-playbook with the
|
credential. It represents a single invocation of ansible-playbook with the
|
||||||
|
|||||||
@@ -1,31 +1,37 @@
|
|||||||
# Python
|
# Python
|
||||||
import os
|
|
||||||
import json
|
|
||||||
from copy import copy, deepcopy
|
from copy import copy, deepcopy
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.contrib.auth.models import User # noqa
|
from django.contrib.auth.models import User # noqa
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
|
from django.utils.crypto import get_random_string
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.models.base import prevent_search
|
from awx.main.models.base import prevent_search
|
||||||
from awx.main.models.rbac import (
|
from awx.main.models.rbac import (
|
||||||
Role, RoleAncestorEntry, get_roles_on_resource
|
Role, RoleAncestorEntry, get_roles_on_resource
|
||||||
)
|
)
|
||||||
from awx.main.utils import parse_yaml_or_json, get_custom_venv_choices
|
from awx.main.utils import parse_yaml_or_json, get_custom_venv_choices, get_licenser
|
||||||
from awx.main.utils.encryption import decrypt_value, get_encryption_key, is_encrypted
|
from awx.main.utils.encryption import decrypt_value, get_encryption_key, is_encrypted
|
||||||
from awx.main.utils.polymorphic import build_polymorphic_ctypes_map
|
from awx.main.utils.polymorphic import build_polymorphic_ctypes_map
|
||||||
from awx.main.fields import JSONField, AskForField
|
from awx.main.fields import JSONField, AskForField
|
||||||
from awx.main.constants import ACTIVE_STATES
|
from awx.main.constants import ACTIVE_STATES
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('awx.main.models.mixins')
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['ResourceMixin', 'SurveyJobTemplateMixin', 'SurveyJobMixin',
|
__all__ = ['ResourceMixin', 'SurveyJobTemplateMixin', 'SurveyJobMixin',
|
||||||
'TaskManagerUnifiedJobMixin', 'TaskManagerJobMixin', 'TaskManagerProjectUpdateMixin',
|
'TaskManagerUnifiedJobMixin', 'TaskManagerJobMixin', 'TaskManagerProjectUpdateMixin',
|
||||||
'TaskManagerInventoryUpdateMixin', 'CustomVirtualEnvMixin']
|
'TaskManagerInventoryUpdateMixin', 'CustomVirtualEnvMixin']
|
||||||
@@ -483,3 +489,139 @@ class RelatedJobsMixin(object):
|
|||||||
raise RuntimeError("Programmer error. Expected _get_active_jobs() to return a QuerySet.")
|
raise RuntimeError("Programmer error. Expected _get_active_jobs() to return a QuerySet.")
|
||||||
|
|
||||||
return [dict(id=t[0], type=mapping[t[1]]) for t in jobs.values_list('id', 'polymorphic_ctype_id')]
|
return [dict(id=t[0], type=mapping[t[1]]) for t in jobs.values_list('id', 'polymorphic_ctype_id')]
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookTemplateMixin(models.Model):
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
SERVICES = [
|
||||||
|
('github', "GitHub"),
|
||||||
|
('gitlab', "GitLab"),
|
||||||
|
]
|
||||||
|
|
||||||
|
webhook_service = models.CharField(
|
||||||
|
max_length=16,
|
||||||
|
choices=SERVICES,
|
||||||
|
blank=True,
|
||||||
|
help_text=_('Service that webhook requests will be accepted from')
|
||||||
|
)
|
||||||
|
webhook_key = prevent_search(models.CharField(
|
||||||
|
max_length=64,
|
||||||
|
blank=True,
|
||||||
|
help_text=_('Shared secret that the webhook service will use to sign requests')
|
||||||
|
))
|
||||||
|
webhook_credential = models.ForeignKey(
|
||||||
|
'Credential',
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='%(class)ss',
|
||||||
|
help_text=_('Personal Access Token for posting back the status to the service API')
|
||||||
|
)
|
||||||
|
|
||||||
|
def rotate_webhook_key(self):
|
||||||
|
self.webhook_key = get_random_string(length=50)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
update_fields = kwargs.get('update_fields')
|
||||||
|
|
||||||
|
if not self.pk or self._values_have_edits({'webhook_service': self.webhook_service}):
|
||||||
|
if self.webhook_service:
|
||||||
|
self.rotate_webhook_key()
|
||||||
|
else:
|
||||||
|
self.webhook_key = ''
|
||||||
|
|
||||||
|
if update_fields and 'webhook_service' in update_fields:
|
||||||
|
update_fields.add('webhook_key')
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookMixin(models.Model):
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
SERVICES = WebhookTemplateMixin.SERVICES
|
||||||
|
|
||||||
|
webhook_service = models.CharField(
|
||||||
|
max_length=16,
|
||||||
|
choices=SERVICES,
|
||||||
|
blank=True,
|
||||||
|
help_text=_('Service that webhook requests will be accepted from')
|
||||||
|
)
|
||||||
|
webhook_credential = models.ForeignKey(
|
||||||
|
'Credential',
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='%(class)ss',
|
||||||
|
help_text=_('Personal Access Token for posting back the status to the service API')
|
||||||
|
)
|
||||||
|
webhook_guid = models.CharField(
|
||||||
|
blank=True,
|
||||||
|
max_length=128,
|
||||||
|
help_text=_('Unique identifier of the event that triggered this webhook')
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_webhook_status(self, status):
|
||||||
|
if not self.webhook_credential:
|
||||||
|
logger.debug("No credential configured to post back webhook status, skipping.")
|
||||||
|
return
|
||||||
|
|
||||||
|
status_api = self.extra_vars_dict.get('tower_webhook_status_api')
|
||||||
|
if not status_api:
|
||||||
|
logger.debug("Webhook event did not have a status API endpoint associated, skipping.")
|
||||||
|
return
|
||||||
|
|
||||||
|
service_header = {
|
||||||
|
'github': ('Authorization', 'token {}'),
|
||||||
|
'gitlab': ('PRIVATE-TOKEN', '{}'),
|
||||||
|
}
|
||||||
|
service_statuses = {
|
||||||
|
'github': {
|
||||||
|
'pending': 'pending',
|
||||||
|
'successful': 'success',
|
||||||
|
'failed': 'failure',
|
||||||
|
'canceled': 'failure', # GitHub doesn't have a 'canceled' status :(
|
||||||
|
'error': 'error',
|
||||||
|
},
|
||||||
|
'gitlab': {
|
||||||
|
'pending': 'pending',
|
||||||
|
'running': 'running',
|
||||||
|
'successful': 'success',
|
||||||
|
'failed': 'failed',
|
||||||
|
'error': 'failed', # GitLab doesn't have an 'error' status distinct from 'failed' :(
|
||||||
|
'canceled': 'canceled',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
statuses = service_statuses[self.webhook_service]
|
||||||
|
if status not in statuses:
|
||||||
|
logger.debug("Skipping webhook job status change: '{}'".format(status))
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
license_type = get_licenser().validate().get('license_type')
|
||||||
|
data = {
|
||||||
|
'state': statuses[status],
|
||||||
|
'context': 'ansible/awx' if license_type == 'open' else 'ansible/tower',
|
||||||
|
'target_url': self.get_ui_url(),
|
||||||
|
}
|
||||||
|
k, v = service_header[self.webhook_service]
|
||||||
|
headers = {
|
||||||
|
k: v.format(self.webhook_credential.get_input('token')),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
response = requests.post(status_api, data=json.dumps(data), headers=headers, timeout=30)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Posting webhook status caused an error.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if response.status_code < 400:
|
||||||
|
logger.debug("Webhook status update sent.")
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
"Posting webhook status failed, code: {}\n"
|
||||||
|
"{}\n"
|
||||||
|
"Payload sent: {}".format(response.status_code, response.text, json.dumps(data))
|
||||||
|
)
|
||||||
|
|||||||
@@ -532,6 +532,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
|||||||
('scheduled', _('Scheduled')), # Job was started from a schedule.
|
('scheduled', _('Scheduled')), # Job was started from a schedule.
|
||||||
('dependency', _('Dependency')), # Job was started as a dependency of another job.
|
('dependency', _('Dependency')), # Job was started as a dependency of another job.
|
||||||
('workflow', _('Workflow')), # Job was started from a workflow job.
|
('workflow', _('Workflow')), # Job was started from a workflow job.
|
||||||
|
('webhook', _('Webhook')), # Job was started from a webhook event.
|
||||||
('sync', _('Sync')), # Job was started from a project sync.
|
('sync', _('Sync')), # Job was started from a project sync.
|
||||||
('scm', _('SCM Update')) # Job was created as an Inventory SCM sync.
|
('scm', _('SCM Update')) # Job was created as an Inventory SCM sync.
|
||||||
]
|
]
|
||||||
@@ -1206,6 +1207,8 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
|||||||
|
|
||||||
def websocket_emit_status(self, status):
|
def websocket_emit_status(self, status):
|
||||||
connection.on_commit(lambda: self._websocket_emit_status(status))
|
connection.on_commit(lambda: self._websocket_emit_status(status))
|
||||||
|
if hasattr(self, 'update_webhook_status'):
|
||||||
|
connection.on_commit(lambda: self.update_webhook_status(status))
|
||||||
|
|
||||||
def notification_data(self):
|
def notification_data(self):
|
||||||
return dict(id=self.id,
|
return dict(id=self.id,
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ from awx.main.models.mixins import (
|
|||||||
SurveyJobTemplateMixin,
|
SurveyJobTemplateMixin,
|
||||||
SurveyJobMixin,
|
SurveyJobMixin,
|
||||||
RelatedJobsMixin,
|
RelatedJobsMixin,
|
||||||
|
WebhookMixin,
|
||||||
|
WebhookTemplateMixin,
|
||||||
)
|
)
|
||||||
from awx.main.models.jobs import LaunchTimeConfigBase, LaunchTimeConfig, JobTemplate
|
from awx.main.models.jobs import LaunchTimeConfigBase, LaunchTimeConfig, JobTemplate
|
||||||
from awx.main.models.credential import Credential
|
from awx.main.models.credential import Credential
|
||||||
@@ -358,7 +360,7 @@ class WorkflowJobOptions(LaunchTimeConfigBase):
|
|||||||
return new_workflow_job
|
return new_workflow_job
|
||||||
|
|
||||||
|
|
||||||
class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTemplateMixin, ResourceMixin, RelatedJobsMixin):
|
class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTemplateMixin, ResourceMixin, RelatedJobsMixin, WebhookTemplateMixin):
|
||||||
|
|
||||||
SOFT_UNIQUE_TOGETHER = [('polymorphic_ctype', 'name', 'organization')]
|
SOFT_UNIQUE_TOGETHER = [('polymorphic_ctype', 'name', 'organization')]
|
||||||
FIELDS_TO_PRESERVE_AT_COPY = [
|
FIELDS_TO_PRESERVE_AT_COPY = [
|
||||||
@@ -530,7 +532,7 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl
|
|||||||
return WorkflowJob.objects.filter(workflow_job_template=self)
|
return WorkflowJob.objects.filter(workflow_job_template=self)
|
||||||
|
|
||||||
|
|
||||||
class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificationMixin):
|
class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificationMixin, WebhookMixin):
|
||||||
class Meta:
|
class Meta:
|
||||||
app_label = 'main'
|
app_label = 'main'
|
||||||
ordering = ('id',)
|
ordering = ('id',)
|
||||||
|
|||||||
@@ -154,12 +154,12 @@ def mk_job_template(name, job_type='run',
|
|||||||
organization=None, inventory=None,
|
organization=None, inventory=None,
|
||||||
credential=None, network_credential=None,
|
credential=None, network_credential=None,
|
||||||
cloud_credential=None, persisted=True, extra_vars='',
|
cloud_credential=None, persisted=True, extra_vars='',
|
||||||
project=None, spec=None):
|
project=None, spec=None, webhook_service=''):
|
||||||
if extra_vars:
|
if extra_vars:
|
||||||
extra_vars = json.dumps(extra_vars)
|
extra_vars = json.dumps(extra_vars)
|
||||||
|
|
||||||
jt = JobTemplate(name=name, job_type=job_type, extra_vars=extra_vars,
|
jt = JobTemplate(name=name, job_type=job_type, extra_vars=extra_vars,
|
||||||
playbook='helloworld.yml')
|
webhook_service=webhook_service, playbook='helloworld.yml')
|
||||||
|
|
||||||
jt.inventory = inventory
|
jt.inventory = inventory
|
||||||
if jt.inventory is None:
|
if jt.inventory is None:
|
||||||
@@ -200,11 +200,13 @@ def mk_workflow_job(status='new', workflow_job_template=None, extra_vars={},
|
|||||||
return job
|
return job
|
||||||
|
|
||||||
|
|
||||||
def mk_workflow_job_template(name, extra_vars='', spec=None, organization=None, persisted=True):
|
def mk_workflow_job_template(name, extra_vars='', spec=None, organization=None, persisted=True,
|
||||||
|
webhook_service=''):
|
||||||
if extra_vars:
|
if extra_vars:
|
||||||
extra_vars = json.dumps(extra_vars)
|
extra_vars = json.dumps(extra_vars)
|
||||||
|
|
||||||
wfjt = WorkflowJobTemplate(name=name, extra_vars=extra_vars, organization=organization)
|
wfjt = WorkflowJobTemplate(name=name, extra_vars=extra_vars, organization=organization,
|
||||||
|
webhook_service=webhook_service)
|
||||||
|
|
||||||
wfjt.survey_spec = spec
|
wfjt.survey_spec = spec
|
||||||
if wfjt.survey_spec:
|
if wfjt.survey_spec:
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ def create_survey_spec(variables=None, default_type='integer', required=True, mi
|
|||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
def create_job_template(name, roles=None, persisted=True, **kwargs):
|
def create_job_template(name, roles=None, persisted=True, webhook_service='', **kwargs):
|
||||||
Objects = generate_objects(["job_template", "jobs",
|
Objects = generate_objects(["job_template", "jobs",
|
||||||
"organization",
|
"organization",
|
||||||
"inventory",
|
"inventory",
|
||||||
@@ -252,11 +252,10 @@ def create_job_template(name, roles=None, persisted=True, **kwargs):
|
|||||||
else:
|
else:
|
||||||
spec = None
|
spec = None
|
||||||
|
|
||||||
jt = mk_job_template(name, project=proj,
|
jt = mk_job_template(name, project=proj, inventory=inv, credential=cred,
|
||||||
inventory=inv, credential=cred,
|
|
||||||
network_credential=net_cred, cloud_credential=cloud_cred,
|
network_credential=net_cred, cloud_credential=cloud_cred,
|
||||||
job_type=job_type, spec=spec, extra_vars=extra_vars,
|
job_type=job_type, spec=spec, extra_vars=extra_vars,
|
||||||
persisted=persisted)
|
persisted=persisted, webhook_service=webhook_service)
|
||||||
|
|
||||||
if 'jobs' in kwargs:
|
if 'jobs' in kwargs:
|
||||||
for i in kwargs['jobs']:
|
for i in kwargs['jobs']:
|
||||||
@@ -401,7 +400,7 @@ def generate_workflow_job_template_nodes(workflow_job_template,
|
|||||||
|
|
||||||
|
|
||||||
# TODO: Implement survey and jobs
|
# TODO: Implement survey and jobs
|
||||||
def create_workflow_job_template(name, organization=None, persisted=True, **kwargs):
|
def create_workflow_job_template(name, organization=None, persisted=True, webhook_service='', **kwargs):
|
||||||
Objects = generate_objects(["workflow_job_template",
|
Objects = generate_objects(["workflow_job_template",
|
||||||
"workflow_job_template_nodes",
|
"workflow_job_template_nodes",
|
||||||
"survey",], kwargs)
|
"survey",], kwargs)
|
||||||
@@ -418,7 +417,8 @@ def create_workflow_job_template(name, organization=None, persisted=True, **kwar
|
|||||||
organization=organization,
|
organization=organization,
|
||||||
spec=spec,
|
spec=spec,
|
||||||
extra_vars=extra_vars,
|
extra_vars=extra_vars,
|
||||||
persisted=persisted)
|
persisted=persisted,
|
||||||
|
webhook_service=webhook_service)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
260
awx/main/tests/functional/api/test_webhooks.py
Normal file
260
awx/main/tests/functional/api/test_webhooks.py
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from awx.api.versioning import reverse
|
||||||
|
from awx.main.models.mixins import WebhookTemplateMixin
|
||||||
|
from awx.main.models.credential import Credential, CredentialType
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"user_role, expect", [
|
||||||
|
('superuser', 200),
|
||||||
|
('org admin', 200),
|
||||||
|
('jt admin', 200),
|
||||||
|
('jt execute', 403),
|
||||||
|
('org member', 403),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_get_webhook_key_jt(organization_factory, job_template_factory, get, user_role, expect):
|
||||||
|
objs = organization_factory("org", superusers=['admin'], users=['user'])
|
||||||
|
jt = job_template_factory("jt", organization=objs.organization,
|
||||||
|
inventory='test_inv', project='test_proj').job_template
|
||||||
|
if user_role == 'superuser':
|
||||||
|
user = objs.superusers.admin
|
||||||
|
else:
|
||||||
|
user = objs.users.user
|
||||||
|
grant_obj = objs.organization if user_role.startswith('org') else jt
|
||||||
|
getattr(grant_obj, '{}_role'.format(user_role.split()[1])).members.add(user)
|
||||||
|
|
||||||
|
url = reverse('api:webhook_key', kwargs={'model_kwarg': 'job_templates', 'pk': jt.pk})
|
||||||
|
response = get(url, user=user, expect=expect)
|
||||||
|
if expect < 400:
|
||||||
|
assert response.data == {'webhook_key': ''}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"user_role, expect", [
|
||||||
|
('superuser', 200),
|
||||||
|
('org admin', 200),
|
||||||
|
('jt admin', 200),
|
||||||
|
('jt execute', 403),
|
||||||
|
('org member', 403),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_get_webhook_key_wfjt(organization_factory, workflow_job_template_factory, get, user_role, expect):
|
||||||
|
objs = organization_factory("org", superusers=['admin'], users=['user'])
|
||||||
|
wfjt = workflow_job_template_factory("wfjt", organization=objs.organization).workflow_job_template
|
||||||
|
if user_role == 'superuser':
|
||||||
|
user = objs.superusers.admin
|
||||||
|
else:
|
||||||
|
user = objs.users.user
|
||||||
|
grant_obj = objs.organization if user_role.startswith('org') else wfjt
|
||||||
|
getattr(grant_obj, '{}_role'.format(user_role.split()[1])).members.add(user)
|
||||||
|
|
||||||
|
url = reverse('api:webhook_key', kwargs={'model_kwarg': 'workflow_job_templates', 'pk': wfjt.pk})
|
||||||
|
response = get(url, user=user, expect=expect)
|
||||||
|
if expect < 400:
|
||||||
|
assert response.data == {'webhook_key': ''}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"user_role, expect", [
|
||||||
|
('superuser', 201),
|
||||||
|
('org admin', 201),
|
||||||
|
('jt admin', 201),
|
||||||
|
('jt execute', 403),
|
||||||
|
('org member', 403),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_post_webhook_key_jt(organization_factory, job_template_factory, post, user_role, expect):
|
||||||
|
objs = organization_factory("org", superusers=['admin'], users=['user'])
|
||||||
|
jt = job_template_factory("jt", organization=objs.organization,
|
||||||
|
inventory='test_inv', project='test_proj').job_template
|
||||||
|
if user_role == 'superuser':
|
||||||
|
user = objs.superusers.admin
|
||||||
|
else:
|
||||||
|
user = objs.users.user
|
||||||
|
grant_obj = objs.organization if user_role.startswith('org') else jt
|
||||||
|
getattr(grant_obj, '{}_role'.format(user_role.split()[1])).members.add(user)
|
||||||
|
|
||||||
|
url = reverse('api:webhook_key', kwargs={'model_kwarg': 'job_templates', 'pk': jt.pk})
|
||||||
|
response = post(url, {}, user=user, expect=expect)
|
||||||
|
if expect < 400:
|
||||||
|
assert bool(response.data.get('webhook_key'))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"user_role, expect", [
|
||||||
|
('superuser', 201),
|
||||||
|
('org admin', 201),
|
||||||
|
('jt admin', 201),
|
||||||
|
('jt execute', 403),
|
||||||
|
('org member', 403),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_post_webhook_key_wfjt(organization_factory, workflow_job_template_factory, post, user_role, expect):
|
||||||
|
objs = organization_factory("org", superusers=['admin'], users=['user'])
|
||||||
|
wfjt = workflow_job_template_factory("wfjt", organization=objs.organization).workflow_job_template
|
||||||
|
if user_role == 'superuser':
|
||||||
|
user = objs.superusers.admin
|
||||||
|
else:
|
||||||
|
user = objs.users.user
|
||||||
|
grant_obj = objs.organization if user_role.startswith('org') else wfjt
|
||||||
|
getattr(grant_obj, '{}_role'.format(user_role.split()[1])).members.add(user)
|
||||||
|
|
||||||
|
url = reverse('api:webhook_key', kwargs={'model_kwarg': 'workflow_job_templates', 'pk': wfjt.pk})
|
||||||
|
response = post(url, {}, user=user, expect=expect)
|
||||||
|
if expect < 400:
|
||||||
|
assert bool(response.data.get('webhook_key'))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"service", [s for s, _ in WebhookTemplateMixin.SERVICES]
|
||||||
|
)
|
||||||
|
def test_set_webhook_service(organization_factory, job_template_factory, patch, service):
|
||||||
|
objs = organization_factory("org", superusers=['admin'])
|
||||||
|
jt = job_template_factory("jt", organization=objs.organization,
|
||||||
|
inventory='test_inv', project='test_proj').job_template
|
||||||
|
admin = objs.superusers.admin
|
||||||
|
assert (jt.webhook_service, jt.webhook_key) == ('', '')
|
||||||
|
|
||||||
|
url = reverse('api:job_template_detail', kwargs={'pk': jt.pk})
|
||||||
|
patch(url, {'webhook_service': service}, user=admin, expect=200)
|
||||||
|
jt.refresh_from_db()
|
||||||
|
|
||||||
|
assert jt.webhook_service == service
|
||||||
|
assert jt.webhook_key != ''
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"service", [s for s, _ in WebhookTemplateMixin.SERVICES]
|
||||||
|
)
|
||||||
|
def test_unset_webhook_service(organization_factory, job_template_factory, patch, service):
|
||||||
|
objs = organization_factory("org", superusers=['admin'])
|
||||||
|
jt = job_template_factory("jt", organization=objs.organization, webhook_service=service,
|
||||||
|
inventory='test_inv', project='test_proj').job_template
|
||||||
|
admin = objs.superusers.admin
|
||||||
|
assert jt.webhook_service == service
|
||||||
|
assert jt.webhook_key != ''
|
||||||
|
|
||||||
|
url = reverse('api:job_template_detail', kwargs={'pk': jt.pk})
|
||||||
|
patch(url, {'webhook_service': ''}, user=admin, expect=200)
|
||||||
|
jt.refresh_from_db()
|
||||||
|
|
||||||
|
assert (jt.webhook_service, jt.webhook_key) == ('', '')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"service", [s for s, _ in WebhookTemplateMixin.SERVICES]
|
||||||
|
)
|
||||||
|
def test_set_webhook_credential(organization_factory, job_template_factory, patch, service):
|
||||||
|
objs = organization_factory("org", superusers=['admin'])
|
||||||
|
jt = job_template_factory("jt", organization=objs.organization, webhook_service=service,
|
||||||
|
inventory='test_inv', project='test_proj').job_template
|
||||||
|
admin = objs.superusers.admin
|
||||||
|
assert jt.webhook_service == service
|
||||||
|
assert jt.webhook_key != ''
|
||||||
|
|
||||||
|
cred_type = CredentialType.defaults['{}_token'.format(service)]()
|
||||||
|
cred_type.save()
|
||||||
|
cred = Credential.objects.create(credential_type=cred_type, name='test-cred',
|
||||||
|
inputs={'token': 'secret'})
|
||||||
|
|
||||||
|
url = reverse('api:job_template_detail', kwargs={'pk': jt.pk})
|
||||||
|
patch(url, {'webhook_credential': cred.pk}, user=admin, expect=200)
|
||||||
|
jt.refresh_from_db()
|
||||||
|
|
||||||
|
assert jt.webhook_service == service
|
||||||
|
assert jt.webhook_key != ''
|
||||||
|
assert jt.webhook_credential == cred
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"service,token", [
|
||||||
|
(s, WebhookTemplateMixin.SERVICES[i - 1][0]) for i, (s, _) in enumerate(WebhookTemplateMixin.SERVICES)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_set_wrong_service_webhook_credential(organization_factory, job_template_factory, patch, service, token):
|
||||||
|
objs = organization_factory("org", superusers=['admin'])
|
||||||
|
jt = job_template_factory("jt", organization=objs.organization, webhook_service=service,
|
||||||
|
inventory='test_inv', project='test_proj').job_template
|
||||||
|
admin = objs.superusers.admin
|
||||||
|
assert jt.webhook_service == service
|
||||||
|
assert jt.webhook_key != ''
|
||||||
|
|
||||||
|
cred_type = CredentialType.defaults['{}_token'.format(token)]()
|
||||||
|
cred_type.save()
|
||||||
|
cred = Credential.objects.create(credential_type=cred_type, name='test-cred',
|
||||||
|
inputs={'token': 'secret'})
|
||||||
|
|
||||||
|
url = reverse('api:job_template_detail', kwargs={'pk': jt.pk})
|
||||||
|
response = patch(url, {'webhook_credential': cred.pk}, user=admin, expect=400)
|
||||||
|
jt.refresh_from_db()
|
||||||
|
|
||||||
|
assert jt.webhook_service == service
|
||||||
|
assert jt.webhook_key != ''
|
||||||
|
assert jt.webhook_credential is None
|
||||||
|
assert response.data == {'webhook_credential': ["Must match the selected webhook service."]}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"service", [s for s, _ in WebhookTemplateMixin.SERVICES]
|
||||||
|
)
|
||||||
|
def test_set_webhook_credential_without_service(organization_factory, job_template_factory, patch, service):
|
||||||
|
objs = organization_factory("org", superusers=['admin'])
|
||||||
|
jt = job_template_factory("jt", organization=objs.organization,
|
||||||
|
inventory='test_inv', project='test_proj').job_template
|
||||||
|
admin = objs.superusers.admin
|
||||||
|
assert jt.webhook_service == ''
|
||||||
|
assert jt.webhook_key == ''
|
||||||
|
|
||||||
|
cred_type = CredentialType.defaults['{}_token'.format(service)]()
|
||||||
|
cred_type.save()
|
||||||
|
cred = Credential.objects.create(credential_type=cred_type, name='test-cred',
|
||||||
|
inputs={'token': 'secret'})
|
||||||
|
|
||||||
|
url = reverse('api:job_template_detail', kwargs={'pk': jt.pk})
|
||||||
|
response = patch(url, {'webhook_credential': cred.pk}, user=admin, expect=400)
|
||||||
|
jt.refresh_from_db()
|
||||||
|
|
||||||
|
assert jt.webhook_service == ''
|
||||||
|
assert jt.webhook_key == ''
|
||||||
|
assert jt.webhook_credential is None
|
||||||
|
assert response.data == {'webhook_credential': ["Must match the selected webhook service."]}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"service", [s for s, _ in WebhookTemplateMixin.SERVICES]
|
||||||
|
)
|
||||||
|
def test_unset_webhook_service_with_credential(organization_factory, job_template_factory, patch, service):
|
||||||
|
objs = organization_factory("org", superusers=['admin'])
|
||||||
|
jt = job_template_factory("jt", organization=objs.organization, webhook_service=service,
|
||||||
|
inventory='test_inv', project='test_proj').job_template
|
||||||
|
admin = objs.superusers.admin
|
||||||
|
assert jt.webhook_service == service
|
||||||
|
assert jt.webhook_key != ''
|
||||||
|
|
||||||
|
cred_type = CredentialType.defaults['{}_token'.format(service)]()
|
||||||
|
cred_type.save()
|
||||||
|
cred = Credential.objects.create(credential_type=cred_type, name='test-cred',
|
||||||
|
inputs={'token': 'secret'})
|
||||||
|
jt.webhook_credential = cred
|
||||||
|
jt.save()
|
||||||
|
|
||||||
|
url = reverse('api:job_template_detail', kwargs={'pk': jt.pk})
|
||||||
|
response = patch(url, {'webhook_service': ''}, user=admin, expect=400)
|
||||||
|
jt.refresh_from_db()
|
||||||
|
|
||||||
|
assert jt.webhook_service == service
|
||||||
|
assert jt.webhook_key != ''
|
||||||
|
assert jt.webhook_credential == cred
|
||||||
|
assert response.data == {'webhook_credential': ["Must match the selected webhook service."]}
|
||||||
@@ -82,6 +82,8 @@ def test_default_cred_types():
|
|||||||
'cloudforms',
|
'cloudforms',
|
||||||
'conjur',
|
'conjur',
|
||||||
'gce',
|
'gce',
|
||||||
|
'github_token',
|
||||||
|
'gitlab_token',
|
||||||
'hashivault_kv',
|
'hashivault_kv',
|
||||||
'hashivault_ssh',
|
'hashivault_ssh',
|
||||||
'insights',
|
'insights',
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ def job_template(mocker):
|
|||||||
mock_jt.pk = 5
|
mock_jt.pk = 5
|
||||||
mock_jt.host_config_key = '9283920492'
|
mock_jt.host_config_key = '9283920492'
|
||||||
mock_jt.validation_errors = mock_JT_resource_data
|
mock_jt.validation_errors = mock_JT_resource_data
|
||||||
|
mock_jt.webhook_service = ''
|
||||||
return mock_jt
|
return mock_jt
|
||||||
|
|
||||||
|
|
||||||
@@ -50,6 +51,7 @@ class TestJobTemplateSerializerGetRelated():
|
|||||||
'schedules',
|
'schedules',
|
||||||
'activity_stream',
|
'activity_stream',
|
||||||
'launch',
|
'launch',
|
||||||
|
'webhook_key',
|
||||||
'notification_templates_started',
|
'notification_templates_started',
|
||||||
'notification_templates_success',
|
'notification_templates_success',
|
||||||
'notification_templates_error',
|
'notification_templates_error',
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class TestWorkflowJobTemplateSerializerGetRelated():
|
|||||||
'workflow_jobs',
|
'workflow_jobs',
|
||||||
'launch',
|
'launch',
|
||||||
'workflow_nodes',
|
'workflow_nodes',
|
||||||
|
'webhook_key',
|
||||||
])
|
])
|
||||||
def test_get_related(self, mocker, test_get_related, workflow_job_template, related_resource_name):
|
def test_get_related(self, mocker, test_get_related, workflow_job_template, related_resource_name):
|
||||||
test_get_related(WorkflowJobTemplateSerializer,
|
test_get_related(WorkflowJobTemplateSerializer,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ function JobsStrings (BaseString) {
|
|||||||
ROW_ITEM_LABEL_INVENTORY: t.s('Inventory'),
|
ROW_ITEM_LABEL_INVENTORY: t.s('Inventory'),
|
||||||
ROW_ITEM_LABEL_PROJECT: t.s('Project'),
|
ROW_ITEM_LABEL_PROJECT: t.s('Project'),
|
||||||
ROW_ITEM_LABEL_CREDENTIALS: t.s('Credentials'),
|
ROW_ITEM_LABEL_CREDENTIALS: t.s('Credentials'),
|
||||||
|
ROW_ITEM_LABEL_WEBHOOK: t.s('Webhook'),
|
||||||
NO_RUNNING: t.s('There are no running jobs.'),
|
NO_RUNNING: t.s('There are no running jobs.'),
|
||||||
JOB: t.s('Job'),
|
JOB: t.s('Job'),
|
||||||
STATUS_TOOLTIP: status => t.s('Job {{status}}. Click for details.', { status }),
|
STATUS_TOOLTIP: status => t.s('Job {{status}}. Click for details.', { status }),
|
||||||
|
|||||||
@@ -142,19 +142,13 @@ function ListJobsController (
|
|||||||
return { icon, link, value };
|
return { icon, link, value };
|
||||||
});
|
});
|
||||||
|
|
||||||
vm.getSliceJobDetails = (job) => {
|
vm.getSecondaryTagLabel = (job) => {
|
||||||
if (!job.job_slice_count) {
|
if (job.job_slice_number && job.job_slice_count && job.job_slice_count > 1) {
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (job.job_slice_count === 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (job.job_slice_number && job.job_slice_count) {
|
|
||||||
return `${strings.get('list.SLICE_JOB')} ${job.job_slice_number}/${job.job_slice_count}`;
|
return `${strings.get('list.SLICE_JOB')} ${job.job_slice_number}/${job.job_slice_count}`;
|
||||||
}
|
}
|
||||||
|
if (job.launch_type === 'webhook') {
|
||||||
|
return strings.get('list.ROW_ITEM_LABEL_WEBHOOK');
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
header-value="{{ job.id }} - {{ job.name }}"
|
header-value="{{ job.id }} - {{ job.name }}"
|
||||||
header-state="{{ vm.getSref(job) }}"
|
header-state="{{ vm.getSref(job) }}"
|
||||||
header-tag="{{ vm.jobTypes[job.type] }}"
|
header-tag="{{ vm.jobTypes[job.type] }}"
|
||||||
secondary-tag="{{ vm.getSliceJobDetails(job) }}">
|
secondary-tag="{{ vm.getSecondaryTagLabel(job) }}">
|
||||||
</at-row-item>
|
</at-row-item>
|
||||||
<div class="at-Row-actions">
|
<div class="at-Row-actions">
|
||||||
<at-relaunch job="job" ng-show="job.summary_fields.user_capabilities.start">
|
<at-relaunch job="job" ng-show="job.summary_fields.user_capabilities.start">
|
||||||
|
|||||||
@@ -251,10 +251,12 @@ function getHostLimitErrorDetails () {
|
|||||||
function getLaunchedByDetails () {
|
function getLaunchedByDetails () {
|
||||||
const createdBy = resource.model.get('summary_fields.created_by');
|
const createdBy = resource.model.get('summary_fields.created_by');
|
||||||
const jobTemplate = resource.model.get('summary_fields.job_template');
|
const jobTemplate = resource.model.get('summary_fields.job_template');
|
||||||
|
const workflowJobTemplate = resource.model.get('summary_fields.workflow_job_template');
|
||||||
const relatedSchedule = resource.model.get('related.schedule');
|
const relatedSchedule = resource.model.get('related.schedule');
|
||||||
const schedule = resource.model.get('summary_fields.schedule');
|
const schedule = resource.model.get('summary_fields.schedule');
|
||||||
|
const launchType = resource.model.get('launch_type');
|
||||||
|
|
||||||
if (!createdBy && !schedule) {
|
if (!createdBy && !schedule && !launchType) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +266,15 @@ function getLaunchedByDetails () {
|
|||||||
let tooltip;
|
let tooltip;
|
||||||
let value;
|
let value;
|
||||||
|
|
||||||
if (createdBy) {
|
if (launchType === 'webhook' && jobTemplate) {
|
||||||
|
tooltip = strings.get('tooltips.WEBHOOK_JOB_TEMPLATE');
|
||||||
|
link = `/#/templates/job_template/${jobTemplate.id}`;
|
||||||
|
value = strings.get('details.WEBHOOK');
|
||||||
|
} else if (launchType === 'webhook' && workflowJobTemplate) {
|
||||||
|
tooltip = strings.get('tooltips.WEBHOOK_WORKFLOW_JOB_TEMPLATE');
|
||||||
|
link = `/#/templates/workflow_job_template/${workflowJobTemplate.id}`;
|
||||||
|
value = strings.get('details.WEBHOOK');
|
||||||
|
} else if (createdBy) {
|
||||||
tooltip = strings.get('tooltips.USER');
|
tooltip = strings.get('tooltips.USER');
|
||||||
link = `/#/users/${createdBy.id}`;
|
link = `/#/users/${createdBy.id}`;
|
||||||
value = $filter('sanitize')(createdBy.username);
|
value = $filter('sanitize')(createdBy.username);
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ function OutputStrings (BaseString) {
|
|||||||
MENU_UP: t.s('Get previous page'),
|
MENU_UP: t.s('Get previous page'),
|
||||||
MENU_LAST: t.s('Go to last page of available output'),
|
MENU_LAST: t.s('Go to last page of available output'),
|
||||||
MENU_FOLLOWING: t.s('Currently following output as it arrives. Click to unfollow'),
|
MENU_FOLLOWING: t.s('Currently following output as it arrives. Click to unfollow'),
|
||||||
|
WEBHOOK_JOB_TEMPLATE: t.s('View the webhook configuration on the job template.'),
|
||||||
|
WEBHOOK_WORKFLOW_JOB_TEMPLATE: t.s('View the webhook configuration on the workflow job template.'),
|
||||||
};
|
};
|
||||||
|
|
||||||
ns.details = {
|
ns.details = {
|
||||||
@@ -48,6 +50,7 @@ function OutputStrings (BaseString) {
|
|||||||
SHOW_LESS: t.s('Show Less'),
|
SHOW_LESS: t.s('Show Less'),
|
||||||
SHOW_MORE: t.s('Show More'),
|
SHOW_MORE: t.s('Show More'),
|
||||||
UNKNOWN: t.s('Finished'),
|
UNKNOWN: t.s('Finished'),
|
||||||
|
WEBHOOK: t.s('Webhook'),
|
||||||
};
|
};
|
||||||
|
|
||||||
ns.labels = {
|
ns.labels = {
|
||||||
|
|||||||
@@ -288,6 +288,8 @@
|
|||||||
line-height: @at-line-height-list-row-item-tag;
|
line-height: @at-line-height-list-row-item-tag;
|
||||||
word-break: keep-all;
|
word-break: keep-all;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.at-RowItem-tag--primary {
|
.at-RowItem-tag--primary {
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
@import '../../src/workflow-results/standard-out.block.less';
|
@import '../../src/workflow-results/standard-out.block.less';
|
||||||
@import '../../src/templates/prompt/prompt.block.less';
|
@import '../../src/templates/prompt/prompt.block.less';
|
||||||
@import '../../src/templates/job_templates/multi-credential/multi-credential.block.less';
|
@import '../../src/templates/job_templates/multi-credential/multi-credential.block.less';
|
||||||
|
@import '../../src/templates/job_templates/webhook-credential/webhook-credential.block.less';
|
||||||
@import '../../src/templates/labels/labelsList.block.less';
|
@import '../../src/templates/labels/labelsList.block.less';
|
||||||
@import '../../src/templates/survey-maker/survey-maker.block.less';
|
@import '../../src/templates/survey-maker/survey-maker.block.less';
|
||||||
@import '../../src/templates/survey-maker/survey-maker.block.less';
|
@import '../../src/templates/survey-maker/survey-maker.block.less';
|
||||||
|
|||||||
@@ -794,9 +794,21 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (field.genHash) {
|
if (field.genHash) {
|
||||||
html += "<span class=\"input-group-btn input-group-prepend\"><button type=\"button\" class=\"btn Form-lookupButton\" ng-click=\"genHash('" + fld + "')\" " +
|
const defaultGenHashButtonTemplate = `
|
||||||
"aw-tool-tip=\"Generate " + field.label + "\" data-placement=\"top\" id=\"" + this.form.name + "_" + fld + "_gen_btn\">" +
|
<span class="input-group-btn input-group-prepend">
|
||||||
"<i class=\"fa fa-magic\"></i></button></span>\n</div>\n";
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn Form-lookupButton"
|
||||||
|
ng-click="genHash('${fld}')"
|
||||||
|
aw-tool-tip="Generate ${field.label}"
|
||||||
|
data-placement="top"
|
||||||
|
id="${this.form.name}_${fld}_gen_btn"
|
||||||
|
>
|
||||||
|
<i class="fa fa-refresh" />
|
||||||
|
</button>
|
||||||
|
</span>`;
|
||||||
|
const genHashButtonTemplate = _.get(field, 'genHashButtonTemplate', defaultGenHashButtonTemplate);
|
||||||
|
html += `${genHashButtonTemplate}\n</div>\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add error messages
|
// Add error messages
|
||||||
|
|||||||
@@ -10,14 +10,14 @@
|
|||||||
'ProcessErrors', 'GetBasePath', 'hashSetup', 'ParseTypeChange', 'Wait',
|
'ProcessErrors', 'GetBasePath', 'hashSetup', 'ParseTypeChange', 'Wait',
|
||||||
'Empty', 'ToJSON', 'CallbackHelpInit', 'GetChoices', '$state', 'availableLabels',
|
'Empty', 'ToJSON', 'CallbackHelpInit', 'GetChoices', '$state', 'availableLabels',
|
||||||
'CreateSelect2', '$q', 'i18n', 'Inventory', 'Project', 'InstanceGroupsService',
|
'CreateSelect2', '$q', 'i18n', 'Inventory', 'Project', 'InstanceGroupsService',
|
||||||
'MultiCredentialService', 'ConfigData', 'resolvedModels',
|
'MultiCredentialService', 'ConfigData', 'resolvedModels', '$compile',
|
||||||
function(
|
function(
|
||||||
$filter, $scope,
|
$filter, $scope,
|
||||||
$stateParams, JobTemplateForm, GenerateForm, Rest, Alert,
|
$stateParams, JobTemplateForm, GenerateForm, Rest, Alert,
|
||||||
ProcessErrors, GetBasePath, hashSetup, ParseTypeChange, Wait,
|
ProcessErrors, GetBasePath, hashSetup, ParseTypeChange, Wait,
|
||||||
Empty, ToJSON, CallbackHelpInit, GetChoices,
|
Empty, ToJSON, CallbackHelpInit, GetChoices,
|
||||||
$state, availableLabels, CreateSelect2, $q, i18n, Inventory, Project, InstanceGroupsService,
|
$state, availableLabels, CreateSelect2, $q, i18n, Inventory, Project, InstanceGroupsService,
|
||||||
MultiCredentialService, ConfigData, resolvedModels
|
MultiCredentialService, ConfigData, resolvedModels, $compile
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// Inject dynamic view
|
// Inject dynamic view
|
||||||
@@ -39,10 +39,121 @@
|
|||||||
$scope.can_edit = true;
|
$scope.can_edit = true;
|
||||||
$scope.allow_callbacks = false;
|
$scope.allow_callbacks = false;
|
||||||
$scope.playbook_options = [];
|
$scope.playbook_options = [];
|
||||||
|
$scope.webhook_service_options = [];
|
||||||
$scope.mode = "add";
|
$scope.mode = "add";
|
||||||
$scope.parseType = 'yaml';
|
$scope.parseType = 'yaml';
|
||||||
$scope.credentialNotPresent = false;
|
$scope.credentialNotPresent = false;
|
||||||
$scope.canGetAllRelatedResources = true;
|
$scope.canGetAllRelatedResources = true;
|
||||||
|
$scope.webhook_key_help = i18n._('Webhook services can use this as a shared secret.');
|
||||||
|
|
||||||
|
//
|
||||||
|
// webhook credential - all handlers, dynamic state, etc. live here
|
||||||
|
//
|
||||||
|
|
||||||
|
$scope.webhookCredential = {
|
||||||
|
id: null,
|
||||||
|
name: null,
|
||||||
|
isModalOpen: false,
|
||||||
|
isModalReady: false,
|
||||||
|
modalTitle: i18n._('Select Webhook Credential'),
|
||||||
|
modalBaseParams: {
|
||||||
|
order_by: 'name',
|
||||||
|
page_size: 5,
|
||||||
|
credential_type__namespace: null,
|
||||||
|
},
|
||||||
|
modalSelectedId: null,
|
||||||
|
modalSelectedName: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialLookupClick = () => {
|
||||||
|
$scope.webhookCredential.modalSelectedId = $scope.webhookCredential.id;
|
||||||
|
$scope.webhookCredential.isModalOpen = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialTagDelete = () => {
|
||||||
|
$scope.webhookCredential.id = null;
|
||||||
|
$scope.webhookCredential.name = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialModalClose = () => {
|
||||||
|
$scope.webhookCredential.isModalOpen = false;
|
||||||
|
$scope.webhookCredential.isModalReady = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialModalReady = () => {
|
||||||
|
$scope.webhookCredential.isModalReady = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialModalItemSelect = (item) => {
|
||||||
|
$scope.webhookCredential.modalSelectedId = item.id;
|
||||||
|
$scope.webhookCredential.modalSelectedName = item.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialModalCancel = () => {
|
||||||
|
$scope.webhookCredential.isModalOpen = false;
|
||||||
|
$scope.webhookCredential.isModalReady = false;
|
||||||
|
$scope.webhookCredential.modalSelectedId = null;
|
||||||
|
$scope.webhookCredential.modalSelectedName = null;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialSelect = () => {
|
||||||
|
$scope.webhookCredential.isModalOpen = false;
|
||||||
|
$scope.webhookCredential.isModalReady = false;
|
||||||
|
$scope.webhookCredential.id = $scope.webhookCredential.modalSelectedId;
|
||||||
|
$scope.webhookCredential.name = $scope.webhookCredential.modalSelectedName;
|
||||||
|
$scope.webhookCredential.modalSelectedId = null;
|
||||||
|
$scope.webhookCredential.modalSelectedName = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookKeyButtonClick = () => {};
|
||||||
|
|
||||||
|
$('#content-container').append($compile(`
|
||||||
|
<at-dialog
|
||||||
|
title="webhookCredential.modalTitle"
|
||||||
|
on-close="handleWebhookCredentialModalClose"
|
||||||
|
ng-if="webhookCredential.isModalOpen"
|
||||||
|
ng-show="webhookCredential.isModalOpen && webhookCredential.isModalReady"
|
||||||
|
>
|
||||||
|
<at-lookup-list
|
||||||
|
ng-show="webhookCredential.isModalOpen && webhookCredential.isModalReady"
|
||||||
|
resource-name="credential"
|
||||||
|
base-params="webhookCredential.modalBaseParams"
|
||||||
|
selected-id="webhookCredential.modalSelectedId"
|
||||||
|
on-ready="handleWebhookCredentialModalReady"
|
||||||
|
on-item-select="handleWebhookCredentialModalItemSelect"
|
||||||
|
/>
|
||||||
|
<at-action-group col="12" pos="right">
|
||||||
|
<at-action-button
|
||||||
|
variant="tertiary"
|
||||||
|
ng-click="handleWebhookCredentialModalCancel()"
|
||||||
|
>
|
||||||
|
${i18n._('CANCEL')}
|
||||||
|
</at-action-button>
|
||||||
|
<at-action-button
|
||||||
|
variant="primary"
|
||||||
|
ng-click="handleWebhookCredentialSelect()"
|
||||||
|
>
|
||||||
|
${i18n._('SELECT')}
|
||||||
|
</at-action-button>
|
||||||
|
</at-action-group>
|
||||||
|
</at-dialog>`)($scope));
|
||||||
|
|
||||||
|
$scope.$watch('webhook_service', (newValue, oldValue) => {
|
||||||
|
const newServiceValue = newValue && typeof newValue === 'object' ? newValue.value : newValue;
|
||||||
|
const oldServiceValue = oldValue && typeof oldValue === 'object' ? oldValue.value : oldValue;
|
||||||
|
if (newServiceValue !== oldServiceValue || newServiceValue === newValue) {
|
||||||
|
$scope.webhook_service = { value: newServiceValue };
|
||||||
|
sync_webhook_service_select2();
|
||||||
|
$scope.webhookCredential.modalBaseParams.credential_type__namespace = newServiceValue ?
|
||||||
|
`${newServiceValue}_token`
|
||||||
|
: null;
|
||||||
|
if (newServiceValue !== newValue || newValue === null) {
|
||||||
|
$scope.webhookCredential.id = null;
|
||||||
|
$scope.webhookCredential.name = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
hashSetup({
|
hashSetup({
|
||||||
scope: $scope,
|
scope: $scope,
|
||||||
@@ -51,6 +162,9 @@
|
|||||||
default_val: false
|
default_val: false
|
||||||
});
|
});
|
||||||
CallbackHelpInit({ scope: $scope });
|
CallbackHelpInit({ scope: $scope });
|
||||||
|
// set initial vals for webhook checkbox
|
||||||
|
$scope.enable_webhook = false;
|
||||||
|
master.enable_webhook = false;
|
||||||
|
|
||||||
$scope.surveyTooltip = i18n._('Please save before adding a survey to this job template.');
|
$scope.surveyTooltip = i18n._('Please save before adding a survey to this job template.');
|
||||||
|
|
||||||
@@ -131,6 +245,14 @@
|
|||||||
multiple: false,
|
multiple: false,
|
||||||
opts: $scope.custom_virtualenvs_options
|
opts: $scope.custom_virtualenvs_options
|
||||||
});
|
});
|
||||||
|
CreateSelect2({
|
||||||
|
element:'#webhook-service-select',
|
||||||
|
addNew: false,
|
||||||
|
multiple: false,
|
||||||
|
scope: $scope,
|
||||||
|
options: 'webhook_service_options',
|
||||||
|
model: 'webhook_service'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -151,7 +273,13 @@
|
|||||||
variable: 'job_type_options',
|
variable: 'job_type_options',
|
||||||
callback: 'choicesReadyVerbosity'
|
callback: 'choicesReadyVerbosity'
|
||||||
});
|
});
|
||||||
|
GetChoices({
|
||||||
|
scope: $scope,
|
||||||
|
url: defaultUrl,
|
||||||
|
field: 'webhook_service',
|
||||||
|
variable: 'webhook_service_options',
|
||||||
|
callback: 'choicesReadyVerbosity'
|
||||||
|
});
|
||||||
$scope.labelOptions = availableLabels
|
$scope.labelOptions = availableLabels
|
||||||
.map((i) => ({label: i.name, value: i.id}));
|
.map((i) => ({label: i.name, value: i.id}));
|
||||||
$scope.$emit("choicesReadyVerbosity");
|
$scope.$emit("choicesReadyVerbosity");
|
||||||
@@ -167,6 +295,17 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sync_webhook_service_select2() {
|
||||||
|
CreateSelect2({
|
||||||
|
element:'#webhook-service-select',
|
||||||
|
addNew: false,
|
||||||
|
multiple: false,
|
||||||
|
scope: $scope,
|
||||||
|
options: 'webhook_service_options',
|
||||||
|
model: 'webhook_service'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$scope.toggleForm = function(key) {
|
$scope.toggleForm = function(key) {
|
||||||
$scope[key] = !$scope[key];
|
$scope[key] = !$scope[key];
|
||||||
};
|
};
|
||||||
@@ -328,6 +467,9 @@
|
|||||||
// be provided to the related credentials endpoint by the template save success handler.
|
// be provided to the related credentials endpoint by the template save success handler.
|
||||||
delete data.credential;
|
delete data.credential;
|
||||||
delete data.vault_credential;
|
delete data.vault_credential;
|
||||||
|
delete data.webhook_url;
|
||||||
|
delete data.webhook_key;
|
||||||
|
data.webhook_credential = $scope.webhookCredential.id;
|
||||||
|
|
||||||
data.extra_vars = ToJSON($scope.parseType, $scope.extra_vars, true);
|
data.extra_vars = ToJSON($scope.parseType, $scope.extra_vars, true);
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default
|
|||||||
'initSurvey', '$state', 'CreateSelect2', 'isNotificationAdmin',
|
'initSurvey', '$state', 'CreateSelect2', 'isNotificationAdmin',
|
||||||
'ToggleNotification','$q', 'InstanceGroupsService', 'InstanceGroupsData',
|
'ToggleNotification','$q', 'InstanceGroupsService', 'InstanceGroupsData',
|
||||||
'MultiCredentialService', 'availableLabels', 'projectGetPermissionDenied',
|
'MultiCredentialService', 'availableLabels', 'projectGetPermissionDenied',
|
||||||
'inventoryGetPermissionDenied', 'jobTemplateData', 'ParseVariableString', 'ConfigData',
|
'inventoryGetPermissionDenied', 'jobTemplateData', 'ParseVariableString', 'ConfigData', '$compile', 'webhookKey',
|
||||||
function(
|
function(
|
||||||
$filter, $scope,
|
$filter, $scope,
|
||||||
$stateParams, JobTemplateForm, GenerateForm, Rest, Alert,
|
$stateParams, JobTemplateForm, GenerateForm, Rest, Alert,
|
||||||
@@ -29,7 +29,7 @@ export default
|
|||||||
SurveyControllerInit, $state, CreateSelect2, isNotificationAdmin,
|
SurveyControllerInit, $state, CreateSelect2, isNotificationAdmin,
|
||||||
ToggleNotification, $q, InstanceGroupsService, InstanceGroupsData,
|
ToggleNotification, $q, InstanceGroupsService, InstanceGroupsData,
|
||||||
MultiCredentialService, availableLabels, projectGetPermissionDenied,
|
MultiCredentialService, availableLabels, projectGetPermissionDenied,
|
||||||
inventoryGetPermissionDenied, jobTemplateData, ParseVariableString, ConfigData
|
inventoryGetPermissionDenied, jobTemplateData, ParseVariableString, ConfigData, $compile, webhookKey
|
||||||
) {
|
) {
|
||||||
|
|
||||||
$scope.$watch('job_template_obj.summary_fields.user_capabilities.edit', function(val) {
|
$scope.$watch('job_template_obj.summary_fields.user_capabilities.edit', function(val) {
|
||||||
@@ -61,7 +61,10 @@ export default
|
|||||||
$scope.sufficientRoleForNotifToggle = isNotificationAdmin;
|
$scope.sufficientRoleForNotifToggle = isNotificationAdmin;
|
||||||
$scope.sufficientRoleForNotif = isNotificationAdmin || $scope.user_is_system_auditor;
|
$scope.sufficientRoleForNotif = isNotificationAdmin || $scope.user_is_system_auditor;
|
||||||
$scope.playbook_options = null;
|
$scope.playbook_options = null;
|
||||||
|
$scope.webhook_service_options = null;
|
||||||
$scope.playbook = null;
|
$scope.playbook = null;
|
||||||
|
$scope.webhook_service = jobTemplateData.webhook_service;
|
||||||
|
$scope.webhook_url = '';
|
||||||
$scope.mode = 'edit';
|
$scope.mode = 'edit';
|
||||||
$scope.parseType = 'yaml';
|
$scope.parseType = 'yaml';
|
||||||
$scope.showJobType = false;
|
$scope.showJobType = false;
|
||||||
@@ -72,6 +75,148 @@ export default
|
|||||||
$scope.skip_tag_options = [];
|
$scope.skip_tag_options = [];
|
||||||
const virtualEnvs = ConfigData.custom_virtualenvs || [];
|
const virtualEnvs = ConfigData.custom_virtualenvs || [];
|
||||||
$scope.custom_virtualenvs_options = virtualEnvs;
|
$scope.custom_virtualenvs_options = virtualEnvs;
|
||||||
|
$scope.webhook_url_help = i18n._('Webhook services can launch jobs with this job template by making a POST request to this URL.');
|
||||||
|
$scope.webhook_key_help = i18n._('Webhook services can use this as a shared secret.');
|
||||||
|
|
||||||
|
$scope.currentlySavedWebhookKey = webhookKey;
|
||||||
|
$scope.webhook_key = webhookKey;
|
||||||
|
|
||||||
|
//
|
||||||
|
// webhook credential - all handlers, dynamic state, etc. live here
|
||||||
|
//
|
||||||
|
|
||||||
|
$scope.webhookCredential = {
|
||||||
|
id: _.get(jobTemplateData, ['summary_fields', 'webhook_credential', 'id']),
|
||||||
|
name: _.get(jobTemplateData, ['summary_fields', 'webhook_credential', 'name']),
|
||||||
|
isModalOpen: false,
|
||||||
|
isModalReady: false,
|
||||||
|
modalSelectedId: null,
|
||||||
|
modalSelectedName: null,
|
||||||
|
modalBaseParams: {
|
||||||
|
order_by: 'name',
|
||||||
|
page_size: 5,
|
||||||
|
credential_type__namespace: `${jobTemplateData.webhook_service}_token`,
|
||||||
|
},
|
||||||
|
modalTitle: i18n._('Select Webhook Credential'),
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialLookupClick = () => {
|
||||||
|
$scope.webhookCredential.modalSelectedId = $scope.webhookCredential.id;
|
||||||
|
$scope.webhookCredential.isModalOpen = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialTagDelete = () => {
|
||||||
|
$scope.webhookCredential.id = null;
|
||||||
|
$scope.webhookCredential.name = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialModalClose = () => {
|
||||||
|
$scope.webhookCredential.isModalOpen = false;
|
||||||
|
$scope.webhookCredential.isModalReady = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialModalReady = () => {
|
||||||
|
$scope.webhookCredential.isModalReady = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialModalItemSelect = (item) => {
|
||||||
|
$scope.webhookCredential.modalSelectedId = item.id;
|
||||||
|
$scope.webhookCredential.modalSelectedName = item.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialModalCancel = () => {
|
||||||
|
$scope.webhookCredential.isModalOpen = false;
|
||||||
|
$scope.webhookCredential.isModalReady = false;
|
||||||
|
$scope.webhookCredential.modalSelectedId = null;
|
||||||
|
$scope.webhookCredential.modalSelectedName = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialSelect = () => {
|
||||||
|
$scope.webhookCredential.isModalOpen = false;
|
||||||
|
$scope.webhookCredential.isModalReady = false;
|
||||||
|
$scope.webhookCredential.id = $scope.webhookCredential.modalSelectedId;
|
||||||
|
$scope.webhookCredential.name = $scope.webhookCredential.modalSelectedName;
|
||||||
|
$scope.webhookCredential.modalSelectedId = null;
|
||||||
|
$scope.webhookCredential.modalSelectedName = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookKeyButtonClick = () => {
|
||||||
|
Rest.setUrl(jobTemplateData.related.webhook_key);
|
||||||
|
Wait('start');
|
||||||
|
Rest.post({})
|
||||||
|
.then(({ data }) => {
|
||||||
|
$scope.currentlySavedWebhookKey = data.webhook_key;
|
||||||
|
$scope.webhook_key = data.webhook_key;
|
||||||
|
})
|
||||||
|
.catch(({ data }) => {
|
||||||
|
const errorMsg = `Failed to generate new webhook key. POST returned status: ${status}`;
|
||||||
|
ProcessErrors($scope, data, status, form, { hdr: 'Error!', msg: errorMsg });
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
Wait('stop');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$('#content-container').append($compile(`
|
||||||
|
<at-dialog
|
||||||
|
title="webhookCredential.modalTitle"
|
||||||
|
on-close="handleWebhookCredentialModalClose"
|
||||||
|
ng-if="webhookCredential.isModalOpen"
|
||||||
|
ng-show="webhookCredential.isModalOpen && webhookCredential.isModalReady"
|
||||||
|
>
|
||||||
|
<at-lookup-list
|
||||||
|
ng-show="webhookCredential.isModalOpen && webhookCredential.isModalReady"
|
||||||
|
resource-name="credential"
|
||||||
|
base-params="webhookCredential.modalBaseParams"
|
||||||
|
selected-id="webhookCredential.modalSelectedId"
|
||||||
|
on-ready="handleWebhookCredentialModalReady"
|
||||||
|
on-item-select="handleWebhookCredentialModalItemSelect"
|
||||||
|
/>
|
||||||
|
<at-action-group col="12" pos="right">
|
||||||
|
<at-action-button
|
||||||
|
variant="tertiary"
|
||||||
|
ng-click="handleWebhookCredentialModalCancel()"
|
||||||
|
>
|
||||||
|
${i18n._('CANCEL')}
|
||||||
|
</at-action-button>
|
||||||
|
<at-action-button
|
||||||
|
variant="primary"
|
||||||
|
ng-click="handleWebhookCredentialSelect()"
|
||||||
|
>
|
||||||
|
${i18n._('SELECT')}
|
||||||
|
</at-action-button>
|
||||||
|
</at-action-group>
|
||||||
|
</at-dialog>`)($scope));
|
||||||
|
|
||||||
|
$scope.$watch('webhook_service', (newValue, oldValue) => {
|
||||||
|
const newServiceValue = newValue && typeof newValue === 'object' ? newValue.value : newValue;
|
||||||
|
const oldServiceValue = oldValue && typeof oldValue === 'object' ? oldValue.value : oldValue;
|
||||||
|
if (newServiceValue) {
|
||||||
|
$scope.webhook_url = `${$scope.callback_server_path}${jobTemplateData.url}${newServiceValue}/`;
|
||||||
|
} else {
|
||||||
|
$scope.webhook_url = '';
|
||||||
|
$scope.webhook_key = '';
|
||||||
|
}
|
||||||
|
if (newServiceValue !== oldServiceValue || newServiceValue === newValue) {
|
||||||
|
$scope.webhook_service = { value: newServiceValue };
|
||||||
|
sync_webhook_service_select2();
|
||||||
|
$scope.webhookCredential.modalBaseParams.credential_type__namespace = newServiceValue ?
|
||||||
|
`${newServiceValue}_token` : null;
|
||||||
|
if (newServiceValue !== newValue || newValue === null) {
|
||||||
|
$scope.webhookCredential.id = null;
|
||||||
|
$scope.webhookCredential.name = null;
|
||||||
|
}
|
||||||
|
if (newServiceValue !== newValue) {
|
||||||
|
if (newServiceValue === jobTemplateData.webhook_service) {
|
||||||
|
$scope.webhook_key = $scope.currentlySavedWebhookKey;
|
||||||
|
} else {
|
||||||
|
$scope.webhook_key = i18n._('A NEW WEBHOOK KEY WILL BE GENERATED ON SAVE');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$watch('verbosity', sync_verbosity_select2);
|
||||||
|
|
||||||
SurveyControllerInit({
|
SurveyControllerInit({
|
||||||
scope: $scope,
|
scope: $scope,
|
||||||
@@ -174,9 +319,6 @@ export default
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// watch for changes to 'verbosity', ensure we keep our select2 in sync when it changes.
|
|
||||||
$scope.$watch('verbosity', sync_verbosity_select2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
callback = function() {
|
callback = function() {
|
||||||
@@ -202,6 +344,17 @@ export default
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sync_webhook_service_select2() {
|
||||||
|
select2LoadDefer.push(CreateSelect2({
|
||||||
|
element:'#webhook-service-select',
|
||||||
|
addNew: false,
|
||||||
|
multiple: false,
|
||||||
|
scope: $scope,
|
||||||
|
options: 'webhook_service_options',
|
||||||
|
model: 'webhook_service'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
function jobTemplateLoadFinished(){
|
function jobTemplateLoadFinished(){
|
||||||
select2LoadDefer.push(CreateSelect2({
|
select2LoadDefer.push(CreateSelect2({
|
||||||
element:'#job_template_job_type',
|
element:'#job_template_job_type',
|
||||||
@@ -225,6 +378,14 @@ export default
|
|||||||
multiple: false,
|
multiple: false,
|
||||||
opts: $scope.custom_virtualenvs_options
|
opts: $scope.custom_virtualenvs_options
|
||||||
}));
|
}));
|
||||||
|
select2LoadDefer.push(CreateSelect2({
|
||||||
|
element:'#webhook-service-select',
|
||||||
|
addNew: false,
|
||||||
|
multiple: false,
|
||||||
|
scope: $scope,
|
||||||
|
options: 'webhook_service_options',
|
||||||
|
model: 'webhook_service'
|
||||||
|
}));
|
||||||
|
|
||||||
if (!launchHasBeenEnabled) {
|
if (!launchHasBeenEnabled) {
|
||||||
$q.all(select2LoadDefer).then(() => {
|
$q.all(select2LoadDefer).then(() => {
|
||||||
@@ -296,6 +457,15 @@ export default
|
|||||||
default_val: dft
|
default_val: dft
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// set initial vals for webhook checkbox
|
||||||
|
if (jobTemplateData.webhook_service) {
|
||||||
|
$scope.enable_webhook = true;
|
||||||
|
master.enable_webhook = true;
|
||||||
|
} else {
|
||||||
|
$scope.enable_webhook = false;
|
||||||
|
master.enable_webhook = false;
|
||||||
|
}
|
||||||
|
|
||||||
ParseTypeChange({
|
ParseTypeChange({
|
||||||
scope: $scope,
|
scope: $scope,
|
||||||
field_id: 'extra_vars',
|
field_id: 'extra_vars',
|
||||||
@@ -498,6 +668,14 @@ export default
|
|||||||
callback: 'choicesReady'
|
callback: 'choicesReady'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
GetChoices({
|
||||||
|
scope: $scope,
|
||||||
|
url: defaultUrl,
|
||||||
|
field: 'webhook_service',
|
||||||
|
variable: 'webhook_service_options',
|
||||||
|
callback: 'choicesReady'
|
||||||
|
});
|
||||||
|
|
||||||
$scope.labelOptions = availableLabels
|
$scope.labelOptions = availableLabels
|
||||||
.map((i) => ({label: i.name, value: i.id}));
|
.map((i) => ({label: i.name, value: i.id}));
|
||||||
|
|
||||||
@@ -553,7 +731,6 @@ export default
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
var orgDefer = $q.defer();
|
var orgDefer = $q.defer();
|
||||||
var associationDefer = $q.defer();
|
var associationDefer = $q.defer();
|
||||||
var associatedLabelsDefer = $q.defer();
|
var associatedLabelsDefer = $q.defer();
|
||||||
@@ -729,6 +906,20 @@ export default
|
|||||||
data.job_tags = (Array.isArray($scope.job_tags)) ? _.uniq($scope.job_tags).join() : "";
|
data.job_tags = (Array.isArray($scope.job_tags)) ? _.uniq($scope.job_tags).join() : "";
|
||||||
data.skip_tags = (Array.isArray($scope.skip_tags)) ? _.uniq($scope.skip_tags).join() : "";
|
data.skip_tags = (Array.isArray($scope.skip_tags)) ? _.uniq($scope.skip_tags).join() : "";
|
||||||
|
|
||||||
|
delete data.webhook_url;
|
||||||
|
delete data.webhook_key;
|
||||||
|
delete data.enable_webhook;
|
||||||
|
data.webhook_credential = $scope.webhookCredential.id;
|
||||||
|
|
||||||
|
if (!data.webhook_service) {
|
||||||
|
data.webhook_credential = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$scope.enable_webhook) {
|
||||||
|
data.webhook_service = '';
|
||||||
|
data.webhook_credential = null;
|
||||||
|
}
|
||||||
|
|
||||||
Rest.setUrl(defaultUrl + $state.params.job_template_id);
|
Rest.setUrl(defaultUrl + $state.params.job_template_id);
|
||||||
Rest.patch(data)
|
Rest.patch(data)
|
||||||
.then(({data}) => {
|
.then(({data}) => {
|
||||||
|
|||||||
@@ -338,6 +338,16 @@ function(NotificationsList, i18n) {
|
|||||||
dataTitle: i18n._('Allow Provisioning Callbacks'),
|
dataTitle: i18n._('Allow Provisioning Callbacks'),
|
||||||
dataContainer: "body",
|
dataContainer: "body",
|
||||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||||
|
}, {
|
||||||
|
name: 'enable_webhook',
|
||||||
|
label: i18n._('Enable Webhook'),
|
||||||
|
type: 'checkbox',
|
||||||
|
column: 2,
|
||||||
|
awPopOver: "<p>" + i18n._("Enable webhook for this job template.") + "</p>",
|
||||||
|
dataPlacement: 'right',
|
||||||
|
dataTitle: i18n._('Enable Webhook'),
|
||||||
|
dataContainer: "body",
|
||||||
|
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
||||||
}, {
|
}, {
|
||||||
name: 'allow_simultaneous',
|
name: 'allow_simultaneous',
|
||||||
label: i18n._('Enable Concurrent Jobs'),
|
label: i18n._('Enable Concurrent Jobs'),
|
||||||
@@ -391,6 +401,80 @@ function(NotificationsList, i18n) {
|
|||||||
alwaysShowAsterisk: true
|
alwaysShowAsterisk: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
webhook_service: {
|
||||||
|
label: i18n._('Webhook Service'),
|
||||||
|
type:'select',
|
||||||
|
defaultText: i18n._('Choose a Webhook Service'),
|
||||||
|
ngOptions: 'svc.label for svc in webhook_service_options track by svc.value',
|
||||||
|
ngShow: "enable_webhook && enable_webhook !== 'false'",
|
||||||
|
ngDisabled: "!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate) || !canGetAllRelatedResources",
|
||||||
|
id: 'webhook-service-select',
|
||||||
|
column: 1,
|
||||||
|
awPopOver: "<p>" + i18n._("Select a webhook service.") + "</p>",
|
||||||
|
dataTitle: i18n._('Webhook Service'),
|
||||||
|
dataPlacement: 'right',
|
||||||
|
dataContainer: "body",
|
||||||
|
},
|
||||||
|
webhook_url: {
|
||||||
|
label: i18n._('Webhook URL'),
|
||||||
|
type: 'text',
|
||||||
|
ngShow: "job_template_obj && enable_webhook && enable_webhook !== 'false'",
|
||||||
|
awPopOver: "webhook_url_help",
|
||||||
|
awPopOverWatch: "webhook_url_help",
|
||||||
|
dataPlacement: 'top',
|
||||||
|
dataTitle: i18n._('Webhook URL'),
|
||||||
|
dataContainer: "body",
|
||||||
|
readonly: true
|
||||||
|
},
|
||||||
|
webhook_key: {
|
||||||
|
label: i18n._('Webhook Key'),
|
||||||
|
type: 'text',
|
||||||
|
ngShow: "enable_webhook && enable_webhook !== 'false'",
|
||||||
|
genHash: true,
|
||||||
|
genHashButtonTemplate: `
|
||||||
|
<span
|
||||||
|
ng-if="job_template_obj && webhook_service.value && currentlySavedWebhookKey === webhook_key"
|
||||||
|
class="input-group-btn input-group-prepend"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn Form-lookupButton"
|
||||||
|
ng-click="handleWebhookKeyButtonClick()"
|
||||||
|
aw-tool-tip="${i18n._('Rotate Webhook Key')}"
|
||||||
|
data-placement="top"
|
||||||
|
id="job_template_webhook_key_gen_btn"
|
||||||
|
>
|
||||||
|
<i class="fa fa-refresh" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
`,
|
||||||
|
genHashButtonClickHandlerName: "handleWebhookKeyButtonClick",
|
||||||
|
awPopOver: "webhook_key_help",
|
||||||
|
awPopOverWatch: "webhook_key_help",
|
||||||
|
dataPlacement: 'right',
|
||||||
|
dataTitle: i18n._("Webhook Key"),
|
||||||
|
dataContainer: "body",
|
||||||
|
readonly: true,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
webhook_credential: {
|
||||||
|
label: i18n._('Webhook Credential'),
|
||||||
|
type: 'custom',
|
||||||
|
ngShow: "enable_webhook && enable_webhook !== 'false'",
|
||||||
|
control: `
|
||||||
|
<webhook-credential-input
|
||||||
|
is-field-disabled="!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate) || !(webhookCredential.modalBaseParams.credential_type__namespace)"
|
||||||
|
tag-name="webhookCredential.name"
|
||||||
|
on-lookup-click="handleWebhookCredentialLookupClick"
|
||||||
|
on-tag-delete="handleWebhookCredentialTagDelete"
|
||||||
|
</webhook-credential-input>`,
|
||||||
|
awPopOver: "<p>" + i18n._("Optionally, select the credential to use to send status updates back to the webhook service") + "</p>",
|
||||||
|
dataTitle: i18n._('Webhook Credential'),
|
||||||
|
dataPlacement: 'right',
|
||||||
|
dataContainer: "body",
|
||||||
|
ngDisabled: '!(webhook_service || webhook_service.value)',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
extra_vars: {
|
extra_vars: {
|
||||||
label: i18n._('Extra Variables'),
|
label: i18n._('Extra Variables'),
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import jobTemplateAdd from './add-job-template/main';
|
import jobTemplateAdd from './add-job-template/main';
|
||||||
import jobTemplateEdit from './edit-job-template/main';
|
import jobTemplateEdit from './edit-job-template/main';
|
||||||
import multiCredential from './multi-credential/main';
|
import multiCredential from './multi-credential/main';
|
||||||
|
import webhookCredential from './webhook-credential';
|
||||||
import hashSetup from './factories/hash-setup.factory';
|
import hashSetup from './factories/hash-setup.factory';
|
||||||
import CallbackHelpInit from './factories/callback-help-init.factory';
|
import CallbackHelpInit from './factories/callback-help-init.factory';
|
||||||
import JobTemplateForm from './job-template.form';
|
import JobTemplateForm from './job-template.form';
|
||||||
|
|
||||||
export default
|
export default
|
||||||
angular.module('jobTemplates', [jobTemplateAdd.name, jobTemplateEdit.name,
|
angular.module('jobTemplates', [jobTemplateAdd.name, jobTemplateEdit.name, multiCredential.name, webhookCredential.name])
|
||||||
multiCredential.name])
|
.factory('hashSetup', hashSetup)
|
||||||
.factory('hashSetup', hashSetup)
|
.factory('CallbackHelpInit', CallbackHelpInit)
|
||||||
.factory('CallbackHelpInit', CallbackHelpInit)
|
.factory('JobTemplateForm', JobTemplateForm);
|
||||||
.factory('JobTemplateForm', JobTemplateForm);
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import webhookCredentialInput from './webhook-credential-input.component';
|
||||||
|
|
||||||
|
export default angular.module('webhookCredential', [])
|
||||||
|
.component('webhookCredentialInput', webhookCredentialInput);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import webhookCredential from './webhook-credential.directive';
|
||||||
|
import webhookCredentialModal from './webhook-credential-modal.directive';
|
||||||
|
import webhookCredentialService from './webhook-credential.service';
|
||||||
|
|
||||||
|
export default
|
||||||
|
angular.module('webhookCredential', [])
|
||||||
|
.directive('webhookCredential', webhookCredential)
|
||||||
|
.directive('webhookCredentialModal', webhookCredentialModal)
|
||||||
|
.service('WebhookCredentialService', webhookCredentialService);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
const templateUrl = require('~src/templates/job_templates/webhook-credential/webhook-credential-input.partial.html');
|
||||||
|
export default {
|
||||||
|
templateUrl,
|
||||||
|
controllerAs: 'vm',
|
||||||
|
bindings: {
|
||||||
|
isFieldDisabled: '<',
|
||||||
|
tagName: '<',
|
||||||
|
onLookupClick: '<',
|
||||||
|
onTagDelete: '<',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<div class="input-group Form-mixedInputGroup">
|
||||||
|
<span class="input-group-btn Form-variableHeightButtonGroup input-group-prepend">
|
||||||
|
<button type="button"
|
||||||
|
class="Form-lookupButton
|
||||||
|
Form-lookupButton--variableHeight btn btn-default"
|
||||||
|
ng-click="vm.onLookupClick()"
|
||||||
|
ng-disabled="vm.isFieldDisabled">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span class="form-control Form-textInput Form-textInput--variableHeight input-medium lookup"
|
||||||
|
ng-disabled="vm.isFieldDisabled"
|
||||||
|
style="padding: 4px 6px;">
|
||||||
|
<div class="WebhookCredential-tags" ng-show="vm.tagName">
|
||||||
|
<div class="WebhookCredential-tagSection">
|
||||||
|
<div class="WebhookCredential-flexContainer">
|
||||||
|
<div class="WebhookCredential-tagContainer ng-scope"
|
||||||
|
ng-class="{'WebhookCredential-tagContainer--disabled': vm.tag.readOnly}">
|
||||||
|
<div class="WebhookCredential-iconContainer--disabled" ng-if="vm.isFieldDisabled">
|
||||||
|
<i class="fa fa-key WebhookCredential-tagIcon"></i>
|
||||||
|
</div>
|
||||||
|
<div class="WebhookCredential-iconContainer" ng-if="!vm.isFieldDisabled">
|
||||||
|
<i class="fa fa-key WebhookCredential-tagIcon"></i>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="WebhookCredential-tag"
|
||||||
|
ng-class="{'WebhookCredential-tag--deletable': !vm.isFieldDisabled, 'WebhookCredential-tag--disabled': vm.isFieldDisabled}"
|
||||||
|
>
|
||||||
|
<span class="WebhookCredential-name--label ng-binding">
|
||||||
|
{{ vm.tagName }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="WebhookCredential-deleteContainer"
|
||||||
|
ng-click="vm.onTagDelete()"
|
||||||
|
ng-hide="vm.isFieldDisabled">
|
||||||
|
<i class="fa fa-times WebhookCredential-tagDelete"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
.WebhookCredential-tags {
|
||||||
|
padding-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.WebhookCredential-flexContainer {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.WebhookCredential-tagContainer {
|
||||||
|
display: flex;
|
||||||
|
max-width: 100%;
|
||||||
|
background-color: @default-link;
|
||||||
|
color: @default-bg;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0px 0px 0px 10px;
|
||||||
|
margin: 3px 10px 3px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.WebhookCredential-tagContainer--disabled {
|
||||||
|
background-color: @default-icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
.WebhookCredential-tag {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 10px;
|
||||||
|
max-width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 2px 0px 2px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.WebhookCredential-tag--disabled {
|
||||||
|
border-top-left-radius: 0px;
|
||||||
|
border-bottom-left-radius: 0px;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.WebhookCredential-tag--deletable {
|
||||||
|
margin-right: 0px;
|
||||||
|
border-top-right-radius: 0px;
|
||||||
|
border-bottom-right-radius: 0px;
|
||||||
|
border-right: 0;
|
||||||
|
max-width: ~"calc(100% - 23px)";
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.WebhookCredential-deleteContainer {
|
||||||
|
border-top-right-radius: 5px;
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.WebhookCredential-tagDelete {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.WebhookCredential-iconContainer {
|
||||||
|
border-top-left-radius: 0px;
|
||||||
|
border-bottom-left-radius: 0px;
|
||||||
|
padding: 0px 5px;
|
||||||
|
margin: 3px 0px;
|
||||||
|
margin-left: -3px;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.WebhookCredential-iconContainer--disabled {
|
||||||
|
border-top-left-radius: 5px;
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
padding: 0 5px;
|
||||||
|
padding-left: 10px;
|
||||||
|
margin: 3px 0px;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.WebhookCredential-tagIcon {
|
||||||
|
margin: 0px 0px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.WebhookCredential-name {
|
||||||
|
flex: initial;
|
||||||
|
font-size: 12px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.WebhookCredential-name--label {
|
||||||
|
color: @default-list-header-bg;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: -8px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.WebhookCredential-tag--deletable > .WebhookCredential-name {
|
||||||
|
max-width: ~"calc(100% - 23px)";
|
||||||
|
}
|
||||||
|
|
||||||
|
.WebhookCredential-deleteContainer:hover {
|
||||||
|
border-color: @default-err;
|
||||||
|
background-color: @default-err!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.WebhookCredential-deleteContainer:hover > .WebhookCredential-tagDelete {
|
||||||
|
color: @default-bg;
|
||||||
|
}
|
||||||
@@ -296,7 +296,24 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p
|
|||||||
msg: i18n._('Failed to get organizations for which this user is a notification administrator. GET returned ') + status
|
msg: i18n._('Failed to get organizations for which this user is a notification administrator. GET returned ') + status
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}]
|
}],
|
||||||
|
webhookKey: ['Rest', 'ProcessErrors', 'jobTemplateData', 'i18n',
|
||||||
|
function(Rest, ProcessErrors, jobTemplateData, i18n) {
|
||||||
|
Rest.setUrl(jobTemplateData.related.webhook_key);
|
||||||
|
return Rest.get()
|
||||||
|
.then(({ data = {} }) => {
|
||||||
|
return data.webhook_key || '';
|
||||||
|
})
|
||||||
|
.catch(({data, status}) => {
|
||||||
|
if (status === 403) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ProcessErrors(null, data, status, null, {
|
||||||
|
hdr: i18n._('Error!'),
|
||||||
|
msg: i18n._('Failed to get webhook key GET returned ') + status
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -457,7 +474,24 @@ angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, p
|
|||||||
msg: i18n._('Failed to get organizations for which this user is a notification administrator. GET returned ') + status
|
msg: i18n._('Failed to get organizations for which this user is a notification administrator. GET returned ') + status
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}]
|
}],
|
||||||
|
webhookKey: ['Rest', 'ProcessErrors', 'workflowJobTemplateData', 'i18n',
|
||||||
|
function(Rest, ProcessErrors, workflowJobTemplateData, i18n) {
|
||||||
|
Rest.setUrl(workflowJobTemplateData.related.webhook_key);
|
||||||
|
return Rest.get()
|
||||||
|
.then(({ data = {} }) => {
|
||||||
|
return data.webhook_key || '';
|
||||||
|
})
|
||||||
|
.catch(({data, status}) => {
|
||||||
|
if (status === 403) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ProcessErrors(null, data, status, null, {
|
||||||
|
hdr: i18n._('Error!'),
|
||||||
|
msg: i18n._('Failed to get webhook key GET returned ') + status
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -162,8 +162,92 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n) {
|
|||||||
dataTitle: i18n._('Enable Concurrent Jobs'),
|
dataTitle: i18n._('Enable Concurrent Jobs'),
|
||||||
dataContainer: "body",
|
dataContainer: "body",
|
||||||
ngDisabled: '!(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddOrEdit)'
|
ngDisabled: '!(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddOrEdit)'
|
||||||
|
}, {
|
||||||
|
name: 'enable_webhook',
|
||||||
|
label: i18n._('Enable Webhook'),
|
||||||
|
type: 'checkbox',
|
||||||
|
column: 2,
|
||||||
|
awPopOver: "<p>" + i18n._("Enable webhook for this workflow job template.") + "</p>",
|
||||||
|
dataPlacement: 'right',
|
||||||
|
dataTitle: i18n._('Enable Webhook'),
|
||||||
|
dataContainer: "body",
|
||||||
|
ngDisabled: '!(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddOrEdit)'
|
||||||
}]
|
}]
|
||||||
}
|
},
|
||||||
|
webhook_service: {
|
||||||
|
label: i18n._('Webhook Service'),
|
||||||
|
type:'select',
|
||||||
|
defaultText: i18n._('Choose a Webhook Service'),
|
||||||
|
ngOptions: 'svc.label for svc in webhook_service_options track by svc.value',
|
||||||
|
ngShow: "enable_webhook && enable_webhook !== 'false'",
|
||||||
|
ngDisabled: "!(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddOrEdit)",
|
||||||
|
id: 'webhook-service-select',
|
||||||
|
column: 1,
|
||||||
|
awPopOver: "<p>" + i18n._("Select a webhook service.") + "</p>",
|
||||||
|
dataTitle: i18n._('Webhook Service'),
|
||||||
|
dataPlacement: 'right',
|
||||||
|
dataContainer: "body",
|
||||||
|
},
|
||||||
|
webhook_url: {
|
||||||
|
label: i18n._('Webhook URL'),
|
||||||
|
type: 'text',
|
||||||
|
ngShow: "workflow_job_template_obj && enable_webhook && enable_webhook !== 'false'",
|
||||||
|
awPopOver: "webhook_url_help",
|
||||||
|
awPopOverWatch: "webhook_url_help",
|
||||||
|
dataPlacement: 'top',
|
||||||
|
dataTitle: i18n._('Webhook URL'),
|
||||||
|
dataContainer: "body",
|
||||||
|
readonly: true
|
||||||
|
},
|
||||||
|
webhook_key: {
|
||||||
|
label: i18n._('Webhook Key'),
|
||||||
|
type: 'text',
|
||||||
|
ngShow: "enable_webhook && enable_webhook !== 'false'",
|
||||||
|
genHash: true,
|
||||||
|
genHashButtonTemplate: `
|
||||||
|
<span
|
||||||
|
ng-if="workflow_job_template_obj && webhook_service.value && currentlySavedWebhookKey === webhook_key"
|
||||||
|
class="input-group-btn input-group-prepend"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn Form-lookupButton"
|
||||||
|
ng-click="handleWebhookKeyButtonClick()"
|
||||||
|
aw-tool-tip="${i18n._('Rotate Webhook Key')}"
|
||||||
|
data-placement="top"
|
||||||
|
id="workflow_job_template_webhook_key_gen_btn"
|
||||||
|
>
|
||||||
|
<i class="fa fa-refresh" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
`,
|
||||||
|
genHashButtonClickHandlerName: "handleWebhookKeyButtonClick",
|
||||||
|
awPopOver: "webhook_key_help",
|
||||||
|
awPopOverWatch: "webhook_key_help",
|
||||||
|
dataPlacement: 'right',
|
||||||
|
dataTitle: i18n._("Webhook Key"),
|
||||||
|
dataContainer: "body",
|
||||||
|
readonly: true,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
webhook_credential: {
|
||||||
|
label: i18n._('Webhook Credential'),
|
||||||
|
type: 'custom',
|
||||||
|
ngShow: "enable_webhook && enable_webhook !== 'false'",
|
||||||
|
control: `
|
||||||
|
<webhook-credential-input
|
||||||
|
is-field-disabled="!(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddOrEdit) || !(webhookCredential.modalBaseParams.credential_type__namespace)"
|
||||||
|
tag-name="webhookCredential.name"
|
||||||
|
on-lookup-click="handleWebhookCredentialLookupClick"
|
||||||
|
on-tag-delete="handleWebhookCredentialTagDelete"
|
||||||
|
</webhook-credential-input>`,
|
||||||
|
awPopOver: "<p>" + i18n._("Select the credential to use with the webhook service.") + "</p>",
|
||||||
|
dataTitle: i18n._('Webhook Credential'),
|
||||||
|
dataPlacement: 'right',
|
||||||
|
dataContainer: "body",
|
||||||
|
ngDisabled: '!webhook_service.value',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
buttons: { //for now always generates <button> tags
|
buttons: { //for now always generates <button> tags
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ export default [
|
|||||||
'$scope', 'WorkflowForm', 'GenerateForm', 'Alert', 'ProcessErrors',
|
'$scope', 'WorkflowForm', 'GenerateForm', 'Alert', 'ProcessErrors',
|
||||||
'Wait', '$state', 'CreateSelect2', 'TemplatesService',
|
'Wait', '$state', 'CreateSelect2', 'TemplatesService',
|
||||||
'ToJSON', 'ParseTypeChange', '$q', 'Rest', 'GetBasePath', 'availableLabels', 'i18n',
|
'ToJSON', 'ParseTypeChange', '$q', 'Rest', 'GetBasePath', 'availableLabels', 'i18n',
|
||||||
'resolvedModels',
|
'resolvedModels', 'GetChoices', '$compile',
|
||||||
function($scope, WorkflowForm, GenerateForm, Alert, ProcessErrors,
|
function($scope, WorkflowForm, GenerateForm, Alert, ProcessErrors,
|
||||||
Wait, $state, CreateSelect2, TemplatesService, ToJSON,
|
Wait, $state, CreateSelect2, TemplatesService, ToJSON,
|
||||||
ParseTypeChange, $q, Rest, GetBasePath, availableLabels, i18n,
|
ParseTypeChange, $q, Rest, GetBasePath, availableLabels, i18n,
|
||||||
resolvedModels) {
|
resolvedModels, GetChoices, $compile) {
|
||||||
|
|
||||||
// Inject dynamic view
|
// Inject dynamic view
|
||||||
let form = WorkflowForm(),
|
let form = WorkflowForm(),
|
||||||
@@ -52,6 +52,138 @@ export default [
|
|||||||
$scope.workflowEditorTooltip = i18n._("Please save before defining the workflow graph.");
|
$scope.workflowEditorTooltip = i18n._("Please save before defining the workflow graph.");
|
||||||
$scope.surveyTooltip = i18n._('Please save before adding a survey to this workflow.');
|
$scope.surveyTooltip = i18n._('Please save before adding a survey to this workflow.');
|
||||||
|
|
||||||
|
$scope.webhook_service_options = null;
|
||||||
|
|
||||||
|
// populate webhook service choices
|
||||||
|
GetChoices({
|
||||||
|
scope: $scope,
|
||||||
|
url: GetBasePath('workflow_job_templates'),
|
||||||
|
field: 'webhook_service',
|
||||||
|
variable: 'webhook_service_options',
|
||||||
|
});
|
||||||
|
|
||||||
|
// set initial val for webhook checkbox
|
||||||
|
$scope.enable_webhook = false;
|
||||||
|
|
||||||
|
//
|
||||||
|
// webhook credential - all handlers, dynamic state, etc. live here
|
||||||
|
//
|
||||||
|
|
||||||
|
$scope.webhookCredential = {
|
||||||
|
id: null,
|
||||||
|
name: null,
|
||||||
|
isModalOpen: false,
|
||||||
|
isModalReady: false,
|
||||||
|
modalSelectedId: null,
|
||||||
|
modalSelectedName: null,
|
||||||
|
modalBaseParams: {
|
||||||
|
order_by: 'name',
|
||||||
|
page_size: 5,
|
||||||
|
credential_type__namespace: null,
|
||||||
|
},
|
||||||
|
modalTitle: i18n._('Select Webhook Credential'),
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialLookupClick = () => {
|
||||||
|
$scope.webhookCredential.modalSelectedId = $scope.webhookCredential.id;
|
||||||
|
$scope.webhookCredential.isModalOpen = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialTagDelete = () => {
|
||||||
|
$scope.webhookCredential.id = null;
|
||||||
|
$scope.webhookCredential.name = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialModalClose = () => {
|
||||||
|
$scope.webhookCredential.isModalOpen = false;
|
||||||
|
$scope.webhookCredential.isModalReady = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialModalReady = () => {
|
||||||
|
$scope.webhookCredential.isModalReady = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialModalItemSelect = (item) => {
|
||||||
|
$scope.webhookCredential.modalSelectedId = item.id;
|
||||||
|
$scope.webhookCredential.modalSelectedName = item.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialModalCancel = () => {
|
||||||
|
$scope.webhookCredential.isModalOpen = false;
|
||||||
|
$scope.webhookCredential.isModalReady = false;
|
||||||
|
$scope.webhookCredential.modalSelectedId = null;
|
||||||
|
$scope.webhookCredential.modalSelectedName = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialSelect = () => {
|
||||||
|
$scope.webhookCredential.isModalOpen = false;
|
||||||
|
$scope.webhookCredential.isModalReady = false;
|
||||||
|
$scope.webhookCredential.id = $scope.webhookCredential.modalSelectedId;
|
||||||
|
$scope.webhookCredential.name = $scope.webhookCredential.modalSelectedName;
|
||||||
|
$scope.webhookCredential.modalSelectedId = null;
|
||||||
|
$scope.webhookCredential.modalSelectedName = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookKeyButtonClick = () => {};
|
||||||
|
|
||||||
|
$('#content-container').append($compile(`
|
||||||
|
<at-dialog
|
||||||
|
title="webhookCredential.modalTitle"
|
||||||
|
on-close="handleWebhookCredentialModalClose"
|
||||||
|
ng-if="webhookCredential.isModalOpen"
|
||||||
|
ng-show="webhookCredential.isModalOpen && webhookCredential.isModalReady"
|
||||||
|
>
|
||||||
|
<at-lookup-list
|
||||||
|
ng-show="webhookCredential.isModalOpen && webhookCredential.isModalReady"
|
||||||
|
resource-name="credential"
|
||||||
|
base-params="webhookCredential.modalBaseParams"
|
||||||
|
selected-id="webhookCredential.modalSelectedId"
|
||||||
|
on-ready="handleWebhookCredentialModalReady"
|
||||||
|
on-item-select="handleWebhookCredentialModalItemSelect"
|
||||||
|
/>
|
||||||
|
<at-action-group col="12" pos="right">
|
||||||
|
<at-action-button
|
||||||
|
variant="tertiary"
|
||||||
|
ng-click="handleWebhookCredentialModalCancel()"
|
||||||
|
>
|
||||||
|
${i18n._('CANCEL')}
|
||||||
|
</at-action-button>
|
||||||
|
<at-action-button
|
||||||
|
variant="primary"
|
||||||
|
ng-click="handleWebhookCredentialSelect()"
|
||||||
|
>
|
||||||
|
${i18n._('SELECT')}
|
||||||
|
</at-action-button>
|
||||||
|
</at-action-group>
|
||||||
|
</at-dialog>`)($scope));
|
||||||
|
|
||||||
|
$scope.$watch('webhook_service', (newValue, oldValue) => {
|
||||||
|
const newServiceValue = newValue && typeof newValue === 'object' ? newValue.value : newValue;
|
||||||
|
const oldServiceValue = oldValue && typeof oldValue === 'object' ? oldValue.value : oldValue;
|
||||||
|
if (newServiceValue !== oldServiceValue || newServiceValue === newValue) {
|
||||||
|
$scope.webhook_service = { value: newServiceValue };
|
||||||
|
sync_webhook_service_select2();
|
||||||
|
$scope.webhookCredential.modalBaseParams.credential_type__namespace = newServiceValue ?
|
||||||
|
`${newServiceValue}_token`
|
||||||
|
: null;
|
||||||
|
if (newServiceValue !== newValue || newValue === null) {
|
||||||
|
$scope.webhookCredential.id = null;
|
||||||
|
$scope.webhookCredential.name = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function sync_webhook_service_select2() {
|
||||||
|
CreateSelect2({
|
||||||
|
element:'#webhook-service-select',
|
||||||
|
addNew: false,
|
||||||
|
multiple: false,
|
||||||
|
scope: $scope,
|
||||||
|
options: 'webhook_service_options',
|
||||||
|
model: 'webhook_service'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$scope.formSave = function () {
|
$scope.formSave = function () {
|
||||||
let fld, data = {};
|
let fld, data = {};
|
||||||
|
|
||||||
@@ -98,6 +230,25 @@ export default [
|
|||||||
.filter("[data-label-is-present=true]")
|
.filter("[data-label-is-present=true]")
|
||||||
.map((i, val) => ({name: $(val).text()}));
|
.map((i, val) => ({name: $(val).text()}));
|
||||||
|
|
||||||
|
|
||||||
|
delete data.webhook_url;
|
||||||
|
delete data.webhook_key;
|
||||||
|
delete data.enable_webhook;
|
||||||
|
data.webhook_credential = $scope.webhookCredential.id;
|
||||||
|
|
||||||
|
if (!data.webhook_service) {
|
||||||
|
data.webhook_credential = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$scope.enable_webhook) {
|
||||||
|
data.webhook_service = '';
|
||||||
|
data.webhook_credential = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.webhook_service && typeof data.webhook_service === 'object') {
|
||||||
|
data.webhook_service = data.webhook_service.value;
|
||||||
|
}
|
||||||
|
|
||||||
TemplatesService.createWorkflowJobTemplate(data)
|
TemplatesService.createWorkflowJobTemplate(data)
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ export default [
|
|||||||
'Wait', 'Empty', 'ToJSON', 'initSurvey', '$state', 'CreateSelect2',
|
'Wait', 'Empty', 'ToJSON', 'initSurvey', '$state', 'CreateSelect2',
|
||||||
'ParseVariableString', 'TemplatesService', 'Rest', 'ToggleNotification',
|
'ParseVariableString', 'TemplatesService', 'Rest', 'ToggleNotification',
|
||||||
'OrgAdminLookup', 'availableLabels', 'selectedLabels', 'workflowJobTemplateData', 'i18n',
|
'OrgAdminLookup', 'availableLabels', 'selectedLabels', 'workflowJobTemplateData', 'i18n',
|
||||||
'workflowLaunch', '$transitions', 'WorkflowJobTemplateModel', 'Inventory', 'isNotificationAdmin',
|
'workflowLaunch', '$transitions', 'WorkflowJobTemplateModel', 'Inventory', 'isNotificationAdmin', 'webhookKey', '$compile', '$location', 'GetChoices',
|
||||||
function($scope, $stateParams, WorkflowForm, GenerateForm, Alert,
|
function($scope, $stateParams, WorkflowForm, GenerateForm, Alert,
|
||||||
ProcessErrors, GetBasePath, $q, ParseTypeChange, Wait, Empty,
|
ProcessErrors, GetBasePath, $q, ParseTypeChange, Wait, Empty,
|
||||||
ToJSON, SurveyControllerInit, $state, CreateSelect2, ParseVariableString,
|
ToJSON, SurveyControllerInit, $state, CreateSelect2, ParseVariableString,
|
||||||
TemplatesService, Rest, ToggleNotification, OrgAdminLookup, availableLabels, selectedLabels, workflowJobTemplateData, i18n,
|
TemplatesService, Rest, ToggleNotification, OrgAdminLookup, availableLabels, selectedLabels, workflowJobTemplateData, i18n,
|
||||||
workflowLaunch, $transitions, WorkflowJobTemplate, Inventory, isNotificationAdmin
|
workflowLaunch, $transitions, WorkflowJobTemplate, Inventory, isNotificationAdmin, webhookKey, $compile, $location, GetChoices
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// To toggle notifications a user needs to have a read role on the WFJT
|
// To toggle notifications a user needs to have a read role on the WFJT
|
||||||
@@ -63,6 +63,179 @@ export default [
|
|||||||
$scope.inventory_name = Inventory.name;
|
$scope.inventory_name = Inventory.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scope.webhook_service_options = null;
|
||||||
|
$scope.webhook_service = workflowJobTemplateData.webhook_service;
|
||||||
|
$scope.webhook_url = '';
|
||||||
|
$scope.webhook_url_help = i18n._('Webhook services can launch jobs with this job template by making a POST request to this URL.');
|
||||||
|
$scope.webhook_key_help = i18n._('Webhook services can use this as a shared secret.');
|
||||||
|
$scope.currentlySavedWebhookKey = webhookKey;
|
||||||
|
$scope.webhook_key = webhookKey;
|
||||||
|
|
||||||
|
// populate webhook service choices
|
||||||
|
GetChoices({
|
||||||
|
scope: $scope,
|
||||||
|
url: GetBasePath('workflow_job_templates'),
|
||||||
|
field: 'webhook_service',
|
||||||
|
variable: 'webhook_service_options',
|
||||||
|
});
|
||||||
|
|
||||||
|
// set initial val for webhook checkbox
|
||||||
|
if (workflowJobTemplateData.webhook_service) {
|
||||||
|
$scope.enable_webhook = true;
|
||||||
|
} else {
|
||||||
|
$scope.enable_webhook = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set domain / base url
|
||||||
|
$scope.baseURL = $location.protocol() + '://' + $location.host() + (($location.port()) ? ':' + $location.port() : '');
|
||||||
|
|
||||||
|
//
|
||||||
|
// webhook credential - all handlers, dynamic state, etc. live here
|
||||||
|
//
|
||||||
|
|
||||||
|
$scope.webhookCredential = {
|
||||||
|
id: _.get(workflowJobTemplateData, ['summary_fields', 'webhook_credential', 'id']),
|
||||||
|
name: _.get(workflowJobTemplateData, ['summary_fields', 'webhook_credential', 'name']),
|
||||||
|
isModalOpen: false,
|
||||||
|
isModalReady: false,
|
||||||
|
modalSelectedId: null,
|
||||||
|
modalSelectedName: null,
|
||||||
|
modalBaseParams: {
|
||||||
|
order_by: 'name',
|
||||||
|
page_size: 5,
|
||||||
|
credential_type__namespace: `${workflowJobTemplateData.webhook_service}_token`,
|
||||||
|
},
|
||||||
|
modalTitle: i18n._('Select Webhook Credential'),
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialLookupClick = () => {
|
||||||
|
$scope.webhookCredential.modalSelectedId = $scope.webhookCredential.id;
|
||||||
|
$scope.webhookCredential.isModalOpen = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialTagDelete = () => {
|
||||||
|
$scope.webhookCredential.id = null;
|
||||||
|
$scope.webhookCredential.name = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialModalClose = () => {
|
||||||
|
$scope.webhookCredential.isModalOpen = false;
|
||||||
|
$scope.webhookCredential.isModalReady = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialModalReady = () => {
|
||||||
|
$scope.webhookCredential.isModalReady = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialModalItemSelect = (item) => {
|
||||||
|
$scope.webhookCredential.modalSelectedId = item.id;
|
||||||
|
$scope.webhookCredential.modalSelectedName = item.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialModalCancel = () => {
|
||||||
|
$scope.webhookCredential.isModalOpen = false;
|
||||||
|
$scope.webhookCredential.isModalReady = false;
|
||||||
|
$scope.webhookCredential.modalSelectedId = null;
|
||||||
|
$scope.webhookCredential.modalSelectedName = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookCredentialSelect = () => {
|
||||||
|
$scope.webhookCredential.isModalOpen = false;
|
||||||
|
$scope.webhookCredential.isModalReady = false;
|
||||||
|
$scope.webhookCredential.id = $scope.webhookCredential.modalSelectedId;
|
||||||
|
$scope.webhookCredential.name = $scope.webhookCredential.modalSelectedName;
|
||||||
|
$scope.webhookCredential.modalSelectedId = null;
|
||||||
|
$scope.webhookCredential.modalSelectedName = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleWebhookKeyButtonClick = () => {
|
||||||
|
Rest.setUrl(workflowJobTemplateData.related.webhook_key);
|
||||||
|
Wait('start');
|
||||||
|
Rest.post({})
|
||||||
|
.then(({ data }) => {
|
||||||
|
$scope.currentlySavedWebhookKey = data.webhook_key;
|
||||||
|
$scope.webhook_key = data.webhook_key;
|
||||||
|
})
|
||||||
|
.catch(({ data }) => {
|
||||||
|
const errorMsg = `Failed to generate new webhook key. POST returned status: ${status}`;
|
||||||
|
ProcessErrors($scope, data, status, form, { hdr: 'Error!', msg: errorMsg });
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
Wait('stop');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$('#content-container').append($compile(`
|
||||||
|
<at-dialog
|
||||||
|
title="webhookCredential.modalTitle"
|
||||||
|
on-close="handleWebhookCredentialModalClose"
|
||||||
|
ng-if="webhookCredential.isModalOpen"
|
||||||
|
ng-show="webhookCredential.isModalOpen && webhookCredential.isModalReady"
|
||||||
|
>
|
||||||
|
<at-lookup-list
|
||||||
|
ng-show="webhookCredential.isModalOpen && webhookCredential.isModalReady"
|
||||||
|
resource-name="credential"
|
||||||
|
base-params="webhookCredential.modalBaseParams"
|
||||||
|
selected-id="webhookCredential.modalSelectedId"
|
||||||
|
on-ready="handleWebhookCredentialModalReady"
|
||||||
|
on-item-select="handleWebhookCredentialModalItemSelect"
|
||||||
|
/>
|
||||||
|
<at-action-group col="12" pos="right">
|
||||||
|
<at-action-button
|
||||||
|
variant="tertiary"
|
||||||
|
ng-click="handleWebhookCredentialModalCancel()"
|
||||||
|
>
|
||||||
|
${i18n._('CANCEL')}
|
||||||
|
</at-action-button>
|
||||||
|
<at-action-button
|
||||||
|
variant="primary"
|
||||||
|
ng-click="handleWebhookCredentialSelect()"
|
||||||
|
>
|
||||||
|
${i18n._('SELECT')}
|
||||||
|
</at-action-button>
|
||||||
|
</at-action-group>
|
||||||
|
</at-dialog>`)($scope));
|
||||||
|
|
||||||
|
$scope.$watch('webhook_service', (newValue, oldValue) => {
|
||||||
|
const newServiceValue = newValue && typeof newValue === 'object' ? newValue.value : newValue;
|
||||||
|
const oldServiceValue = oldValue && typeof oldValue === 'object' ? oldValue.value : oldValue;
|
||||||
|
if (newServiceValue) {
|
||||||
|
$scope.webhook_url = `${$scope.baseURL}${workflowJobTemplateData.url}${newServiceValue}/`;
|
||||||
|
$scope.enable_webhook = true;
|
||||||
|
} else {
|
||||||
|
$scope.webhook_url = '';
|
||||||
|
$scope.webhook_key = '';
|
||||||
|
}
|
||||||
|
if (newServiceValue !== oldServiceValue || newServiceValue === newValue) {
|
||||||
|
$scope.webhook_service = { value: newServiceValue };
|
||||||
|
sync_webhook_service_select2();
|
||||||
|
$scope.webhookCredential.modalBaseParams.credential_type__namespace = newServiceValue ?
|
||||||
|
`${newServiceValue}_token` : null;
|
||||||
|
if (newServiceValue !== newValue || newValue === null) {
|
||||||
|
$scope.webhookCredential.id = null;
|
||||||
|
$scope.webhookCredential.name = null;
|
||||||
|
}
|
||||||
|
if (newServiceValue !== newValue) {
|
||||||
|
if (newServiceValue === workflowJobTemplateData.webhook_service) {
|
||||||
|
$scope.webhook_key = $scope.currentlySavedWebhookKey;
|
||||||
|
} else {
|
||||||
|
$scope.webhook_key = i18n._('A NEW WEBHOOK KEY WILL BE GENERATED ON SAVE');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function sync_webhook_service_select2() {
|
||||||
|
CreateSelect2({
|
||||||
|
element:'#webhook-service-select',
|
||||||
|
addNew: false,
|
||||||
|
multiple: false,
|
||||||
|
scope: $scope,
|
||||||
|
options: 'webhook_service_options',
|
||||||
|
model: 'webhook_service'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$scope.openWorkflowMaker = function() {
|
$scope.openWorkflowMaker = function() {
|
||||||
$state.go('templates.editWorkflowJobTemplate.workflowMaker');
|
$state.go('templates.editWorkflowJobTemplate.workflowMaker');
|
||||||
};
|
};
|
||||||
@@ -124,6 +297,25 @@ export default [
|
|||||||
.filter("[data-label-is-present=true]")
|
.filter("[data-label-is-present=true]")
|
||||||
.map((i, val) => ({name: $(val).text()}));
|
.map((i, val) => ({name: $(val).text()}));
|
||||||
|
|
||||||
|
|
||||||
|
delete data.webhook_url;
|
||||||
|
delete data.webhook_key;
|
||||||
|
delete data.enable_webhook;
|
||||||
|
data.webhook_credential = $scope.webhookCredential.id;
|
||||||
|
|
||||||
|
if (!data.webhook_service) {
|
||||||
|
data.webhook_credential = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$scope.enable_webhook) {
|
||||||
|
data.webhook_service = '';
|
||||||
|
data.webhook_credential = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.webhook_service && typeof data.webhook_service === 'object') {
|
||||||
|
data.webhook_service = data.webhook_service.value;
|
||||||
|
}
|
||||||
|
|
||||||
TemplatesService.updateWorkflowJobTemplate({
|
TemplatesService.updateWorkflowJobTemplate({
|
||||||
id: id,
|
id: id,
|
||||||
data: data
|
data: data
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions',
|
|||||||
$scope.cloud_credential_link = getLink('cloud_credential');
|
$scope.cloud_credential_link = getLink('cloud_credential');
|
||||||
$scope.network_credential_link = getLink('network_credential');
|
$scope.network_credential_link = getLink('network_credential');
|
||||||
|
|
||||||
|
$scope.launched_by_webhook_link = null;
|
||||||
|
if ($scope.workflow.launch_type === 'webhook') {
|
||||||
|
$scope.launched_by_webhook_link = $scope.workflow_template_link;
|
||||||
|
}
|
||||||
|
|
||||||
if ($scope.workflow.summary_fields.inventory) {
|
if ($scope.workflow.summary_fields.inventory) {
|
||||||
if ($scope.workflow.summary_fields.inventory.kind === 'smart') {
|
if ($scope.workflow.summary_fields.inventory.kind === 'smart') {
|
||||||
$scope.inventory_link = '/#/inventories/smart/' + $scope.workflow.inventory;
|
$scope.inventory_link = '/#/inventories/smart/' + $scope.workflow.inventory;
|
||||||
@@ -57,6 +62,7 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions',
|
|||||||
EDIT_INVENTORY: i18n._('Edit the inventory'),
|
EDIT_INVENTORY: i18n._('Edit the inventory'),
|
||||||
SOURCE_WORKFLOW_JOB: i18n._('View the source Workflow Job'),
|
SOURCE_WORKFLOW_JOB: i18n._('View the source Workflow Job'),
|
||||||
TOGGLE_STDOUT_FULLSCREEN: i18n._('Expand Output'),
|
TOGGLE_STDOUT_FULLSCREEN: i18n._('Expand Output'),
|
||||||
|
WEBHOOK_WORKFLOW_JOB_TEMPLATE: i18n._('View the webhook configuration on the workflow job template.'),
|
||||||
STATUS: '' // re-assigned elsewhere
|
STATUS: '' // re-assigned elsewhere
|
||||||
},
|
},
|
||||||
labels: {
|
labels: {
|
||||||
@@ -79,6 +85,7 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions',
|
|||||||
NOT_STARTED: i18n._('Not Started'),
|
NOT_STARTED: i18n._('Not Started'),
|
||||||
SHOW_LESS: i18n._('Show Less'),
|
SHOW_LESS: i18n._('Show Less'),
|
||||||
SHOW_MORE: i18n._('Show More'),
|
SHOW_MORE: i18n._('Show More'),
|
||||||
|
WEBHOOK: i18n._('Webhook'),
|
||||||
},
|
},
|
||||||
results: {
|
results: {
|
||||||
TOTAL_NODES: i18n._('Total Nodes'),
|
TOTAL_NODES: i18n._('Total Nodes'),
|
||||||
|
|||||||
@@ -177,7 +177,7 @@
|
|||||||
|
|
||||||
<!-- CREATED BY DETAIL -->
|
<!-- CREATED BY DETAIL -->
|
||||||
<div class="WorkflowResults-resultRow"
|
<div class="WorkflowResults-resultRow"
|
||||||
ng-show="workflow.summary_fields.created_by.username">
|
ng-show="workflow.summary_fields.created_by.username && !launched_by_webhook_link">
|
||||||
<label class="WorkflowResults-resultRowLabel">
|
<label class="WorkflowResults-resultRowLabel">
|
||||||
{{ strings.labels.LAUNCHED_BY }}
|
{{ strings.labels.LAUNCHED_BY }}
|
||||||
</label>
|
</label>
|
||||||
@@ -192,7 +192,7 @@
|
|||||||
|
|
||||||
<!-- SCHEDULED BY DETAIL -->
|
<!-- SCHEDULED BY DETAIL -->
|
||||||
<div class="WorkflowResults-resultRow toggle-show"
|
<div class="WorkflowResults-resultRow toggle-show"
|
||||||
ng-show="workflow.summary_fields.schedule.name">
|
ng-show="workflow.summary_fields.schedule.name && !launched_by_webhook_link">
|
||||||
<label
|
<label
|
||||||
class="WorkflowResults-resultRowLabel">
|
class="WorkflowResults-resultRowLabel">
|
||||||
{{ strings.labels.LAUNCHED_BY }}
|
{{ strings.labels.LAUNCHED_BY }}
|
||||||
@@ -206,6 +206,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- LAUNCHED BY WEBHOOK DETAIL -->
|
||||||
|
<div class="WorkflowResults-resultRow toggle-show"
|
||||||
|
ng-show="launched_by_webhook_link">
|
||||||
|
<label
|
||||||
|
class="WorkflowResults-resultRowLabel">
|
||||||
|
{{ strings.labels.LAUNCHED_BY }}
|
||||||
|
</label>
|
||||||
|
<div class="WorkflowResults-resultRowText">
|
||||||
|
<a href="{{ launched_by_webhook_link }}"
|
||||||
|
aw-tool-tip="{{ strings.tooltips.WEBHOOK_WORKFLOW_JOB_TEMPLATE }}"
|
||||||
|
data-placement="top">
|
||||||
|
{{ strings.details.WEBHOOK }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- SLIIIIIICE -->
|
<!-- SLIIIIIICE -->
|
||||||
<div class="WorkflowResults-resultRow"
|
<div class="WorkflowResults-resultRow"
|
||||||
ng-show="workflow.summary_fields.job_template.name">
|
ng-show="workflow.summary_fields.job_template.name">
|
||||||
|
|||||||
@@ -97,6 +97,10 @@ describe('Controller: WorkflowAdd', () => {
|
|||||||
.whenGET(/^\/api\/?$/)
|
.whenGET(/^\/api\/?$/)
|
||||||
.respond(200, '');
|
.respond(200, '');
|
||||||
|
|
||||||
|
$httpBackend
|
||||||
|
.when('OPTIONS', '/')
|
||||||
|
.respond(200, '');
|
||||||
|
|
||||||
$httpBackend
|
$httpBackend
|
||||||
.whenGET(/\/static\/*/)
|
.whenGET(/\/static\/*/)
|
||||||
.respond(200, {});
|
.respond(200, {});
|
||||||
@@ -149,6 +153,8 @@ describe('Controller: WorkflowAdd', () => {
|
|||||||
labels: undefined,
|
labels: undefined,
|
||||||
variables: undefined,
|
variables: undefined,
|
||||||
allow_simultaneous: undefined,
|
allow_simultaneous: undefined,
|
||||||
|
webhook_service: '',
|
||||||
|
webhook_credential: null,
|
||||||
ask_inventory_on_launch: false,
|
ask_inventory_on_launch: false,
|
||||||
ask_variables_on_launch: false,
|
ask_variables_on_launch: false,
|
||||||
ask_limit_on_launch: false,
|
ask_limit_on_launch: false,
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ import './file.unit';
|
|||||||
import './layout.unit';
|
import './layout.unit';
|
||||||
import './side-nav.unit';
|
import './side-nav.unit';
|
||||||
import './side-nav-item.unit';
|
import './side-nav-item.unit';
|
||||||
import './jobs-list-split-jobs.unit';
|
import './jobs-list.unit';
|
||||||
import './job-details-split-jobs.unit';
|
import './job-details-split-jobs.unit';
|
||||||
import './stream.unit';
|
import './stream.unit';
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ describe('View: Split Jobs List', () => {
|
|||||||
if (str === 'list.SLICE_JOB') {
|
if (str === 'list.SLICE_JOB') {
|
||||||
return 'Slice Job';
|
return 'Slice Job';
|
||||||
}
|
}
|
||||||
|
if (str === 'list.ROW_ITEM_LABEL_WEBHOOK') {
|
||||||
|
return 'Webhook';
|
||||||
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -96,40 +99,51 @@ describe('View: Split Jobs List', () => {
|
|||||||
it('is created successfully', () => {
|
it('is created successfully', () => {
|
||||||
expect(JobList).toBeDefined();
|
expect(JobList).toBeDefined();
|
||||||
});
|
});
|
||||||
it('has method "getSplitJobDetails"', () => {
|
it('has method "getSecondaryTagLabel"', () => {
|
||||||
expect(JobList.getSliceJobDetails).toBeDefined();
|
expect(JobList.getSecondaryTagLabel).toBeDefined();
|
||||||
});
|
});
|
||||||
it('returns a string', () => {
|
it('returns the expected string when slice data is available', () => {
|
||||||
const data = {
|
const data = {
|
||||||
job_slice_number: 1,
|
job_slice_number: 1,
|
||||||
job_slice_count: 2
|
job_slice_count: 2,
|
||||||
|
launch_type: 'manual',
|
||||||
};
|
};
|
||||||
const result = JobList.getSliceJobDetails(data);
|
const result = JobList.getSecondaryTagLabel(data);
|
||||||
expect(result).toEqual('Slice Job 1/2');
|
expect(result).toEqual('Slice Job 1/2');
|
||||||
});
|
});
|
||||||
it('returns null when data is null', () => {
|
it('returns null when slice data is null', () => {
|
||||||
const data = {
|
const data = {
|
||||||
job_slice_number: null,
|
job_slice_number: null,
|
||||||
job_slice_count: null
|
job_slice_count: null,
|
||||||
|
launch_type: 'manual',
|
||||||
};
|
};
|
||||||
const result = JobList.getSliceJobDetails(data);
|
const result = JobList.getSecondaryTagLabel(data);
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
it('returns null when data is undefined', () => {
|
it('returns null when slice data is undefined', () => {
|
||||||
const data = {
|
const data = {
|
||||||
job_slice_number: undefined,
|
job_slice_number: undefined,
|
||||||
job_slice_count: undefined
|
job_slice_count: undefined,
|
||||||
|
launch_type: 'manual',
|
||||||
};
|
};
|
||||||
const result = JobList.getSliceJobDetails(data);
|
const result = JobList.getSecondaryTagLabel(data);
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
it('returns null when job is not a sliced job', () => {
|
it('returns null when job is not a sliced or webhook job', () => {
|
||||||
const data = {
|
const data = {
|
||||||
job_slice_number: null,
|
job_slice_number: null,
|
||||||
job_slice_count: 1
|
job_slice_count: 1,
|
||||||
|
launch_type: 'manual',
|
||||||
};
|
};
|
||||||
const result = JobList.getSliceJobDetails(data);
|
const result = JobList.getSecondaryTagLabel(data);
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
it('returns the expected string for webhook jobs', () => {
|
||||||
|
const data = {
|
||||||
|
launch_type: 'webhook',
|
||||||
|
};
|
||||||
|
const result = JobList.getSecondaryTagLabel(data);
|
||||||
|
expect(result).toEqual('Webhook');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -28,10 +28,6 @@ if settings.SETTINGS_MODULE == 'awx.settings.development':
|
|||||||
try:
|
try:
|
||||||
import debug_toolbar
|
import debug_toolbar
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
# for Django version 2.0
|
|
||||||
# path('__debug__/', include(debug_toolbar.urls)),
|
|
||||||
|
|
||||||
# TODO: this is the Django < 2.0 version, REMOVEME
|
|
||||||
url(r'^__debug__/', include(debug_toolbar.urls))
|
url(r'^__debug__/', include(debug_toolbar.urls))
|
||||||
]
|
]
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ class JobTemplate(
|
|||||||
'vault_credential',
|
'vault_credential',
|
||||||
'verbosity',
|
'verbosity',
|
||||||
'job_slice_count',
|
'job_slice_count',
|
||||||
|
'webhook_service',
|
||||||
|
'webhook_credential',
|
||||||
'scm_branch')
|
'scm_branch')
|
||||||
|
|
||||||
update_payload(payload, optional_fields, kwargs)
|
update_payload(payload, optional_fields, kwargs)
|
||||||
@@ -102,6 +104,15 @@ class JobTemplate(
|
|||||||
payload.update(inventory=kwargs.get('inventory').id)
|
payload.update(inventory=kwargs.get('inventory').id)
|
||||||
if kwargs.get('credential'):
|
if kwargs.get('credential'):
|
||||||
payload.update(credential=kwargs.get('credential').id)
|
payload.update(credential=kwargs.get('credential').id)
|
||||||
|
if kwargs.get('webhook_credential'):
|
||||||
|
webhook_cred = kwargs.get('webhook_credential')
|
||||||
|
if isinstance(webhook_cred, int):
|
||||||
|
payload.update(webhook_credential=int(webhook_cred))
|
||||||
|
elif hasattr(webhook_cred, 'id'):
|
||||||
|
payload.update(webhook_credential=webhook_cred.id)
|
||||||
|
else:
|
||||||
|
raise AttributeError("Webhook credential must either be integer of pkid or Credential object")
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
def add_label(self, label):
|
def add_label(self, label):
|
||||||
|
|||||||
@@ -36,9 +36,15 @@ class WorkflowJobTemplate(HasCopy, HasCreate, HasNotifications, HasSurvey, Unifi
|
|||||||
|
|
||||||
optional_fields = (
|
optional_fields = (
|
||||||
"allow_simultaneous",
|
"allow_simultaneous",
|
||||||
"ask_variables_on_launch", "ask_inventory_on_launch", "ask_scm_branch_on_launch", "ask_limit_on_launch",
|
"ask_variables_on_launch",
|
||||||
"limit", "scm_branch",
|
"ask_inventory_on_launch",
|
||||||
"survey_enabled"
|
"ask_scm_branch_on_launch",
|
||||||
|
"ask_limit_on_launch",
|
||||||
|
"limit",
|
||||||
|
"scm_branch",
|
||||||
|
"survey_enabled",
|
||||||
|
"webhook_service",
|
||||||
|
"webhook_credential",
|
||||||
)
|
)
|
||||||
update_payload(payload, optional_fields, kwargs)
|
update_payload(payload, optional_fields, kwargs)
|
||||||
|
|
||||||
@@ -54,6 +60,15 @@ class WorkflowJobTemplate(HasCopy, HasCreate, HasNotifications, HasSurvey, Unifi
|
|||||||
if kwargs.get('inventory'):
|
if kwargs.get('inventory'):
|
||||||
payload.inventory = kwargs.get('inventory').id
|
payload.inventory = kwargs.get('inventory').id
|
||||||
|
|
||||||
|
if kwargs.get('webhook_credential'):
|
||||||
|
webhook_cred = kwargs.get('webhook_credential')
|
||||||
|
if isinstance(webhook_cred, int):
|
||||||
|
payload.update(webhook_credential=int(webhook_cred))
|
||||||
|
elif hasattr(webhook_cred, 'id'):
|
||||||
|
payload.update(webhook_credential=webhook_cred.id)
|
||||||
|
else:
|
||||||
|
raise AttributeError("Webhook credential must either be integer of pkid or Credential object")
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
def create_payload(self, name='', description='', organization=None, **kwargs):
|
def create_payload(self, name='', description='', organization=None, **kwargs):
|
||||||
|
|||||||
Reference in New Issue
Block a user