GitLab系列4 GitLab Shell

看了前三期 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

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-shellwhooriginal 变量值

# 打印变量值
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 请求,咱们作了相应的解释,咱们再回头看变量 whooriginal_cmd

  • who : sshd 调用 GitLab Shell 时传入的参数
  • 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
复制代码

Git 钩子为什么物

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 Shell 组件用于处理 GitLab 全部的 git SSH 会话。当用户以 SSH 的方式访问 GitLab 时(例如 git pull/push over ssh),GitLab Shell 组件会作下列事情:

  1. 限制用户使用预约义的 git 命令(git push, git pull 等)
  2. 调用 GitLab Rails 的 API 接口以检查用户是否已受权,以及判断用户经过哪台 Gitaly 服务器访问代码仓库(Gitaly 组件的主要功能是进行与代码仓库相关的操做)
  3. 在 SSH 客户端和 Gitaly 服务器之间来回拷贝数据

当咱们执行 git pull/push over ssh 时,分别发生如下的事情:

  1. git pull over ssh -> gitlab-shell -> 调用 gitlab-rails 的 api 接口以认证用户信息并受权 -> 受权成功或失败 -> 创建 Gitaly 会话
  2. git push over ssh -> gitlab-shell(此时服务器的 git 命令还未被执行) -> 创建 Gitaly 会话 -> 在 Gitaly 服务器执行 gitlab-shell 的 pre-receive 钩子脚本 -> 调用 gitlab-rails 的 api 接口以认证用户信息并受权 -> 受权成功或失败

因为历史缘由 gitlab-shell 组件也包含了容许 GitLab 校验用户 git push 命令的钩子脚本(例如,判断当前用户是否有权限将本地代码变更 push 到某保护分支),这些钩子脚本同时也能触发 GitLab 的事件(好比当用户成功推送代码后触发 CI 流水线启动等)。从目前的架构来看,gitlab-shell 的 git 钩子脚本是属于 Gitaly 组件的,钩子脚本只会运行在 Gitaly 服务器。在 Gitaly 服务器安装 gitlab-shell 组件实属不必,具体看 gitlab.com/gitlab-org/…

附录

参考连接

gitlab-shell 官方仓库

Pro git 2

GitLab系列1 基础功能及架构简介
GitLab系列2 GitLab Workhorse
GitLab系列3 Unicorn

相关文章
相关标签/搜索