jenkins 构建 job 并获取其状态的实现

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 //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: //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 //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: //localhost:8080/job/rdc-pipline/
Content-Length: 0
Server: Jetty(9.4.30.v20200611)

贴出 fiddler 捕获结果

POST //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: //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 //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: //localhost:8080/queue/item/98/
Content-Length: 0
Server: Jetty(9.4.30.v20200611)

贴出 fiddler 捕获结果

POST //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: //localhost:8080/queue/item/118/
Content-Length: 0
Server: Jetty(9.4.30.v20200611)

对比和测试两种有参请求,有如下区别

URL 状态码 其他
/job/:job-name/buildWithParameters 201 Location 携带队列编号,形如 //localhost:8080/queue/item/118/
/job/:job-name/build 201 或 302,规律未知 Location 仅为 Job 地址,形如 //localhost:8080/job/rdc-pipline/

同时观察第一种方式 jenkins 返回的响应,可以看到出现了相同的队列编号,这表示同时提交的构建任务不会重复入队。编排一系列请求如下,以对入队不同 job 及参数变化的情况进行观察。

curl -i -X POST //localhost:8080/job/demo/build -H 'Authorization: Basic YWRtaW46MTFiNDY0NGYwMDRkYWM3MWU0YjI4ODMyNmQwZWUwNmFhMw=='
curl -i -X POST //localhost:8080/job/rdc-pipline/buildWithParameters?branch=A  -H 'Authorization: Basic YWRtaW46MTFiNDY0NGYwMDRkYWM3MWU0YjI4ODMyNmQwZWUwNmFhMw=='
curl -i -X POST //localhost:8080/job/demo/build  -H 'Authorization: Basic YWRtaW46MTFiNDY0NGYwMDRkYWM3MWU0YjI4ODMyNmQwZWUwNmFhMw=='
curl -i -X POST //localhost:8080/job/rdc-pipline/buildWithParameters?branch=A  -H 'Authorization: Basic YWRtaW46MTFiNDY0NGYwMDRkYWM3MWU0YjI4ODMyNmQwZWUwNmFhMw=='
curl -i -X POST //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 项,我的本地地址是 //localhost:8080/cli/

$ java -jar jenkins-cli.jar -s //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 //localhost:8080/ -webSocket -auth admin:admin build demo

$ java -jar jenkins-cli.jar -s //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 原创,转载请注明来自博客园

Tags: