本文转载自http://answ.me/post/gitlab-webhook-with-django/


Django

一个 python web 框架. 请看官方主页 Django.

Gitlab CE

一个开源的代码托管工具. 请看官方主页 Gitlab CE.

Web hooks

项目的 web hooks 可以用来在项目发生变化时绑定一些事件. 它允许你在新的代码被 push 或者新建 了一个 issue 的时候, 来触发一个 URL.

你可以设置 web hooks 来监听特殊事件 (例如 pushes, issues 或者 merge requests). Gitlab 会发送 包含相应数据的 POST请求给设置的 URL.

基本实现思路

其实原理很简单, 我们只要在 Django 中创建一个 view 来响应这个 POST 请求就行了.

基本代码

views.py:

1
2
3
4
5
6
7
8
9
from django.http import HttpResponsefrom django.views.decorators.csrf import csrf_exempt@csrf_exemptdef gitlab_webhook(request):
    if request.method == 'POST':
        # Add your code here
        print 'Do something here'
        return HttpResponse('Done!')
    return HttpResponse('Hehe! Use POST please!')

注意这里的 @csrf_exempt 是为了解除该方法的 Cross Site Request Forgeries 限制.

更安全的实现

如果你的 webhook 使用来重启你的 web 服务, 或者类似我后续博客中将要提到的, 利用 web hook 来触发 docker 的一些重启操作的话, 那么一定得好好验证 web hook 请求来源是否属实.

例如, 有人不间断地发送 POST 请求给你的 webhook URL, 而你又没有任何验证, 就会导致你的 服务一直在不断地重启.

所以我们要验证一些信息. 由于 Gitlab 在向你的 webhook URL 发送 POST 请求的时候, 会设置 request header 以及 request body, 因此我们可以利用这些信息来验证请求来源.

以下是一个 gitlab 的 push event 的 webhook 发送 POST 请求时包含的内容:

  1. 会在 request header 中加入一个字段 'HTTP_X_GITLAB_EVENT':'Push Hook'.

  2. request body 为如下信息:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    {
      "object_kind": "push",
      "before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
      "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
      "ref": "refs/heads/master",
      "user_id": 4,
      "user_name": "John Smith",
      "user_email": "john@example.com",
      "project_id": 15,
      "repository": {
        "name": "Diaspora",
        "url": "git@example.com:mike/diasporadiaspora.git",
        "description": "",
        "homepage": "http://example.com/mike/diaspora", 
        "git_http_url":"http://example.com/mike/diaspora.git",
        "git_ssh_url":"git@example.com:mike/diaspora.git",
        "visibility_level":0
      },
      "commits": [
        {
          "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
          "message": "Update Catalan translation to e38cb41.",
          "timestamp": "2011-12-12T14:27:31+02:00",
          "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
          "author": {
            "name": "Jordi Mallach",
            "email": "jordi@softcatala.org"
          }
        },
        {
          "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
          "message": "fixed readme",
          "timestamp": "2012-01-03T23:36:29+02:00",
          "url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
          "author": {
            "name": "GitLab dev user",
            "email": "gitlabdev@dv6700.(none)"
          }
        }
      ],
      "total_commits_count": 4
    }

主要过程分为两步:

  1. 定义 web hook 的 model, 然后将需要响应的 webhook 对应的信息存入数据库.

  2. 在 view 中, 收到 POST 请求时, 查找数据库中是否有对应的 webhook 信息, 并对其请求内容进行验证. 如果通过验证, 那就执行响应操作(重启服务之类); 如果没有通过验证, 那随便返回一个 200/401/403 之类的 响应就行了.

代码

我这里只演示验证 request body 中的 object_kindproject_idrepository 中的 repo_name 和 repo_url, 以及 request header 中的 HTTP_X_GITLAB_EVENT 信息. 你可以根据你的需求调整. 所以一个 webhook 对应的 model 为:

models.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# coding=utf-8from django.db import modelsfrom django.utils.translation import ugettext as _# Create your models here.class Gitlab_Webhook(models.Model):
    ''' Model for webhook of gitlab    '''
    repo_name = models.CharField(
        verbose_name = _(u'repository name'),
        help_text = _(u' '),
        max_length = 255
    )
    repo_url = models.CharField(
        verbose_name = _(u'repository url'),
        help_text = _(u' '),
        max_length = 255
    )
    object_kind = models.CharField(
        verbose_name = _(u'object_kind value'),
        help_text = _(u'push, tag_push, issue, note'),
        max_length = 255
    )
    project_id = models.IntegerField(
        verbose_name = _(u'project_id value'),
        help_text = _(u'Your project id in gitlab'),
        default = 0
    )
    http_x_gitlab_event = models.CharField(
        verbose_name = _(u'X-Gitlab-Event in request header'),
        help_text = _(u'Push Hook, Tag Push Hook, Issue Hook, Note Hook'),
        max_length = 255
    )

    def __unicode__(self):
        return u'%s %s' % (self.repo_name, self.object_kind)

views.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@csrf_exemptdef gitlab_webhook(request):
    if request.method == 'POST' and request.body:
        http_x_gitlab_event = request.META.get('HTTP_X_GITLAB_EVENT', '')
        json_data = json.loads(request.body)
        object_kind = json_data.get('object_kind', '')
        project_id = json_data.get('project_id', '')
        repo_data = json_data.get('repository', '')
        user_id = json_data.get('user_id', '')
        user_name = json_data.get('user_name', '')
        user_email = json_data.get('user_email', '')
        if repo_data:
            repo_name = repo_data.get('name', '')
            repo_url = repo_data.get('url', '')
            webhook = Gitlab_Webhook.objects.filter(
                repo_name = repo_name,
                repo_url = repo_url,
                object_kind = object_kind,
                project_id = project_id,
                http_x_gitlab_event = http_x_gitlab_event
            ).first()
            if webhook:
                # Add your code here
                print 'User %s %s %s call this webhook' % (user_id, user_name, user_email)
                return HttpResponse('Done!')

    return HttpResponse('Hehe!')

进一步改进

上述方法要求我们使用 django 自带的 admin 后台, 先添加好 webhook 的那5个信息. 但是 对于 project_id 这个参数, 我还没有找到怎么从 gitlab 的 web 界面上找到. 所以这个信息 手动输入比较麻烦. (当然也可以直接删掉不校验这个参数啦) 但是我讨厌逃避这种问题...... 所以那找一种方法来自动添加这个 project_id 好了. 当然还是使用 gitlab 这个 POST 请求提供的信息. 我们的想法是现在 gitlab 的项目里设置一个用于注册的 webhook URL, 然后使用 gitlab 的 Test Hook 来进行 一次注册, 然后再删掉这个 webhook URL, 添加正式使用的 webhook URL. 下面先给出相关代码, 然后讲一下用法.

views.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@csrf_exemptdef gitlab_webhook_register(request):
    if request.method == 'POST' and request.body:
        http_x_gitlab_event = request.META.get('HTTP_X_GITLAB_EVENT', '')
        json_data = json.loads(request.body)
        object_kind = json_data.get('object_kind', '')
        project_id = json_data.get('project_id', '')
        repo_data = json_data.get('repository', '')
        user_id = json_data.get('user_id', '')
        user_name = json_data.get('user_name', '')
        user_email = json_data.get('user_email', '')
        if repo_data:
            repo_name = repo_data.get('name', '')
            repo_url = repo_data.get('url', '')
            print 'repo_name ', repo_name
            print 'repo_url ', repo_url
            print 'object_kind', object_kind
            print 'project_id', project_id
            print 'http_x_gitlab_event',  http_x_gitlab_event
            webhook = Gitlab_Webhook.objects.filter(
                repo_name = repo_name,
                repo_url = repo_url,
                object_kind = object_kind,
                http_x_gitlab_event = http_x_gitlab_event
            ).first()
            print 'Find webook: ', webhook
            if webhook:
                if webhook.project_id == -1:
                    webhook.project_id = project_id
                    webhook.save()
                    content = u'Save webhook: [%s] by user %s, %s, %s' % (
                        webhook, user_id, user_name, user_email)
                    write_log(content)
                    print 'Save webhook: ', webhook
                else:
                    print 'Webhook already exists!'

    return HttpResponse('HeHe!')

urls.py:

1
2
3
4
5
6
from . import viewsurlpatterns = [
    url(r'^gitlab-webhoook/something/$', views.gitlab_webhook, name='gitlab-webhook-something'),
    url(r'^gitlab-webhoook/register/$', views.gitlab_webhook_register, name='gitlab-webhook-register'),]

现在讲一下具体怎么用. 从上面的 urls.py 可以看到, 我们设置了一个 http://test.iwanna.xyz/webhook/gitlab-webhook/register/ 来作为注册使用.

  1. 我们首先利用 django 的 admin 后台添加相应信息, 但是把其 project_id 填成 -1.

  2. 然后在 gitlab 项目中的 webhooks 中首先添加这个 http://test.iwanna.xyz/webhook/gitlab-webhook/register/. 然后 点击 Webhooks 右侧的 Test Hook 按钮. 如果 gitlab 显示 'Hook successfully executed', 并且你看见你的 django admin 后台 中对应项 project_id 已经修改成功, 就可以了.

  3. 删除刚刚添加的用于注册的 webhook URL, 添加正式的 webhook URL http://test.iwanna.xyz/webhook/gitlab-webhook/something/.