jenkins 構建 job 並獲取其狀態的實現

leoninew發表於2020-10-16

leoninew 原創,轉載請註明來自部落格園

BACKGROUND

使用 jenkins rest api 觸發的 job 會先進入任務佇列,然後非同步執行,而無法直接獲取到被觸發的 job 構建記錄編號。雖然 job 的描述資訊中 lastBuild 欄位告知了最後的構建記錄,但無論是先獲取 lastBuild,自增其編號作為下次構建 id,還是請求內等待 lastBuild 更新作為構建記錄的做法,都存在若干問題:

  1. 由於構建任務延遲觸發,先觸發 job 構建再緊接著獲取 lastBuild 的多數情況下將返回歷史而非當前的構建記錄,不可行。
  2. 以 lastBuild 編號自增作為下次 job 構建編號的做法不可靠,部分機制如外掛可以修改自增步進,見 Changing Jenkins build number: If you have access to the script console (Manage Jenkins -> Script Console), then you can do this following:

Jenkins.instance.getItemByFullName("YourJobName").updateNextBuildNumber(45)

  1. 在多個呼叫方同時進行 job 構建時,無法判斷誰的構建先觸發,出現後續的狀態/日誌錯位。

INVESTIGATION I

雖然 jenkins 在觸發 job 構建的請求中僅返回了 201(No content),但 jenkins 提供了內建佇列查詢介面。另一方面閱讀網頁和檢視 jenkins sdk 的 python 版本 python-jenkins 實現後,得知 jenkins 的 job 構建介面有以下實現細節,其中第 1、3 種情況下,返回的響應會攜帶 Location 欄位,攜帶了佇列編號。

  1. 無參:POST /job/:job-name/build,無 Content-Type 要求
$ curl -i -X POST http://localhost:8080/job/demo/build  -H 'Authorization: Basic YWRtaW46MTFiNDY0NGYwMDRkYWM3MWU0YjI4ODMyNmQwZWUwNmFhMw=='
HTTP/1.1 201 Created
Date: Wed, 14 Oct 2020 10:14:36 GMT
X-Content-Type-Options: nosniff
Location: http://localhost:8080/queue/item/110/
Content-Length: 0
Server: Jetty(9.4.30.v20200611)
  1. 有參:POST /job/:job-name/build,要求表單格式(application/x-www-form-urlencoded),請求訊息體有特殊格式要求
  • 以 name+value 鍵值對集合作為請求引數,再進行序列化,形如 {"parameter":[{"name":"branch","value":"test"}]}
  • 將請求引數轉義,以表單格式(application/x-www-form-urlencoded)傳送,鍵為固定值 json

以 curl 形式呼叫的命令為

$ curl -i http://localhost:8080/job/rdc-pipline/build -H 'Authorization: Basic YWRtaW46MTFiNDY0NGYwMDRkYWM3MWU0YjI4ODMyNmQwZWUwNmFhMw==' -d "json=%7B%22parameter%22%3A%5B%7B%22name%22%3A%22branch%22%2C%22value%22%3A%22test%22%7D%5D%7D"
HTTP/1.1 302 Found
Date: Wed, 14 Oct 2020 09:23:36 GMT
X-Content-Type-Options: nosniff
Location: http://localhost:8080/job/rdc-pipline/
Content-Length: 0
Server: Jetty(9.4.30.v20200611)

貼出 fiddler 捕獲結果

POST http://localhost:8080/job/rdc-pipline/build HTTP/1.1
Host: localhost:8080
User-Agent: python-requests/2.24.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Jenkins-Crumb: d5a1f46c7e02e9633a1d73741a264fa98bc3729e1e4ebdb4974f2a5b4004afb3
Cookie: JSESSIONID.0e0c708f=node018835f7lya5y821r3looclhze104.node0
Content-Length: 93
Authorization: Basic YWRtaW46MTFiNDY0NGYwMDRkYWM3MWU0YjI4ODMyNmQwZWUwNmFhMw==
Content-Type: application/x-www-form-urlencoded

json=%7B%22parameter%22%3A%5B%7B%22name%22%3A%22branch%22%2C%22value%22%3A%22test%22%7D%5D%7D

HTTP/1.1 201 Created
Date: Wed, 14 Oct 2020 10:17:18 GMT
X-Content-Type-Options: nosniff
Location: http://localhost:8080/job/rdc-pipline/
Content-Length: 0
Server: Jetty(9.4.30.v20200611)
  • 有參:POST /job/:job-name/buildWithParameters?...,無 Content-Type 要求,引數拼接到 QueryString 中,該形式對引數格式沒有示例。

以 curl 形式呼叫的命令為

$ curl -i -X POST http://localhost:8080/job/rdc-pipline/buildWithParameters?branch=A  -H 'Authorization: Basic YWRtaW46MTFiNDY0NGYwMDRkYWM3MWU0YjI4ODMyNmQwZWUwNmFhMw=='
HTTP/1.1 201 Created
Date: Wed, 14 Oct 2020 09:26:09 GMT
X-Content-Type-Options: nosniff
Location: http://localhost:8080/queue/item/98/
Content-Length: 0
Server: Jetty(9.4.30.v20200611)

貼出 fiddler 捕獲結果

POST http://localhost:8080/job/rdc-pipline/buildWithParameters?branch=A HTTP/1.1
Authorization: basic YWRtaW46MTFiNDY0NGYwMDRkYWM3MWU0YjI4ODMyNmQwZWUwNmFhMw==
User-Agent: PostmanRuntime/7.26.5
Accept: */*
Postman-Token: 41dda8ba-a376-44f5-b3e5-f59e8fa2fca7
Host: localhost:8080
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: JSESSIONID.0e0c708f=node06gr4lsp9c2wg2dg1vlgm9er496.node0
Content-Length: 0

HTTP/1.1 201 Created
Date: Wed, 14 Oct 2020 10:23:11 GMT
X-Content-Type-Options: nosniff
Location: http://localhost:8080/queue/item/118/
Content-Length: 0
Server: Jetty(9.4.30.v20200611)

對比和測試兩種有參請求,有如下區別

URL 狀態碼 其他
/job/:job-name/buildWithParameters 201 Location 攜帶佇列編號,形如 http://localhost:8080/queue/item/118/
/job/:job-name/build 201 或 302,規律未知 Location 僅為 Job 地址,形如 http://localhost:8080/job/rdc-pipline/

同時觀察第一種方式 jenkins 返回的響應,可以看到出現了相同的佇列編號,這表示同時提交的構建任務不會重複入隊。編排一系列請求如下,以對入隊不同 job 及引數變化的情況進行觀察。

curl -i -X POST http://localhost:8080/job/demo/build -H 'Authorization: Basic YWRtaW46MTFiNDY0NGYwMDRkYWM3MWU0YjI4ODMyNmQwZWUwNmFhMw=='
curl -i -X POST http://localhost:8080/job/rdc-pipline/buildWithParameters?branch=A  -H 'Authorization: Basic YWRtaW46MTFiNDY0NGYwMDRkYWM3MWU0YjI4ODMyNmQwZWUwNmFhMw=='
curl -i -X POST http://localhost:8080/job/demo/build  -H 'Authorization: Basic YWRtaW46MTFiNDY0NGYwMDRkYWM3MWU0YjI4ODMyNmQwZWUwNmFhMw=='
curl -i -X POST http://localhost:8080/job/rdc-pipline/buildWithParameters?branch=A  -H 'Authorization: Basic YWRtaW46MTFiNDY0NGYwMDRkYWM3MWU0YjI4ODMyNmQwZWUwNmFhMw=='
curl -i -X POST http://localhost:8080/job/rdc-pipline/buildWithParameters?branch=B  -H 'Authorization: Basic YWRtaW46MTFiNDY0NGYwMDRkYWM3MWU0YjI4ODMyNmQwZWUwNmFhMw=='

5條 curl 命令執行以下操作

  1. 觸發名為 demo 的 job 構建
  2. 觸發名為 rdc-pipline 的 job 構建,使用引數 branch=A
  3. 觸發名為 demo 的 job 構建
  4. 觸發名為 rdc-pipline 的 job 構建,使用引數 branch=A
  5. 觸發名為 rdc-pipline 的 job 構建,變更引數,使用引數 branch=B

可以看到分別返回了以下 Location

可以看到最後的引數變化生成了兩條佇列記錄,目前為止我們有以下結論:

1. 連續觸發的相同 job 構建不會重複入隊

job 構建的成本因內內容不同差異很大,但觸發 job 構建的成本很小,我們可以輕易地提交大量構建請求,基於以下認知可以理解 jenkins 不進行重複構建的意義:

  1. 雖然程式碼可能觸發前後的短時間內變化,但這是小概率事件;
  2. 常規情況下我們並不需要獲取多份各自獨立的編譯結果,將請求入隊以節流方式構建是可行的;

所以 jenkins 只會為短時間內重複觸發的 job 構建一次。

2. 連續觸發的不同 job 構建會各自入隊

雖然 jenkins 不會重複構建相同 job,但在多個 job 同時觸發構建的時候,執行構建仍是必須的,入隊只是前置。

為什麼 jenkins 不在佇列為空、資源可用的情況下避免入隊環節直接構建?這裡沒有深入調查。

3. 引數變動的相同 job 構建將分別入隊

雖然有避免重複構建相同 job 的必要,但是當引數變化時,構建仍是必須的,否則就丟失了請求,這是不能容忍的。

目前所用 sdk 只實現了基於 /job/:job-name/build 的構建介面,因入隊後佇列編號的意義重大,後續的討論基於實現和使用 /job/:job-name/buildWithParameters 之上。

4. 允許同時觸發構建將有資料錯亂的可能

雖然引數相同的構建會被 jenkins 以排重形式處理,但引數不同時,沒有區分策略可言

  1. user1 和 user2 準備發起相同名稱但引數不同的 job 構建;
  2. 無論當前的構建佇列是否為空,user1 和 user2 觸發的構建都會入隊
  3. 檢查 job 資訊,無法知曉 lastBuild 或 nextBuildNumber 的歸屬
sequenceDiagram participant user1 participant user2 participant jenkins user1-->>jenkins: {...} user2-->>jenkins: {...} jenkins->>jenkins: enqueue and dequeue user1->>jenkins: which job? user2->>jenkins: which job?

5. 分散式鎖強制使得入隊或構建觸發序列化不可行

  1. 強制使構建過程式列化,在計算資源及時間成本上不可接受;
  2. 強制使入隊序列化,意味著 jenkins 的排重機制不生效,將出現構建過程式列化;

這類做法違背了 jenkins 的設計意圖,加劇了伺服器負載,使得響應時間延長,有導致伺服器不可用的風險。

INVESTIGATION II

看起來無法把希望寄託於目前 sdk 提供的 job 資訊中 lastBuild 資訊上,應在佇列上繼續調查。閱讀到 Check Jenkins job status after triggering a build remotely 後,找到可行方法,其步驟如下:

  1. 提交 job 構建請求到地址 /job/:job-name/buildWithParameters ,並解析響應中 Location 欄位得到佇列編號
  2. 從 /queue/item/:queue-id/api/json 檢查佇列資訊,此時 executable 可能為空
  3. 間斷髮起輪詢,直到返回的 job 攜帶非空的 executable
  4. 使用 job.executable.number 作為構建編號,從 /job/:job-name/:job-id/api/json 獲取到 job 狀態等。
sequenceDiagram participant user1 participant user2 participant jenkins user1->>+jenkins: {...} jenkins->>-user1: /queue/item/1 user2->>+jenkins: {...} jenkins->>-user2: /queue/item/2 jenkins->>jenkins: enqueue and dequeue user1->>+jenkins: which job for /queue/item/1? jenkins->>-user1: /job/101 user2->>+jenkins: which job for /queue/item/2? jenkins->>-user2: /job/102

LINQPad 的執行結果如下:

拿到的 job 即使已經出隊,也可能沒有第1時間被執行,至此狀態欄位 Result 可能為空,仍然需要重新整理其任務狀態的後續工作。

FUTHER MORE

雖然可以使用輪詢獲取到構建編號,但因不是原生手段可能有少許延遲。jenkins 提供了名為 JENKINS-CLI 的互動工具,見管理頁 "Tools and Actions" 下的 Jenkins CLI 項,我的本地地址是 http://localhost:8080/cli/

$ java -jar jenkins-cli.jar -s http://localhost:8080/ -webSocket -auth admin:admin build --help
java -jar jenkins-cli.jar build JOB [-c] [-f] [-p] [-r N] [-s] [-v] [-w]
-w  : Wait until the start of the command (default: false)

其中 job 構建命令支援 -w 引數等待構建觸發

If you use the -w option, the command will not return until the build starts and then it will print "Started build #N"

$ java -jar jenkins-cli.jar -s http://localhost:8080/ -webSocket -auth admin:admin build demo

$ java -jar jenkins-cli.jar -s http://localhost:8080/ -webSocket -auth admin:admin build demo -w
Started demo #53 

可以看到返回了 job 編號,然而以 http 形式呼叫並使用抓包工具檢查。

發現請求並未使用其約定的地址 /job/:job-name/build 或 /job/:job-name/buildWithParameters,而是向地址 /cli?remoting=false 發起了請求,正文應是預先約定的特定格式

c
00000007000005build
b
00000006000004demo
9
00000004000002-w
a
00000005020003GBK
c
00000007010005en_US
5
0000000003

在遠期開發中,該 sdk 值得深入挖掘。

SUMMARY

這裡梳理了本人調查和使用 jenkins rest api 構建 job 並獲取狀態的過程,並試用了 jenkins cli 發現其並未呼叫常規 endpoints。到目前為止,jenkins 的 job 狀態和日誌查詢已沒有巨大的障礙,但許多實現路徑有著各種差別,另行討論。

leoninew 原創,轉載請註明來自部落格園

相關文章