当静态博客生成器和Git都是实机部署的时候,直接用post-recieve这个hook来调用生成器是非常方便的事情。但是当Git是用docker部署,或者是Gitlab这样不支持repository hook的解决方案的场景下,就需要用webhook来调用生成器了。本文会介绍以下内容:
- 如何写uwsgi作为响应webhook的backend
- 如何生成token以无密码clone private仓库
本文假设已经按照 archlinux部署gitlab 完成部署。
UWSGI安装
由于处理响应的是python写的程序,所以除了安装uwsgi之外还需要安装python插件:sudo pacman -S uwsgi uwsgi-plugin-python
。
安装完成之后不需要做其他操作,因为我们后续会用systemd service的方式托管,不需要用到uwsgi自己的什么“emperor”模式。
webhook设置
GitLab
解决webhook无法请求本机的问题
默认安装下gitlab是不允许webhook请求localhost的地址的,也就是不能在URL里填127.0.0.1+端口。解决这个问题有两个办法,一是简单粗暴允许所有webhook访问localhost,但是会有安全隐患,所以放在后面说;二是用让nginx监听一个server_name,把server_name填进allowlist里面。
首先来配置nginx,顺便把转发给python的部分也写上。假设我们的repo在收到push之后,会发送一个webhook给http://blogbuilder.internal:25666
,那么nginx的server配置就应该这么写:
1
2
3
4
5
6
7
8
9
server {
listen 127.0.0.1:25666;
server_name blogbuilder.internal;
location / {
include uwsgi_params;
# 我们的uwsgi应用监听这个9091端口
uwsgi_pass 127.0.0.1:9091;
}
}
测试并重载nginx:sudo nginx -t && sudo systemctl reload nginx
。
然后需要配置dns,让dns解析器响应blogbuilder.internal
为127.0.0.1
。这个因使用的DNS软件而异,我用的mosdns就只需要在hosts插件里新增一条记录就行了。配置好之后ping blogbuilder.internal
应该有如下显示:
1
2
3
➜ ping blogbuilder.internal
PING blogbuilder.internal (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.017 ms
最后就是配置GitLab把blogbuilder.internal
放进allowlist。登录GitLab管理员账户之后,点击Configure GitLab,左侧栏点击Settings下面的Network,然后往下拉到Outbound requests:
然后要按Save changes。
给仓库添加webhook
打开一个仓库,左侧边栏最下面有个Settings,里面有个Webhooks,点击之后是这个页面:
URL填写:http://blogbuilder.internal:25666
Trigger勾选Push events,如果有多个branch可以自行设置pattern
拉到最下面,SSL verification
可以取消勾,因为我们是本机http请求,没有ssl。然后点击Add webhook按钮,没有问题的话就会添加成功了。
GitLab这边就设置完了,接下来就是编写响应的部分了。
Python
application
网上关于uwsgi的文章很多,但是都说得有点谜语人。我们用uwsgi的主要原因是为了能够让build和respond异步执行,也就是说能够尽快回应200给GitLab,让博客生成在后面慢慢摸。
那么首先我们就需要用到subprocess来启动独立进程执行博客生成器,另外还需要shlex帮忙拆分shell命令给subprocess。
1
2
3
#!/usr/bin/env python3
import subprocess
import shlex
uwsgi的入口函数是application,需要包含两个东西:1,响应码;2,返回一个list(包含生成的content)。
1
2
3
def application(environ, start_response):
start_response('200',[])
return []
接下来填入调用我们的外部shell script(负责build的琐碎东西)的命令:
1
2
3
4
5
6
7
8
9
def application(environ, start_response):
start_response('200',[])
# 使用env -i来去掉所有的环境变量,主要是避免http_proxy干扰DNS
# 毕竟我们要用本地DNS来解析、HTTP直连本机的GitLab,避免绕路公网转发
cmd = "env -i ./build.sh"
cmds = shlex.split(cmd)
# 用Popen来detach执行shell script,避免阻塞
subprocess.Popen(cmds,start_new_session=True)
return []
但是这样的话是不辨别webhook发送的什么内容就直接触发生成。一般来说还是要判断一下发了什么过来的,这就需要用到environ变量了:
1
2
3
4
5
6
7
8
9
10
def application(environ, start_response):
start_response('200',[])
# keys = environ.keys()
# for key in keys:
# print("{}: {}".format(key, environ[key]))
if environ['REQUEST_METHOD'] == "POST" and environ['HTTP_X_GITLAB_EVENT'] == "Push Hook":
cmd = "env -i ./build.sh"
cmds = shlex.split(cmd)
subprocess.Popen(cmds,start_new_session=True)
return []
如果需要inspect一下environ包含了什么内容,可以把注释掉的那几行拿出来用——建议在真正写执行部分之前自己搞清楚environ的内容,毕竟有时候文档写的跟你真正收到的内容是不一样的。
假设我们把以下内容放在了/opt/autogenblog/application.py
里面:
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python3
import subprocess
import shlex
def application(environ, start_response):
start_response('200',[])
# keys = environ.keys()
# for key in keys:
# print("{}: {}".format(key, environ[key]))
if environ['REQUEST_METHOD'] == "POST" and environ['HTTP_X_GITLAB_EVENT'] == "Push Hook":
cmd = "env -i ./build.sh"
cmds = shlex.split(cmd)
subprocess.Popen(cmds,start_new_session=True)
return []
systemd
那么现在就要写一个systemd服务来启动这个uwsgi了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[Unit]
Description=Auto generate hugo content
After=network.target nss-lookup.target
[Service]
PrivateTmp=true
ProtectSystem=full
ProtectKernelTunables=true
NoNewPrivileges=true
User=blogbuilder
Group=http
ReadWritePaths=/var/www/blog
WorkingDirectory=/opt/autogenblog
ExecStart=/usr/bin/uwsgi --socket 127.0.0.1:9091 --plugin python --wsgi-file /opt/autogenblog/application.py --master --processes 2 --threads 2
KillSignal=SIGQUIT
Restart=always
[Install]
WantedBy=multi-user.target
这些内容需要放到/etc/systemd/system/autogenblog.service
里。它的内容(Service部分)需要慢慢解释:
-
开头四行是安全防护的内容,含义参考systemd.exec(5)。
-
User用blogbuilder,如果还没有这个用户,
sudo useradd --no-user-group --create-home --system blogbuilder
-
Group用Nginx的http组
-
由于开头4行的保护,整个系统除了
/etc
和/tmp
都是不可写入的,但是我们的博客需要写入/var/www/blog
来让nginx对外serve content,所以在ReadWritePaths
指定该路径 -
由于2、3、4点,所以
/var/www/blog
的权属就应该是blogbuilder:http
,权限是755
,请用chown
和chmod
来修正这些权限。1 2 3 4 5
➜ ls /var/www/blog -alh total 36K drwxrwxr-x 3 blogbuilder http 28K Oct 10 14:16 . drwxr-xr-x 7 http http 4.0K Oct 4 14:38 .. drwxr-xr-x 20 blogbuilder http 4.0K Oct 10 14:16 public
-
WorkingDirectory工作目录设置成application.py所在的目录,以便
cmd = "env -i ./build.sh"
能够调用正确的build.sh -
ExecStart=
指定的socket就是我们在nginx的server里指定的uwsgi_pass的值,wsgi-file是我们的application.py的完整路径 -
KillSignal是因为如果不指定的话,uwsgi主进程会等待60秒才强制杀掉所有子进程,这就有点太久了
现在就可以启动并开机启动这个service了sudo systemctl enable --now autogenblog.service
。
build.sh
做完前面的所有东西之后,现在就只差一个uwsgi调用的build.sh需要编写了。但是因为我们的blog仓库是private的(除非你真的开诚布公,没有任何secret,否则不建议把博客源代码都设置成public仓库),所以需要生成GitLab Access Token来让脚本可以不用密码来clone。
Access Token
点开博客仓库,左边最下面的Settings,点Access Token,填Token name,role改成Maintainer(不然会被deny),scope勾上read_repository,然后点create就行。把token抄下来,不然切了页面就没办法再显示了,需要重新生成了。
token的用法是:
如果不是本机直连访问,请务必使用https,否则明文传输很危险。
1
git clone https://用户名:token@git.mutebot.net/用户/仓库名.git
build.sh
根据以上内容就可以编写这个build脚本了。具体内容请替换成自己的实际内容:
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
#!/usr/bin/env bash
set -e
GIT_NAME=你的gitlab用户名
GIT_TOKEN=你的token
GIT_REPO=你的博客repo名字
TMP_GIT_CLONE=/tmp/blog_repo
set +e
# 清理临时目录
rm -rf $TMP_GIT_CLONE
# 这里用http是因为本机直连本机的gitlab
# 主机名也注意改一下,如果你不是git.mutebot.net的用户
# 这里还指定了branch,如果你的branch不叫master请自己改
# 用depth参数指定只clone最新的commit
git clone http://"$GIT_NAME":"$GIT_TOKEN"@git.mutebot.net/"$GIT_NAME"/"$GIT_REPO".git $TMP_GIT_CLONE -b master --depth=1
# 切换到临时目录
pushd $TMP_GIT_CLONE
set -e
# 生成博客内容
hugo
set +e
# 清理nginx的博客文件目录
rm -rf /var/www/blog/*
set -e
# 把新的内容放到nginx目录里
mv public /var/www/blog
# 退出临时目录
popd
set +e
# 清理临时目录
rm -rf $TMP_GIT_CLONE
exit
把这个文件保存到application.py所在的目录,按本文的路径就是/opt/autogenblog/build.sh
。记得加上可执行权限sudo chmod +x build.sh
。
想要测试的话可以用webhook页面拉到最下面有个test,选择push的test就行。用sudo journalctl -u autogenblog --since "1m ago" -f
应该可以看到hugo命令运行的输出以及整个脚本执行的结果。再执行sudo ls -alh /var/www/blog
就能够看到更新时间应该是非常近的。
Tips and Tricks
允许webhook访问任何本地网络的资源
如果整个实例只有自己一个人用的话,直接允许webhook访问本地网络也不会有多大风险。毕竟这个选项带来的风险主要是:有些本地网络的resource并不是开放给其他用户的,但是如果开启了这个选项,其他用户就可以通过用webhook来非法访问这些resource。
登录GitLab管理员账户之后,点击Configure GitLab,左侧栏点击Settings下面的Network,然后往下拉到Outbound requests,把webhooks那个勾勾上,按保存就可以了。