与任何编程环境一样,在Jenkins流水线中,集中化功能,共享公共代码和代码重用都是快速、有效地进行开发的基本技术,这些实践鼓励使用标准方法来调用功能,为更复杂的操作创建构建块并隐藏复杂性。他们还可以用于提供一致性以及鼓励约定优于配置以简化任务。

Jenkins允许用户完成所有这些操作的一个关键方法就是使用流水线共享库(pipeline share library)。共享流水线库是由存储在代码仓库中的代码组成的,该代码仓库由Jenkins自动下载并可供流水线使用。

以上中文描述来自《Jenkins 2权威指南》。
关于jenkins pipeline share library的更多介绍,可以参考官方文档

1、需求引入

随着devops理念在公司越来越多的实践,jenkins等工具的应用场景越来越多,当我们在执行完成某个流水线任务后,常常需要关注的是这个任务为什么执行,执行成功与否等等。于是就需要在执行完流水线后进行一定程度的消息推送,在现今的工作流中消息推送无外乎分为两大类:邮件和企业沟通协作软件,相比之下,我们可能更多的会去关注和使用沟通软件来发送消息而不是通过邮件的方式。而常用的企业沟通协作软件有以下几类:腾讯系的企业微信、阿里系的钉钉、字节跳动的飞书等等,当然有能力的企业也会自己研发这类软件。

本文示例以钉钉为例,通过流水线共享库实现自定义消息通知器。

2、钉钉机器人

钉钉的群机器人是钉钉群的高级扩展功能。群机器人可以将第三方服务的信息聚合到群聊中,实现自动化的信息同步。例如:通过聚合GitHubGitLab等源码管理服务,实现源码更新同步;通过聚合TrelloJIRA等项目协调服务,实现项目信息同步。不仅如此,群机器人支持Webhook协议的自定义接入,支持更多可能性。

自定义钉钉机器人支持以下类型消息类型数据格式的推送,更多定义方法可参考官方的接口文档

  • text类型
  • markdown类型
  • 整体跳转ActionCard类型
  • 独立跳转ActionCard类型
  • FeedCard类型

钉钉机器人在2019年的下半年进行过升级,在新增机器人时,需要选择一种安全条件(自定义关键词、加签、ip地址或ip地址段)来保障自定义机器人的安全。可以理解为即使机器人的token泄漏,如果不知道设置的安全条件是什么,还是无法盗用的。

3、jenkins消息推送插件

这里要提到的是在jenkins插件列表中有一个钉钉插件

简单对此插件做了下分析:截止目前此插件在20201月份有相应代码提交,并且发布了2.0版本,从jenkins插件官网中可以看到此版本的插件在在消息中支持了更多内容,效果如下,但是此插件目前还暂不支持流水线中使用

在此之前的上一版本提交记录已经是2018年了,此插件使用方法类似,推送的消息效果如下

此版本支持在流水线中使用,相应内容如下

dingTalk accessToken: "xxx", 
imageUrl: "xxx", 
jenkinsUrl: "https://127.0.0.1:8080", 
message: "项目构建成功", 
notifyPeople: "155xxxx5533"

如上所示,在流水线脚本中配置钉钉机器人token、图片路径、jenkins地址、消息内容、要提醒的人手机号码即可,可以发现,此消息还是有局限性,不够友好。

因此在没有编写插件能力的情况下,我们可以通过更为灵活的自定义流水线共享库的形式,并且按照钉钉机器人的官方接口文档,自定义一个消息推送通知器。

4、自定义通知器的实现

4.1、内容定义

无论jenkins任务的构建触发原因是使用者手动构建或通过代码推送的自动触发,往往关注此消息的人群是开发者们。因此通过一段时间的需求调研以及综合各方的建议,最终将消息推送的内容中包含了以下信息:

  • 应用名称
  • 构建结果
  • 当前版本
  • 构建发起
  • 持续时间
  • 构建日志
  • 更新记录(包含用户提交的短日志,用户名称,提交时间)

每次构建结果通知中包含了以上就基本完备。

4.2、共享库创建

本文不过多介绍共享库具体的创建与在pipeline流水线中的引用方法,整体来说,共享库的代码目录结构如下

(root)
+- src                     # Groovy source files
|   +- org
|       +- foo
|           +- Bar.groovy  # for org.foo.Bar class
+- vars
|   +- foo.groovy          # for global 'foo' variable
|   +- foo.txt             # help for 'foo' variable
+- resources               # resource files (external libraries only)
|   +- org
|       +- foo
|           +- bar.json    # static helper data for org.foo.Bar

官方描述:
src目录应该看起来像标准的Java源目录结构。当执行流水线时,该目录被添加到类路径下。

vars目录定义可从流水线访问的全局变量的脚本。 每个 *.groovy文件的基名应该是一个Groovy (~ Java)标识符, 通常是camelCased。 匹配*.txt, 如果存在, 可以包含文档, 通过系统的配置标记格式化从处理 (所以可能是HTML, Markdown等,虽然txt扩展是必需的)。

这些目录中的Groovy源文件 在脚本化流水线中的CPS transformation一样。

resources目录允许从外部库中使用 libraryResource步骤来加载有关的非Groovy文件。 目前,内部库不支持该特性。

根目录下的其他目录被保留下来以便于将来的增强。

4.3、方法的具体实现

定义共享库中src/org/devops目录为共享库方法的主目录,在这个目录下创建一个名为dingmes.groovy的文件作为钉钉消息推送方法的代码文件。

构建一个消息通知器的主要思路:

  • 消息指标内容从哪来
  • 消息模板如何定义
  • 消息怎么发送,发到哪里

4.3.1、消息来源

首先,消息内容从哪来,上面提到的需要在消息中体现的每个指标的可取的获取方式

指标名称 指标来源定义
应用名称 定义为jenkins的任务名称,通过全局变量env.JOB_NAME获取或者在pipeline中自定义一个变量给出
构建结果 在pipeline中post字段指标判断并给出
当前版本 定义为jenkins的构建编号,通过全局变量env.BUILD_NUMBER或者在pipeline中自定义版本号
构建发起 通过全局变量env.BUILD_USER获取
持续时间 通过全局变量currentBuild.durationString获取,这个值更为友好
构建日志 日志太多,给个链接即可,通过全局变量env.BUILD_URL/console获取
更新记录 这个指标是指代码提交到版本库中的更新信息,而且包含提交时间,提交者名称,获取思路可以通过在检出代码后通过类似git log的命令过滤出或者根据全局变量currentBuild.changeSet获取

分析:
本文中的共享库用于jenkins+k8s自动化ci测试环境,因此某些指标的定义方法为:
应用名称自定义,用变量给出,在pipeline前文定义全局变量,在这里传入变量即可
当前版本自定义,以代码分支+commitid作为docker镜像的tag,在pipeline前文中实现或亦通过共享库实现,在这里传入变量即可
更新记录根据全局变量获取,在这里通过代码实现

较为复杂的是如何解读currentBuild.changeSet这个全局变量,通过jenkins上的全局变量列表文档查看如下

点击其中的链接查看官方文档

通过进一步查看官方文档得知,currentBuild.changeSet返回的是一个集合,这个集合中包含了提交日志,commitid,作者id,作者全称,时间戳等信息,具体对象相关属性如下

currentBuild.changeSets{
    items[{
        msg //提交注释
        commitId //提交hash值
        author{ //提交用户相关信息
            id
            fullName
        }
        timestamp
        affectedFiles[{ //受影响的文件列表
            editType{
                name
            } 
            path: "path"
        }]
        affectedPaths[// 受影响的目录,是个Collection<String>
            "path-a","path-b"
        ]
    }]
}

因此,可以通过循环遍历得出我们需要的相关属性值,通过groovy脚本定义方法并返回相应字符串,其中为了更优化,需要对提交日志做一下长度限制,对时间戳进行格式化,这两个功能需要不断调试。其中changeString变量的赋值格式定义为markdown的无序列表,最终方法如下

def getChangeString() {
    def changeString = ""
    def MAX_MSG_LEN = 20
    def changeLogSets = currentBuild.changeSets
    for (int i = 0; i < changeLogSets.size(); i++) {
        def entries = changeLogSets[i].items
        for (int j = 0; j < entries.length; j++) {
            def entry = entries[j]
            truncatedMsg = entry.msg.take(MAX_MSG_LEN)
            commitTime = new Date(entry.timestamp).format("yyyy-MM-dd HH:mm:ss")
            changeString += " - ${truncatedMsg} [${entry.author} ${commitTime}]\n"
        }
    }
    if (!changeString) {
        changeString = " - No new changes"
    }
    return (changeString)
}

4.3.2、消息模板定义

消息中的相关字段都获取到了,下一步需要做的就是定义一个消息模板,如果使用邮件发送通知,同样的也需要定义一个模板。

这里使用更为友好的markdown格式来发送通知,钉钉机器人接口接收的消息是json格式,具体内容可以通过查看官方文档,为了避免换行出错,手动指定换行符,最终的json格式数据和markdown格式模板如下

{
    "msgtype":"markdown",
    "markdown":{
        "title":"项目构建信息",
        "text":"### 构建信息\n>- 应用名称: **${AppName}**\n- 构建结果: **${Status} ${CatchInfo}**\n- 当前版本: **${ImageTag}**\n- 构建发起: **${env.BUILD_USER}**\n- 持续时间: **${currentBuild.durationString}**\n- 构建日志: [点击查看详情](${env.BUILD_URL}console)\n#### 更新记录: \n${ChangeLog}"
    },
    "at":{
        "atMobiles":[
            "155xxxx5533"
        ],
        "isAtAll":false
    }
}

4.3.3、消息发送方法

在流水线中按照消息模板渲染好的消息发送给钉钉的接口地址,可以实现的方法包括但不限于以下几种:

  • 通过执行shell命令发送,例如curl命令指定参数即可,最为简单,但不够友好
  • 通过pipeline语法和插件实现,例如使用HTTP Request插件,在Jenkins pipeline中发送HTTP请求给钉钉接口。
  • 通过调用其他脚本发送,例如python脚本,较复杂,不推荐。

综上比较,选择一种友好且不复杂的方案,即通过pipeline语法和插件实现

首先在插件安装中安装好HTTP Request插件,打开语法片段生成器查看对应语法

相应参数对应如下:
httpRequest步骤返回的response对象包含两个字段。

  • content:响应内容。
  • status:响应码。 以下是httpRequest步骤支持的参数。
  • url:字符串类型,请求URL。
  • acceptType:枚举类型,HTTP请求Header的“Accept”的值类型为NOT_SET、 TEXT_HTML、TEXT_PLAIN、APPLICATION_FORM、APPLICATION_JSON、 APPLICATION_JSON_UTF8、APPLICATION_TAR、APPLICATION_ZIP、 APPLICATION_OCTETSTREAM。
  • authentication:字符串类型,Username with password凭证的ID,采用的是HTTP Basic认证方式。
  • consoleLogResponseBody:布尔类型,是否将请求的响应body打印出来。
  • contentType:枚举类型,HTTP请求Header的“Content-type”的值类型,与acceptType 支持的枚举一样。
  • customHeaders:HttpRequestNameValuePair对象数组,HTTP请求Header部分的内 容,该对象有3个参数。 ◦ name:字符串类型,Header名称。 ◦ value:字符串类型,Header值。 ◦ maskValue:布尔类型,是否隐藏Header值。如果设置为true,则在打印时使用“*”代 替。 • httpMode:枚举类型,HTTP方法,有GET(默认)、HEAD、POST、PUT、 DELETE、OPTIONS、PATCH。
  • httpProxy:字符串类型,HTTP代理地址
  • ignoreSslErrors:布尔类型,是否忽略SSL错误。
  • requestBody:字符串类型,请求的body内容。
  • timeout:整型,超时时间,单位为秒。默认值为0,代表不设置超时时间。
  • validResponseCodes:字符串类型,代表HTTP请求成功的状态码。它支持3种格式的 值。 ◦ 单状态值:比如200,当收到200响应状态码时,表示HTTP请求成功。 ◦ 多状态值:当响应状态码符合多个状态码中的一个时,代表请求成功。多个状态码 之间使用逗号(,)分隔。比如200,404,500。 ◦ 范围状态值:格式为“From:To”。比如200:302,代表收到200到302的响应状态码 都代表请求成功。
  • validResponseContent:字符串类型,比如设置它的值为“showme.codes”,那么只有 当HTTP返回的内容中包含了“showme.codes”时,才代表请求成功。
  • quiet:布尔类型,是否关闭所有的日志打印,默认值为false。
  • responseHandle:枚举类型,获取HTTP响应内容的方式。其值可以为 ◦ NONE:不读取响应内容。 ◦ LEAVE_OPEN:当执行完请求后,并不会返回响应的内容,而是返回一个打开了的 inputStream,由你自己决定该如何读取响应内容。但是在使用完之后,记得调用inputStream的close()方法关闭。 ◦ STRING(默认值):将响应内容转换成一个字符串。
  • outputFile:字符串类型,请求响应内容的输出路径。

虽然参数有些多,但是只有url是必需的,其他参数都是可选的。这里我们传入请求内容以及url,并省去其他不必要的参数,如下

httpRequest acceptType: 'APPLICATION_JSON_UTF8', 
        consoleLogResponseBody: false, 
        contentType: 'APPLICATION_JSON_UTF8', 
        httpMode: 'POST', 
        ignoreSslErrors: true, 
        requestBody: ReqBody, 
        responseHandle: 'NONE', 
        url: "${DingTalkHook}",
        quiet: true

4.3.4、最终方法

综上所述,在调用此共享库方法时传入应用名称变量AppName、应用版本(镜像tag)变量ImageTag、构建状态变量Status、以及在pipeline前文中实现的异常信息捕捉变量CatchInfo,并结合前面实现的方法内容,最终方法dingmes.groovy内容如下

/* dingmes.groovy
   ##################################################
   # Created by SSgeek                              #
   #                                                #
   # A Part of the Project jenkins-library          #
   ##################################################
*/

package org.devops

def getChangeString() {
    def changeString = ""
    def MAX_MSG_LEN = 20
    def changeLogSets = currentBuild.changeSets
    for (int i = 0; i < changeLogSets.size(); i++) {
        def entries = changeLogSets[i].items
        for (int j = 0; j < entries.length; j++) {
            def entry = entries[j]
            truncatedMsg = entry.msg.take(MAX_MSG_LEN)
            commitTime = new Date(entry.timestamp).format("yyyy-MM-dd HH:mm:ss")
            changeString += " - ${truncatedMsg} [${entry.author} ${commitTime}]\n"
        }
    }
    if (!changeString) {
        changeString = " - No new changes"
    }
    return (changeString)
}

def HttpReq(AppName,ImageTag=' ',Status,CatchInfo=' '){
    wrap([$class: 'BuildUser']){
        def DingTalkHook = "https://oapi.dingtalk.com/robot/send?access_token=67449753547bfcb8e2ee6088fdebaf2cdc7228787201fca83406fc449ffaf92"
        def ChangeLog = getChangeString()
        def ReqBody = """{
            "msgtype": "markdown",
            "markdown": {
                "title": "项目构建信息",
                "text": "### 构建信息\n>- 应用名称: **${AppName}**\n- 构建结果: **${Status} ${CatchInfo}**\n- 当前版本: **${ImageTag}**\n- 构建发起: **${env.BUILD_USER}**\n- 持续时间: **${currentBuild.durationString}**\n- 构建日志: [点击查看详情](${env.BUILD_URL}console)\n#### 更新记录: \n${ChangeLog}"
            },
            "at": {
                "atMobiles": [
                    "155xxxx5533"
                ], 
                "isAtAll": false
                }
            }"""
        // println(currentBuild.description)
        // println(currentBuild.changeSets)
        httpRequest acceptType: 'APPLICATION_JSON_UTF8', 
                consoleLogResponseBody: false, 
                contentType: 'APPLICATION_JSON_UTF8', 
                httpMode: 'POST', 
                ignoreSslErrors: true, 
                requestBody: ReqBody, 
                responseHandle: 'NONE', 
                url: "${DingTalkHook}",
                quiet: true
    }
}

4.4、方法调用

此消息通知的方法通常在pipelinepost部分调用,如下所示

post{
    success{
        script{
            tools.PrintMes("========pipeline executed successfully========",'green')
            dingmes.HttpReq(AppName,ImageTag,"构建成功 ✅")
        }
    }
    failure{
        script{
            tools.PrintMes("========pipeline execution failed========",'red')
            dingmes.HttpReq(AppName,ImageTag,"构建失败 ❌",CatchInfo)
        }
    }
    unstable{
        script{
            tools.PrintMes("========pipeline execution unstable========",'red')
            dingmes.HttpReq(AppName,ImageTag,"构建失败 ❌","不稳定异常")
        }
    }
    aborted{
        script{
            tools.PrintMes("========pipeline execution aborted========",'blue')
            dingmes.HttpReq(AppName,ImageTag,"构建失败 ❌","暂停或中断")
        }
    }
}

4.5、最终效果

测试代码提交,执行流水线,最终的消息通知效果如下图

5、总结

至此,本文记录通过自定义jenkins pipeline流水线共享库方法,实现了较为灵活的自定义钉钉机器人消息通知。如果是使用企信等其他软件,与此实现思路相近。

参考:https://jenkins.io/doc/book/pipeline/shared-libraries/

转载请注明出处,文章转自山山仙人博客