看了前三期 GitLab 相关解析的读者(伪装有读者)
nginx
都会发现我总会贴 GitLab 的架构图。至此咱们将 GitLab 如何处理 HTTP/HTTPS 请求的过程解释了“一半”:用户请求从 HTTP/HTTPS 入口进来,经由 nginx 到达 gitlab-workhorse ,若是能处理的请求本身优先处理,不能处理的请求再交给 unicorn ,unicorn 能够认为是一层外壳,保障了请求能高效地调度与处理,而真正处理请求的内核主要仍是看 GitLab Rails ,即 gitlab-ce ,请求处理的“另外一半”就交给 gitlab-ce 去处理了git
本期我并不打算介绍 GitLab Rails ,由于这个项目实在太庞大和复杂了(固然也能够简单地当作是一个须要持久化数据库 PostgreSQL 和分布式缓存 Redis 支撑的 web 应用),彻底能够另起一个专题进行介绍。并且本系列打算尽量介绍 GitLab 每个组件的功能(而不是介绍开源项目的代码结构等),刚才也说了 GitLab Rails 至关于 GitLab 的真内核,大部分用户请求的处理逻辑都在这里实现了(有个打算是,将来将结合某些业务场景对 gitlab-ce 作源码跟踪及分析)web
那本期介绍什么呢?别忘了前几期都是讲 http/https 的路由状况,那 ssh 的呢?从架构图上看,ssh 入口的第一站便是 GitLab Shell。本节主要讲解理解 GitLab-Shell 运做原理的预备知识算法
一谈到 Shell,咱们可能会联想到相似 bash 或 zsh 这样的命令行终端,某种程度上 GitLab Shell 也能够被当作是一系列预约义命令的集大成者,但也不止是这样shell
还记得以前第一期的时候贴的这张解释 SSH 利用对称加密算法登陆登陆服务器的流程吗?回顾一下:其实整个过程主要使用了公钥加密、私钥解密的对称加密算法:用户将本身本机的 SSH 公钥上传至远程服务器上(远程服务器的 $HOME/.ssh/authorized_keys
文件将保存用户上传的公钥,通常状况直接追加到文件末尾便可)登陆的时候远程服务器向用户发送一段随机序列,用户用本身本机的 SSH 私钥加密后发回远程服务器,远程服务器用事先储存的公钥进行解密,若是成功就证实用户是可信的,容许你登陆而且再也不要求密码数据库
那当咱们在本机执行 git-over-ssh 操做的时候,难道就能登陆到服务器的 bash 终端搞事情吗?显然是不可能的:服务器不可能让咱们用户登陆终端执行一切命令,除非服务器管理人员想让用户体验 rm -rf /*
删库跑路的感受api
所以 GitLab 使用了 SSH 的一个特性:经过 authorized_keys 指定登陆后要执行的命令。以前登陆的时候远程服务器是直接将用户公钥追加到 $HOME/.ssh/authorized_keys
文件末尾缓存
而如今做为服务器管理人员,你确定是不但愿用户登陆到你服务器的终端乱搞的,因此思路就是仅容许用户执行管理员所指定的 shell 命令,达到安全控制的做用,以下图所示安全
上图主要是说,当咱们在 $HOME/.ssh/authorized_keys
末尾追加以下格式的内容:bash
# command="./cmd ssh-rsa <my-rsa-key>"
command="/home/git/gitlab-shell/bin/gitlab-shell key-10",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa <my-rsa-key>
复制代码
用户就没办法用 ssh 登陆到服务器终端搞事情了,用户登陆后只能执行这条命令,执行完毕就退出
/home/git/gitlab-shell/bin/gitlab-shell key-10
复制代码
这就是为何 GitLab Shell 如此命名,其实仍是很形象的:仅容许你在服务器执行 GitLab Shell ,别的 Shell 命令,好比 rm -rf /*
一把梭的用户少给我乱来,其中这个 key-10
就是 /home/git/gitlab-shell/bin/gitlab-shell
命令的执行参数 ARGV
,表示的固然是用户的密钥标识
接下来咱们来看看 /home/git/gitlab-shell/bin/gitlab-shell
的代码
注意,咱们还能用 $SSH_ORIGINAL_COMMAND
变量取到客户端发来的命令,如今作个实验:打印出 /home/git/gitlab-shell/bin/gitlab-shell
的 who
和 original
变量值
# 打印变量值
File.write("/tmp/git_original_cmd", original_cmd)
File.write("/tmp/git_who", who)
复制代码
当我在本地分别执行 git push
| git fetch
| git pull
| git clone
的时候,打印的值分别是:
// action => original_cmd who
git push => git-receive-pack 'BradeHyj/ToyProject.git' key-11
git fetch => git-upload-pack 'BradeHyj/ToyProject.git' key-11
git pull => git-upload-pack 'BradeHyj/ToyProject.git' key-11
git clone => git-upload-pack 'BradeHyj/ToyProject.git' key-11
复制代码
git-receive-pack - Receive what is pushed into the repository
git-upload-pack - Send objects packed back to git-fetch-pack
上述说明了什么?当咱们在本地执行 git push 操做的时候,包含了登陆服务器执行命令的操做(并且这个登陆操做后面是带 gitlab-shell 所能识别的命令),写具体点就是:
# git push
ssh user@host:port git-receive-pack 'BradeHyj/ToyProject.git'
复制代码
以上,针对 GitLab Shell 是如何接收到客户端 ssh 请求,咱们作了相应的解释,咱们再回头看变量 who
和 original_cmd
:
$SSH_ORIGINAL_COMMAND
变量,取到后即移除感兴趣的能够深刻研究 Pro git 2 关于 ssh 智能传输协议的介绍,具体以下:git-scm.com/book/zh/v1/…
凡是通过 gitlab-shell 执行的 git 命令,在真正执行前都会进行校验权限的操做,具体以下代码所示
# lib/gitlab-shell.rb
...
def verify_access
status = api.check_access(@git_access, nil, @repo_name, @who || @gl_id, '_any', GL_PROTOCOL)
raise AccessDeniedError, status.message unless status.allowed?
status
end
复制代码
GitLab Shell 的处理逻辑是依赖 git 钩子脚本的。GitLab 服务器端存储的全部代码仓库的 hooks
文件夹都是连接到 /home/git/gitlab-shell/hooks
中的,因此理解 gitlab-shell 钩子脚本的执行逻辑很是重要
此处引用 Pro Git 2 对服务端钩子的内容介绍:git-scm.com/book/zh/v2/…
钩子都被存储在 Git 目录下的 hooks 子目录中。 也即绝大部分项目中的 .git/hooks 。 当你用 git init 初始化一个新版本库时,Git 默认会在这个目录中放置一些示例脚本。这些脚本除了自己能够被调用外,它们还透露了被触发时所传入的参数。 全部的示例都是 shell 脚本,其中一些还混杂了 Perl 代码,不过,任何正确命名的可执行脚本均可以正常使用 —— 你能够用 Ruby 或 Python,或其它语言编写它们。 这些示例的名字都是以 .sample 结尾,若是你想启用它们,得先移除这个后缀。
把一个正确命名且可执行的文件放入 Git 目录下的 hooks 子目录中,便可激活该钩子脚本。 这样一来,它就能被 Git 调用
做为系统管理员,你可使用若干服务器端的钩子对项目强制执行各类类型的策略。这些钩子脚本在推送到服务器以前和以后运行。推送到服务器前运行的钩子能够在任什么时候候以非零值退出,拒绝推送并给客户端返回错误消息,还能够依你所想设置足够复杂的推送策略。
pre-receive
: 处理来自客户端的推送操做时,最早被调用的脚本是 pre-receive
。 它从标准输入获取一系列被推送的引用。若是它以非零值退出,全部的推送内容都不会被接受。 你能够用这个钩子阻止对引用进行非快进(non-fast-forward)的更新,或者对该推送所修改的全部引用和文件进行访问控制。update
: update
脚本和 pre-receive
脚本十分相似,不一样之处在于它会为每个准备更新的分支各运行一次。 假如推送者同时向多个分支推送内容,pre-receive
只运行一次,相比之下 update
则会为每个被推送的分支各运行一次。 它不会从标准输入读取内容,而是接受三个参数:引用的名字(分支),推送前的引用指向的内容的 SHA-1 值,以及用户准备推送的内容的 SHA-1 值。 若是 update
脚本以非零值退出,只有相应的那一个引用会被拒绝;其他的依然会被更新。post-receive
: post-receive
挂钩在整个过程完结之后运行,能够用来更新其余系统服务或者通知用户。 它接受与 pre-receive
相同的标准输入数据。 它的用途包括给某个邮件列表发信,通知持续集成(continous integration)的服务器,或者更新问题追踪系统(ticket-tracking system) —— 甚至能够经过分析提交信息来决定某个问题(ticket)是否应该被开启,修改或者关闭。 该脚本没法终止推送进程,不过客户端在它结束运行以前将保持链接状态,因此若是你想作其余操做需谨慎使用它,由于它将耗费你很长的一段时间。我们先看看 gitlab-shell 的pre-receive
的逻辑(如下示例代码均在 $gitlab-shell/hooks
文件夹里)
能够看到在对服务器仓库进行推送(git-receive-pack)以前得先执行用户权限认证及受权等准备工做
post-receive
同理,与 pre-receive
的区别主要是执行逻辑不一样
对于服务器端仓库操做的任何 git 命令执行完成后,都要调用 GitLab Rails 的 post_receive
接口处理后续逻辑
GitLab Shell 组件用于处理 GitLab 全部的 git SSH 会话。当用户以 SSH 的方式访问 GitLab 时(例如 git pull/push over ssh),GitLab Shell 组件会作下列事情:
当咱们执行 git pull/push over ssh 时,分别发生如下的事情:
因为历史缘由 gitlab-shell 组件也包含了容许 GitLab 校验用户 git push 命令的钩子脚本(例如,判断当前用户是否有权限将本地代码变更 push 到某保护分支),这些钩子脚本同时也能触发 GitLab 的事件(好比当用户成功推送代码后触发 CI 流水线启动等)。从目前的架构来看,gitlab-shell 的 git 钩子脚本是属于 Gitaly 组件的,钩子脚本只会运行在 Gitaly 服务器。在 Gitaly 服务器安装 gitlab-shell 组件实属不必,具体看 gitlab.com/gitlab-org/…
参考连接
GitLab系列1 基础功能及架构简介
GitLab系列2 GitLab Workhorse
GitLab系列3 Unicorn