用 Django 来响应 Gitlab Webhook

瞳人


发布于 July 14, 2015, 11:36 a.m.

8 个评论

Django Gitlab


利用 Django 来响应 Gitlab 的 Web hooks 请求.

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 HttpResponse
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def 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_kind, project_id, repository 中的 repo_namerepo_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-8
from django.db import models
from 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_exempt
def 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_exempt
def 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 views

urlpatterns = [
    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/.

基本上的实现就这样了. 后续一篇文章会讲一下当你的 do something 太耗时的情况下, 该如何处理. 因为 gitlab 会对 webhook URL 的 POST 请求有一个超时重发机制, 会导致虽然实际上你的耗时较长的 webhook 工作了, 但是 gitlab 以为没有正确完成, 然后不断地重复发送 POST, 直至一个上限例如100次,然后标记为失败. 这个可以在 gitlab 的 background jobs 中找到. 这样对 gitlab 本身以及你的 django webhook 都是不好的.


哎呦, 不错哦!

8 Comments

IT-S_ME- Aug. 14, 2015, 3:39 p.m. | Reply

“后续一篇文章会讲一下当你的 do something 太耗时的情况下, 该如何处理. 因为 gitlab 会对 webhook URL 的 POST 请求有一个超时重发机制, 会导致虽然实际上你的耗时较长的 webhook 工作了, 但是 gitlab 以为没有正确完成, 然后不断地重复发送 POST”
今天遇到了这个额外问题。这个超时时间貌似还是挺短的。请教下你是怎么处理这个问题的?

瞳人 Sept. 23, 2015, 3:43 p.m. | Reply

不好意思. 之前太忙就没有接着更新这个后续博客. 而且多说也没给我推送回复提示. 是利用 celery 来作为任务队列来执行异步任务. 当收到 webhook 的 post 请求时, 把需要执行的任务交给 celery, 然后直接返回 200 的 http response.

IT-S_ME- Oct. 27, 2015, 8:40 p.m. | Reply

非常感谢你的回复,我也是今天才看到多说的通知的.
后来发现在github上有很多关于webhook的程序,也就随便找了一个来用了,虽然不完美,将就下还是可以的

瞳人 Oct. 28, 2015, 5:23 p.m. | Reply

恩恩, 是什么项目呢, 让我也学习一下.
我之前发现多说不稳定, 所以就换成了django 的评论系统.
现在还在完善中.

IT-S_ME- Oct. 28, 2015, 10:38 p.m. | Reply

我用的是这个,很简单的一个python写的工具https://github.com/olipo186/Git-Auto-Deploy

瞳人 Oct. 30, 2015, 8:59 p.m. | Reply

恩恩 好的 谢谢

王万 Sept. 24, 2015, 3:44 p.m. | Reply

真是太感谢了,最近想用 gitlab 和 django 来做自动更新的系统,正好遇到这篇文章。

瞳人 Sept. 29, 2015, 3:46 p.m. | Reply

不用谢啦. 其实除了利用 webhook, 还可以使用 gitlab ci 集成.


Leave a Comment:

博客搜索

友情链接

公告

本博客代码已经公布在 Github 上,欢迎交流指正。

QQ 邮箱对 mailgun 不太友好, 所以使用 QQ 邮箱的评论, 可能会无法及时收到邮件。我会尽快寻找其他解决方案的。

本人现在独自使用 linode vps, 20 美元/月, 感觉压力大, 如果有意一起合租, 可以联系我. 在我的任意一篇文章下面留言即可. 关于使用方式, 现在倾向于使用 docker.