Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion awx/api/urls/webhooks.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from django.urls import re_path

from awx.api.views.webhooks import WebhookKeyView, GithubWebhookReceiver, GitlabWebhookReceiver, BitbucketDcWebhookReceiver
from awx.api.views.webhooks import WebhookKeyView, GithubWebhookReceiver, GitlabWebhookReceiver, BitbucketDcWebhookReceiver, BitbucketCloudWebhookReceiver


urlpatterns = [
re_path(r'^webhook_key/$', WebhookKeyView.as_view(), name='webhook_key'),
re_path(r'^github/$', GithubWebhookReceiver.as_view(), name='webhook_receiver_github'),
re_path(r'^gitlab/$', GitlabWebhookReceiver.as_view(), name='webhook_receiver_gitlab'),
re_path(r'^bitbucket_dc/$', BitbucketDcWebhookReceiver.as_view(), name='webhook_receiver_bitbucket_dc'),
re_path(r'^bitbucket_cloud/$', BitbucketCloudWebhookReceiver.as_view(), name='webhook_receiver_bitbucket_cloud'),
]
53 changes: 53 additions & 0 deletions awx/api/views/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,3 +305,56 @@ def get_signature(self):
raise PermissionDenied
hash_alg, signature = header_sig.split('=')
return hash_alg, force_bytes(signature)


class BitbucketCloudWebhookReceiver(WebhookReceiverBase):
service = 'bitbucket_cloud'

ref_keys = {
'repo:push': 'push.changes.0.target.hash',
'pullrequest:created': 'pullrequest.source.commit.hash',
'pullrequest:updated': 'pullrequest.source.commit.hash',
'pullrequest:fulfilled': 'pullrequest.source.commit.hash',
}

def get_event_type(self):
return self.request.META.get('HTTP_X_EVENT_KEY')

def get_event_guid(self):
return self.request.META.get('HTTP_X_REQUEST_UUID')

def get_event_status_api(self):
# <bitbucket-base-url>/commit/<commit-hash>/statuses/build
if self.get_event_type() not in self.ref_keys.keys():
return
if self.get_event_ref() is None:
return
any_url = None
if 'repository' in self.request.data:
any_url = self.request.data['repository'].get('links', {}).get('self')
if any_url is None:
return
any_url = any_url.get('href')
if any_url is None:
return
return "{}/commit/{}/statuses/build".format(any_url, self.get_event_ref())

def is_ignored_request(self):
return self.get_event_type() not in [
'repo:push',
'pullrequest:created',
'pullrequest:updated',
'pullrequest:fulfilled',
]

def must_check_signature(self):
# Bitbucket does not sign ping requests...
return self.get_event_type() != 'diagnostics:ping'

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 X_HUB_SIGNATURE")
raise PermissionDenied
hash_alg, signature = header_sig.split('=')
return hash_alg, force_bytes(signature)
53 changes: 53 additions & 0 deletions awx/main/migrations/0200_alter_job_webhook_service_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Generated by Django 4.2.16 on 2024-12-11 09:36

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('main', '0199_alter_oauth2application_unique_together_and_more'),
]

operations = [
migrations.AlterField(
model_name='job',
name='webhook_service',
field=models.CharField(
blank=True,
choices=[('github', 'GitHub'), ('gitlab', 'GitLab'), ('bitbucket_dc', 'BitBucket DataCenter'), ('bitbucket_cloud', 'BitBucket Cloud')],
help_text='Service that webhook requests will be accepted from',
max_length=16,
),
),
migrations.AlterField(
model_name='jobtemplate',
name='webhook_service',
field=models.CharField(
blank=True,
choices=[('github', 'GitHub'), ('gitlab', 'GitLab'), ('bitbucket_dc', 'BitBucket DataCenter'), ('bitbucket_cloud', 'BitBucket Cloud')],
help_text='Service that webhook requests will be accepted from',
max_length=16,
),
),
migrations.AlterField(
model_name='workflowjob',
name='webhook_service',
field=models.CharField(
blank=True,
choices=[('github', 'GitHub'), ('gitlab', 'GitLab'), ('bitbucket_dc', 'BitBucket DataCenter'), ('bitbucket_cloud', 'BitBucket Cloud')],
help_text='Service that webhook requests will be accepted from',
max_length=16,
),
),
migrations.AlterField(
model_name='workflowjobtemplate',
name='webhook_service',
field=models.CharField(
blank=True,
choices=[('github', 'GitHub'), ('gitlab', 'GitLab'), ('bitbucket_dc', 'BitBucket DataCenter'), ('bitbucket_cloud', 'BitBucket Cloud')],
help_text='Service that webhook requests will be accepted from',
max_length=16,
),
),
]
17 changes: 17 additions & 0 deletions awx/main/models/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,7 @@ class Meta:
('github', "GitHub"),
('gitlab', "GitLab"),
('bitbucket_dc', "BitBucket DataCenter"),
('bitbucket_cloud', "BitBucket Cloud"),
]

webhook_service = models.CharField(max_length=16, choices=SERVICES, blank=True, help_text=_('Service that webhook requests will be accepted from'))
Expand Down Expand Up @@ -636,6 +637,7 @@ def update_webhook_status(self, status):
'github': ('Authorization', 'token {}'),
'gitlab': ('PRIVATE-TOKEN', '{}'),
'bitbucket_dc': ('Authorization', 'Bearer {}'),
'bitbucket_cloud': ('Authorization', 'Bearer {}'),
}
service_statuses = {
'github': {
Expand All @@ -661,6 +663,14 @@ def update_webhook_status(self, status):
'error': 'FAILED',
'canceled': 'FAILED',
},
'bitbucket_cloud': {
'pending': 'INPROGRESS', # Bitbucket Cloud doesn't have any other statuses distinct from INPROGRESS, SUCCESSFUL, FAILED and STOPPED
'running': 'INPROGRESS',
'successful': 'SUCCESSFUL',
'failed': 'FAILED',
'error': 'FAILED',
'canceled': 'STOPPED',
},
}

statuses = service_statuses[self.webhook_service]
Expand All @@ -675,6 +685,13 @@ def update_webhook_status(self, status):
'key': 'ansible/awx' if license_type == 'open' else 'ansible/tower',
'url': self.get_ui_url(),
}
elif self.webhook_service == 'bitbucket_cloud':
data = {
'type': 'build',
'state': statuses[status],
'key': 'ansible/awx' if license_type == 'open' else 'ansible/tower',
'url': self.get_ui_url(),
}
else:
data = {
'state': statuses[status],
Expand Down
1 change: 1 addition & 0 deletions awx/main/tests/functional/test_credential.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def test_default_cred_types():
'aws_secretsmanager_credential',
'azure_kv',
'azure_rm',
'bitbucket_cloud_token',
'bitbucket_dc_token',
'centrify_vault_kv',
'conjur',
Expand Down