DevOps-7:Jenkins API介绍

Posted 北亮bl

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了DevOps-7:Jenkins API介绍相关的知识,希望对你有一定的参考价值。

前言:

为什么要使用Jenkins的API?
我在使用Jenkins的过程中,觉得Jenkins的UI还是有不少问题的:

  • UI性能差,尤其是有一些任务在构建中时,UI非常卡顿,等个十来秒都正常,极端时甚至会崩溃;
  • 权限管理功能薄弱,虽然有Role-based Authorization Strategy类似的插件提供了角色管理,但是还是不太好用;
  • 一些需要人工介入的能力扩展不太方便,比如发布前需要比对数据库表结构并支持人工忽略时,就不好扩展。

如上,因此我还是比较推荐自行开发一个发布系统,自行实现权限能力和一些扩展,通过调用Jenkins的构建API和状态API完成项目的发布和管理操作,包括历史和审计相关能力。


API认证方式说明

Jenkins的API认证支持两种方式,

  • 第一种是使用账号密码进行认证,这种请求会报错:Error 403 No valid crumb was included in the request,需要在所有请求之前先获取crumb,再把crumb添加在后续请求的Header里,参考:https://stackoverflow.com/questions/44711696/jenkins-403-no-valid-crumb-was-included-in-the-request
  • 第二种是在Jenkins生成一个Token,在API请求里使用这个Token代替密码,这种不会存在crumb问题,生成用户Token的方法,在Jenkins首页左侧->People->对应用户->Configure->Add new Token

官方API文档介绍

  • 安装完成Jenkins之后,直接访问 http://你的Jenkins域名/api 就可以看到API的介绍了,这里会有创建Job和复制Job等API介绍。
  • 进入具体的某个job,在url后面直接添加 /api,就可以看到这个job的API介绍,如 http://你的Jenkins域名/job/具体job名/api 这里会有删除Job、启动Job构建、禁用/启用Job等API介绍。

Java调用API说明

网上也有一些封装好的Maven库可以使用,我都是直接用feign去调用,下面就以feign的代码进行举例说明:

feign类基础定义

  • 定义一个配置类,返回Jenkins的认证信息(建议使用Jenkins用户token方案)
class MultipartSupportConfig 
    @Value("$jenkins.username:") // yml里配置的jenkins用户名
    private String jenkinsUser;
    @Value("$jenkins.password:") // yml里配置的jenkins用户token
    private String jenkinsPassword;
    
    @Bean
    public Contract feignContract() 
        return new Contract.Default();
    

    @Bean
    public BasicAuthRequestInterceptor basicAuthorizationInterceptor() 
        return new BasicAuthRequestInterceptor(jenkinsUser, jenkinsPassword);
    

  • 定义feignClient类,指向上面的配置类:
@FeignClient(value = "jenkins", url = "$jenkins.worker.url", configuration = JenkinsFeign.MultipartSupportConfig.class)
public interface JenkinsFeign 
    // 具体的jenkins API方法定义

常用Jenkins API方法声明

注:通过feign调用Jenkins,不能用RequestMapping注解,不太记得为啥了,当时是出了很多问题,包括JobName的拼接之类,所以最后采用了RequestLine注解实现,你也可以再使用RequestMapping验证看看。
以下是API方法声明:

  • crumb头信息获取接口
    如果你直接使用Jenkins用户名和密码作为API请求认证方式,那么在所有API调用的Header里,都要有crumb值,这个接口就是用于获取crumb值的(注:不推荐使用)
// 直接请求后续接口会报错: Error 403 No valid crumb was included in the request
// 这个方法就是获取crumb数据的,再在后续请求头里添加 Jenkins-Crumb: xxx
@RequestLine("GET /crumbIssuer/api/json")
String getCrumb();
  • 启动Job构建接口,加入队列
    此接口用于启动某个job的构建,启动成功后,会在响应header里,返回加入到Jenkins的队列序号信息。
    后续需要根据这个队列序号,去查找真正的构建任务编号数据。
    注1:如果该Job配置了This project is parameterized,则必须在方法参数列表里增加这个Job的Parameter值,
    如果调用时未提供对应参数,则会用空值传递给Jenkins(旧版本里会报错,说未提供某某参数,我现在的版本2.361.2不会报错,直接用空值启动)
    注2:如果该Job无任何参数,请使用API: /job/jenkinsName/build
/**
 * 启动job构建
 * @param jenkinsName Jenkins的job名,用于url拼接
 * @param pack_env job的参数1:构建环境:test/prev/prod
 * @param publish_branch job的参数2:使用的git分支名
 * @param is_publish job的参数3:是否直接发布
 * @param version_desc job的参数4:发布说明
 * @return 没有响应body,只有header,如:
 * Headers:
 *   content-length: 0
 *   date: Fri, 23 Dec 2022 06:05:01 GMT
 *   location: http://10.100.72.165:8080/queue/item/79/
 *   server: Jetty(10.0.11)
 *   x-content-type-options: nosniff
 */
@RequestLine("POST /job/jenkinsName/buildWithParameters")
@Headers("Content-Type: application/x-www-form-urlencoded")
ResponseEntity<String> startDeploy(
        @Param("jenkinsName") String jenkinsName,
        @Param("pack_env") String pack_env,
        @Param("publish_branch") String publish_branch,
        @Param("is_publish") String is_publish,
        @Param("version_desc") String version_desc
);
  • 根据startDeploy方法返回的队列ID,查询真实任务构建ID(buildNum)接口
    队列ID表示还在排队,并没有启动,要轮询此接口,得到buildNum再访问后续接口。
/**
 * buildWithParameters方法启动构建,得到的只是QueueId,不是Job任务号BuildNum。
 * Jenkins会在队列中等待一个可用的线程,再真的启动Job。
 * 此方法就是根据QueueId去查询启动的Job任务号。
 * 注:需要不断轮询,直到结果出现如下结果为止
 * "executable":"_class":"hudson.model.FreeStyleBuild","number":142,
 * @param queueId 队列id
 * @return json字符串
 */
@RequestLine("GET /queue/item/queueId/api/json")
HashMap<String, Object> queueItem(@Param("queueId") String queueId);
  • 根据任务构建ID(buildNum) 查询构建进度文本结果的接口
    这个文本结果,用于展示Jenkins工作的中间过程数据。
/**
 * 获取某一次构建的进度文本结果信息.
 * 注:也可以用/logText/progressiveText接口,或 /consoleText接口,但是缺少时间数据
 *
 * @param jenkinsName JOB名
 * @param buildNum    具体的构建序列号,可以是字符串"lastBuild",表示最后一次构建。
 * @param start       从第几个字节开始获取,默认0
 * @return 构建过程html数据,用于展示时,建议套上标签<pre> </pre>
 */
@RequestLine("GET /job/jenkinsName/buildNum/logText/progressiveHtml?start=start")
String getBuildProgressive(@Param("jenkinsName") String jenkinsName,
                           @Param("buildNum") int buildNum,
                           @Param("start") int start);
  • 根据任务构建ID(buildNum) 查询构建是否完成的接口
    如果返回值的isBuilding为false,表示构建结束了,去看看成功还是失败吧。
/**
 * 获取某一次构建的进展信息。
 * 需要轮询,直到结果出现:"building":false,表示构建结束
 *
 * @param jenkinsName JOB名
 * @param number      具体的构建序列号,可以是字符串"lastBuild",表示最后一次构建。
 * @return json字符串
 */
@RequestLine("GET /job/jenkinsName/number/api/json?tree=*,executor[*],actions[parameters[*]]")
JenkinsBuildInfo getBuildInfo(@Param("jenkinsName") String jenkinsName,
                              @Param("number") int number);

一个Jenkins JOB的完整构建代码过程

private final JenkinsFeign jenkinsFeign;

private void startProjectBuild(String jenkinsName) 
    ResponseEntity response = jenkinsFeign.startDeploy(jenkinsName, "test", "origin/master", "true", "aaa");
    String queueId = getQueueId(response);
    // 轮询,直到构建启动
    int taskNum = getTaskNumber(queueId);
    // 轮询,直到构建结束
    waitTaskFinish(jenkinsName, taskNum);


// 获取Jenkins响应里的队列ID
private String getQueueId(ResponseEntity response) 
    String location = response.getHeaders().getFirst("location");
    // location参考 http://10.100.72.165:8080/queue/item/37/ 最后一个数字是队列ID
    String[] split = location.split("/");
    return split[split.length - 1];


// 根据Jenkins的QueueId,循环查找执行中的TaskId,如果任务处于Pending状态时,会返回0
private int getTaskNumber(String queueId) 
    int times = 0;
    while (times < 10) 
        ThreadHelper.sleep(3000);
        HashMap<String, Object> map = jenkinsFeign.queueItem(queueId);
        Object objExe = map.get("executable");
        if (objExe != null) 
            Object ret = ((Map<String, Object>) objExe).get("number");
            if (ret != null)
                return (int) ret;
        
        times++;
    
    return 0;


// 循环等待Jenkins任务构建结束
private String waitTaskFinish(String jenkinsName, int taskNum) 
    while (true) 
        ThreadHelper.sleep(10000);
        JenkinsBuildInfo info = jenkinsFeign.getBuildInfo(jenkinsName, taskNum);
        if (!info.isBuilding()) 
            return info.getResult();
        
    


附:我在用的完整FeignClient类定义

import com.zixun.zixunops.builds.jenkins.dtos.JenkinsBuildInfo;
import feign.Contract;
import feign.Headers;
import feign.Param;
import feign.RequestLine;
import feign.auth.BasicAuthRequestInterceptor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.annotation.Bean;
import org.springframework.http.ResponseEntity;

import java.util.HashMap;

@FeignClient(value = "jenkins", url = "$jenkins.worker.url", configuration = JenkinsFeign.MultipartSupportConfig.class)
public interface JenkinsFeign 

    // 直接请求后续接口会报错: Error 403 No valid crumb was included in the request
    // 这个方法就是获取crumb数据的,再在后续请求头里添加 Jenkins-Crumb: xxx
    @RequestLine("GET /crumbIssuer/api/json")
    String getCrumb();

    /**
     * 启动job构建
     *
     * @param jenkinsName    Jenkins的job名,用于url拼接
     * @param pack_env       job的参数1:构建环境:test/prev/prod
     * @param publish_branch job的参数2:使用的git分支名
     * @param is_publish     job的参数3:是否直接发布
     * @param version_desc   job的参数4:发布说明
     * @return 没有响应body,只有header,如:
     * Headers:
     * content-length: 0
     * date: Fri, 23 Dec 2022 06:05:01 GMT
     * location: http://10.100.72.165:8080/queue/item/79/
     * server: Jetty(10.0.11)
     * x-content-type-options: nosniff
     */
    @RequestLine("POST /job/jenkinsName/buildWithParameters")
    @Headers("Content-Type: application/x-www-form-urlencoded")
    ResponseEntity<String> startDeploy(
            @Param("jenkinsName") String jenkinsName,
            @Param("pack_env") String pack_env,
            @Param("publish_branch") String publish_branch,
            @Param("is_publish") String is_publish,
            @Param("version_desc") String version_desc
    );

    /**
     * buildWithParameters方法启动构建,得到的只是QueueId,不是Job任务号BuildNum。
     * Jenkins会在队列中等待一个可用的线程,再真的启动Job。
     * 此方法就是根据QueueId去查询启动的Job任务号。
     * 注:需要不断轮询,直到结果出现如下结果为止
     * "executable":"_class":"hudson.model.FreeStyleBuild","number":142,
     *
     * @param queueId 队列id
     * @return json字符串
     */
    @RequestLine("GET /queue/item/queueId/api/json")
    HashMap<String, Object> queueItem(@Param("queueId") String queueId);


    /**
     * 获取某一次构建的进度文本结果信息.
     * 注:也可以用/logText/progressiveText接口,或 /consoleText接口,但是缺少时间数据
     *
     * @param jenkinsName JOB名
     * @param buildNum    具体的构建序列号,可以是字符串"lastBuild",表示最后一次构建。
     * @param start       从第几个字节开始获取,默认0
     * @return 构建过程html数据,用于展示时,建议套上标签<pre> </pre>
     */
    @RequestLine("GET /job/jenkinsName/buildNum/logText/progressiveHtml?start=start")
    String getBuildProgressive(@Param("jenkinsName") String jenkinsName,
                               @Param("buildNum") int buildNum,
                               @Param("start") int start);

    /**
     * 获取某一次构建的进展信息。
     * 需要轮询,直到结果出现:"building":false,表示构建结束
     *
     * @param jenkinsName JOB名
     * @param number      具体的构建序列号,可以是字符串"lastBuild",表示最后一次构建。
     * @return json字符串
     */
    @RequestLine("GET /job/jenkinsName/number/api/json?tree=*,executor[*],actions[parameters[*]]")
    JenkinsBuildInfo getBuildInfo(@Param("jenkinsName") String jenkinsName,
                                  @Param("number") int number);

    // 获取job的配置,可以用于备份
    @RequestLine("GET /job/jenkinsName/config.xml")
    String getJobConfig(@Param("jenkinsName") String jenkinsName);
    
    // 创建job
    @RequestLine("POST /createItem?name=jenkinsName")
    @Headers("Content-Type: application/xml")
    void createJob(@Param("jenkinsName") String jenkinsName, String config);

    // 删除job
    @RequestLine("POST /job/jenkinsName/doDelete")
    void deleteJob(@Param("jenkinsName") String jenkinsName);

    // 更新job的配置
    @RequestLine("POST /job/jenkinsName/config.xml")
    @Headers("Content-Type: application/x-www-form-urlencoded")
    void updateJobConfig(@Param("jenkinsName") String jenkinsName, String config);


    class MultipartSupportConfig 
        @Value("$jenkins.worker.username:")
        private String jenkinsUser;
        @Value("$jenkins.worker.password:")
        private String jenkinsPassword;

        @Bean
        public Contract feignContract() 
            return new Contract.Default();
        

        @Bean
        public BasicAuthRequestInterceptor basicAuthorizationInterceptor() 
            return new BasicAuthRequestInterceptor(jenkinsUser, jenkinsPassword);
        
    

以上是关于DevOps-7:Jenkins API介绍的主要内容,如果未能解决你的问题,请参考以下文章

DevOps-7:Jenkins API介绍

接口调用限制次数

API验证插件

API验证策略

python获取请求头里面的cookie

js 前端请求头里传 token