Verified Commit 8cd1adfa authored by Huste, Tobias (FWCC) - 111645's avatar Huste, Tobias (FWCC) - 111645
Browse files

global: automatic handling of webhooks in GitLab

parent d3463cd8
Pipeline #8616 passed with stage
in 3 minutes and 8 seconds
......@@ -32,7 +32,7 @@ from werkzeug.local import LocalProxy
from werkzeug.utils import cached_property
from .models import Project
from .utils import iso_utcnow
from .utils import iso_utcnow, parse_timestamp, utcnow
class GitLabAPI(object):
......@@ -135,7 +135,7 @@ class GitLabAPI(object):
for gl_project_id, gl_project in gitlab_projects.items():
active_projects[gl_project_id] = {
'id': gl_project_id,
'full_name': gl_project['name_with_namespace'],
'full_name': gl_project['path_with_namespace'],
'description': gl_project['description'],
}
......@@ -151,7 +151,8 @@ class GitLabAPI(object):
for project in db_projects:
gl_project = gitlab_projects.get(project.gitlab_id)
if gl_project and project.name != gl_project.full_name:
if (gl_project and
project.name != gl_project['path_with_namespace']):
project.name = gl_project.full_name
db.session.add(project)
......@@ -169,3 +170,63 @@ class GitLabAPI(object):
))
self.account.extra_data.changed()
db.session.add(self.account)
def check_sync(self):
"""Check if sync is required based on the last sync date."""
# If no refresh interval is given, refresh every time.
expiration = utcnow()
refresh_td = current_app.config.get('GITLAB_REFRESH_TIMEDELTA')
if refresh_td:
expiration -= refresh_td
last_sync = parse_timestamp(self.account.extra_data['last_sync'])
return last_sync < expiration
def create_hook(self, project_id, project_name):
"""Create project webhook."""
attributes = {
'url': self.webhook_url,
'push_events': 0,
'tag_push_events': 1,
'token': current_app.config['GITLAB_SHARED_SECRET'],
'enable_ssl_verification': 1 if not current_app.config[
'GITLAB_INSECURE_SSL'] else 0,
}
gl_project = self.api.projects.get(project_id)
if gl_project:
try:
# Check, if hook is already installed.
hooks = (h for h in gl_project.hooks.list()
if h.attributes.get('url', '') == attributes['url'])
for h in hooks:
h.delete()
# Recreate the webhook.
hook = gl_project.hooks.create(attributes)
except gitlab.GitlabError:
current_app.logger.error('Could not create webhook.')
finally:
if hook:
Project.enable(
user_id=self.user_id,
gitlab_id=project_id,
name=project_name,
hook=hook.id,
)
return True
return False
def remove_hook(self, project_id, name):
"""Remove webook from GitLab project."""
gl_project = self.api.projects.get(project_id)
if gl_project:
# Check, if hook is installed.
hooks = (h for h in gl_project.hooks.list()
if h.attributes.get('url', '') == self.webhook_url)
for h in hooks:
h.delete()
Project.disable(
user_id=self.user_id,
gitlab_id=project_id,
name=name,
)
return True
return False
......@@ -22,6 +22,7 @@
from __future__ import absolute_import
from copy import deepcopy
from datetime import timedelta
from .handlers import REMOTE_APP
......@@ -31,6 +32,19 @@ GITLAB_BASE_URL = 'https://gitlab.com'
GITLAB_REMOTE_APP = deepcopy(REMOTE_APP)
"""GitLab remote application configuration."""
GITLAB_INSECURE_SSL = False
"""Configure, if the GitLab webhhok verifies the SSL certificate.
Never set this to True in a production environment. It is only useful for
development and integration servers.
"""
GITLAB_REFRESH_TIMEDELTA = timedelta(days=1)
"""Time period after which a GitLab account sync should be initiated."""
GITLAB_SHARED_SECRET = 'CHANGEME'
"""Shared secret between the application and GitLab."""
GITLAB_WEBHOOK_RECEIVER_URL = None
"""URL format to be used when creating a webhook in GitLab.
......
......@@ -202,9 +202,17 @@ class Project(db.Model, Timestamp):
except NoResultFound:
project = cls.create(
user_id=user_id, gitlab_id=gitlab_id, name=name)
project.hook = hook
project.user_id = user_id
return project
project.hook = hook
project.user_id = user_id
return project
@classmethod
def disable(cls, user_id, gitlab_id, name):
"""Disable webhooks for a repository."""
project = cls.get(user_id, gitlab_id=gitlab_id, name=name)
project.hook = None
project.user_id = None
return project
@property
def enabled(self):
......
......@@ -52,7 +52,7 @@
{%- endblock %}
{%- endmacro %}
{%- macro repo_switch(repo, repo_id, size='mini') %}
{%- macro repo_switch(project, project_id, size='mini') %}
{%- block repo_switch scoped %}
<i class="hook-status fa" data-repo-id="{{project_id}}"></i>
{%- set inaccessible = (project and project.user_id and (project.user_id != current_user.id)) %}
......
......@@ -65,7 +65,7 @@ def index():
gitlab.init_account()
db.session.commit()
if request.method == 'POST':
if request.method == 'POST' or gitlab.check_sync():
# When we're in an XHR request, synchronously sync hooks
gitlab.sync(async_hooks=(not request.is_xhr))
db.session.commit()
......@@ -99,4 +99,33 @@ def index():
@login_required
def hook():
"""Install or delete GitLab webhook."""
abort(501)
project_id = request.json['id']
gitlab = GitLabAPI(user_id=current_user.id)
projects = gitlab.account.extra_data['projects']
if project_id not in projects:
abort(404)
if request.method == 'DELETE':
try:
if gitlab.remove_hook(project_id,
projects[project_id]['full_name']):
db.session.commit()
return '', 204
else:
abort(400)
except Exception:
abort(403)
elif request.method == 'POST':
try:
if gitlab.create_hook(project_id,
projects[project_id]['full_name']):
db.session.commit()
return '', 201
else:
abort(400)
except Exception:
abort(403)
else:
abort(400)
......@@ -54,11 +54,11 @@ class GLUser(object):
class GLProjects(object):
"""Class for mocking a GitLab project."""
def __init__(self, id=1234, name='test', name_with_namespace='test/test'):
def __init__(self, id=1234, name='test', path_with_namespace='test/test'):
"""Init project."""
self.id = id
self.name = name
self.name_with_namespace = name_with_namespace
self.path_with_namespace = path_with_namespace
@property
def attributes(self):
......@@ -67,7 +67,7 @@ class GLProjects(object):
id=self.id,
description='Test project description.',
name=self.name,
name_with_namespace=self.name_with_namespace,
path_with_namespace=self.path_with_namespace,
created_at='2018-08-07T18:31:43.564Z',
)
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment