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