diff --git a/awx/api/views/webhooks.py b/awx/api/views/webhooks.py index be673c6af4..bd05a55e3b 100644 --- a/awx/api/views/webhooks.py +++ b/awx/api/views/webhooks.py @@ -2,6 +2,7 @@ from hashlib import sha1 import hmac import json import logging +import urllib.parse from django.utils.encoding import force_bytes from django.views.decorators.csrf import csrf_exempt @@ -53,7 +54,7 @@ class WebhookReceiverBase(APIView): permission_classes = (AllowAny,) authentication_classes = () - event_keys = {} + ref_keys = {} def get_queryset(self): qs_models = { @@ -83,8 +84,11 @@ class WebhookReceiverBase(APIView): def get_event_guid(self): raise NotImplementedError + def get_event_status_api(self): + raise NotImplementedError + def get_event_ref(self): - key = self.event_keys.get(self.get_event_type(), '') + key = self.ref_keys.get(self.get_event_type(), '') value = self.request.data for element in key.split('.'): try: @@ -126,6 +130,7 @@ class WebhookReceiverBase(APIView): 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 = { 'webhook_service': obj.webhook_service, @@ -147,11 +152,10 @@ class WebhookReceiverBase(APIView): '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, }) } - # if event_ref: - # kwargs['scm_branch'] = event_ref new_job = obj.create_unified_job(**kwargs) new_job.signal_start() @@ -162,7 +166,7 @@ class WebhookReceiverBase(APIView): class GithubWebhookReceiver(WebhookReceiverBase): service = 'github' - event_keys = { + ref_keys = { 'pull_request': 'pull_request.head.sha', 'pull_request_review': 'pull_request.head.sha', 'pull_request_review_comment': 'pull_request.head.sha', @@ -179,6 +183,11 @@ class GithubWebhookReceiver(WebhookReceiverBase): 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: @@ -192,7 +201,7 @@ class GithubWebhookReceiver(WebhookReceiverBase): class GitlabWebhookReceiver(WebhookReceiverBase): service = 'gitlab' - event_keys = { + ref_keys = { 'Push Hook': 'checkout_sha', 'Tag Push Hook': 'checkout_sha', 'Merge Request Hook': 'object_attributes.last_commit.id', @@ -207,6 +216,18 @@ class GitlabWebhookReceiver(WebhookReceiverBase): 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 "{}://{}/projects/{}/repository/commits/{}/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')) @@ -224,7 +245,7 @@ class GitlabWebhookReceiver(WebhookReceiverBase): class BitbucketWebhookReceiver(WebhookReceiverBase): service = 'bitbucket' - event_keys = { + ref_keys = { # Bitbucket Server 'repo:refs_changed': 'changes.0.toHash', 'repo:comment:added': 'commit', diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index dddca05d6e..58b772b425 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -1,8 +1,10 @@ # Python -import os -import json from copy import copy, deepcopy +import json +import logging +import os +import requests # Django from django.apps import apps @@ -27,6 +29,9 @@ from awx.main.fields import JSONField, AskForField from awx.main.constants import ACTIVE_STATES +logger = logging.getLogger('awx.main.models.mixins') + + __all__ = ['ResourceMixin', 'SurveyJobTemplateMixin', 'SurveyJobMixin', 'TaskManagerUnifiedJobMixin', 'TaskManagerJobMixin', 'TaskManagerProjectUpdateMixin', 'TaskManagerInventoryUpdateMixin', 'CustomVirtualEnvMixin'] @@ -553,3 +558,29 @@ class WebhookMixin(models.Model): blank=True, max_length=128 ) + + def update_scm_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', + 'gitlab': 'PRIVATE-TOKEN', + } + try: + headers = {service_header[self.webhook_service]: self.webhook_credential.get_input('token')} + response = requests.post(status_api, headers=headers) + except Exception: + logger.exception("Posting webhook status caused an error.") + return + + if response.status_code < 400: + logger.debug("Webhook status update sent.") + else: + logger.debug("Posting webhook status failed, code: {}".format(response.status_code)) diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index b8dd1d1cf8..80848e7ac9 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -1422,3 +1422,6 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique def is_isolated(self): return bool(self.controller_node) + + def update_scm_status(self, status): + return