1. 程式人生 > >jenkins 構建 job 並獲取其狀態的實現

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

[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