jenkins 構建 job 並獲取其狀態的實現
阿新 • • 發佈:2020-10-16
[TOC]
leoninew 原創,轉載請註明來自[部落格園](https://www.cnblogs.com/leoninew/p/13825992.html)
## BACKGROUND
使用 jenkins rest api 觸發的 job 會先進入任務佇列,然後非同步執行,而無法直接獲取到被觸發的 job 構建記錄編號。雖然 job 的描述資訊中 lastBuild 欄位告知了最後的構建記錄,但無論是先獲取 lastBuild,自增其編號作為下次構建 id,還是請求內等待 lastBuild 更新作為構建記錄的做法,都存在若干問題:
1. 由於構建任務延遲觸發,先觸發 job 構建再緊接著獲取 lastBuild 的多數情況下將返回歷史而非當前的構建記錄,不可行。
2. 以 lastBuild 編號自增作為下次 job 構建編號的做法不可靠,部分機制如外掛可以修改自增步進,見 [Changing Jenkins build number](https://stackoverflow.com/questions/19645430/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)
3. 在多個呼叫方同時進行 job 構建時,無法判斷誰的構建先觸發,出現後續的狀態/日誌錯位。
## INVESTIGATION I
雖然 jenkins 在觸發 job 構建的請求中僅返回了 201(No content),但 jenkins 提供了內建佇列查詢介面。另一方面閱讀網頁和檢視 jenkins sdk 的 python 版本 [python-jenkins](https://opendev.org/jjb/python-jenkins) 實現後,得知 jenkins 的 job 構建介面有以下實現細節,其中第 1、3 種情況下,返回的響應會攜帶 Location 欄位,攜帶了佇列編號。
1. 無參:POST /job/:job-name/build,無 Content-Type 要求
```bash
$ 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)
```
2. 有參:POST /job/:job-name/build,要求表單格式(application/x-www-form-urlencoded),請求訊息體有特殊格式要求
* 以 name+value 鍵值對集合作為請求引數,再進行序列化,形如 `{"parameter":[{"name":"branch","value":"test"}]}`
* 將請求引數轉義,以表單格式(application/x-www-form-urlencoded)傳送,鍵為固定值 `json`。
以 curl 形式呼叫的命令為
```bash
$ 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 形式呼叫的命令為
```bash
$ 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 及引數變化的情況進行觀察。
```bash
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
![](https://img2020.cnblogs.com/blog/1800602/202010/1800602-20201016131653461-1889741832.png)
可以看到分別返回了以下 Location
* http://localhost:8080/queue/item/128/
* http://localhost:8080/queue/item/129/
* http://localhost:8080/queue/item/128/
* http://localhost:8080/queue/item/129/
* http://localhost:8080/queue/item/130/
可以看到最後的引數變化生成了兩條佇列記錄,目前為止我們有以下結論:
#### 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 觸發的構建都會入隊
> 4. 檢查 job 資訊,無法知曉 lastBuild 或 nextBuildNumber 的歸屬
```mermaid
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](https://stackoverflow.com/questions/28311030/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 狀態等。
```mermaid
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 的執行結果如下:
![](https://img2020.cnblogs.com/blog/1800602/202010/1800602-20201016131705453-485015038.png)
> 拿到的 job 即使已經出隊,也可能沒有第1時間被執行,至此狀態欄位 Result 可能為空,仍然需要重新整理其任務狀態的後續工作。
## FUTHER MORE
雖然可以使用輪詢獲取到構建編號,但因不是原生手段可能有少許延遲。jenkins 提供了名為 JENKINS-CLI 的互動工具,見管理頁 "Tools and Actions" 下的 Jenkins CLI 項,我的本地地址是 http://localhost:8080/cli/
```bash
$ 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"
```bash
$ 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 形式呼叫並使用抓包工具檢查。
![](https://img2020.cnblogs.com/blog/1800602/202010/1800602-20201016131710381-1480292448.png)
發現請求並未使用其約定的地址 /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 原創,轉載請註明來自[部落格園](https://www.cnblogs.com/leoninew/p/138259