侧边栏壁纸
博主头像
张种恩的技术小栈博主等级

行动起来,活在当下

  • 累计撰写 748 篇文章
  • 累计创建 65 个标签
  • 累计收到 39 条评论

目 录CONTENT

文章目录

Jenkins pipeline 小结

zze
zze
2020-12-18 / 2 评论 / 9 点赞 / 1407 阅读 / 37377 字

不定期更新相关视频,抖音点击左上角加号后扫一扫右方侧边栏二维码关注我~正在更新《Shell其实很简单》系列

最简结构

pipeline {
    agent any

    stages {
        stage('build') {
            steps {
                echo 'hello'
            }
        }
    }
}
  • pipeline:代表整条流水线,包含整条流水线的逻辑。
  • stage 部分:阶段,代表流水线的阶段。每个阶段都必须有名称。本例中,build 就是此阶段的名称。
  • stages 部分:流水线中多个 stage 的容器。stages 部分至少包含一个 stage
  • steps 部分:代表阶段中的一个或多个具体步骤(step)的容器。steps 部分至少包含一个步骤,本例中,echo 就是一个步骤。在一个 stage 中有且只有一个 steps
  • agent 部分:指定流水线的执行位置(Jenkins agent)。流水线中的每个阶段都必须在某个地方(物理机、虚拟机或 Docker 容器)执行,agent 部分即指定具体在哪里执行。

以上每一个部分( section)都是必需的,少一个,Jenkins 都会报错。

步骤(step)

pipeline 基本结构决定的是 pipeline 整体流程,但是真正“做事”的还是 pipeline 中的每一个步骤。步骤是 pipeline 已经不能再拆分的最小操作。前文中,我们只看到一个步骤。echo 是指执行 echo 命令。这个步骤只是 Jenkins pipeline 内置的大量步骤中的一个。

那是不是说,Jenkins pipeline 内置了所有可能需要用到的步骤呢?显然没有必要。因为有些步骤我们可能一辈子也不会用到。

更好的设计是:步骤是可插拔的,就像 Jenkins 的插件一样。

Jenkins 就是这样做的,已经有哪些插件适配了 Jenkins pipeline呢? pipeline plugin Github 仓库给出了一个列表:

只要安装了这些适配了 Jenkins pipeline 的插件,就可以使用其提供的 pipeline 步骤。

Jenkins 官方还提供了 pipeline 步骤参考文档:

agent 部分

agent 部分描述的是整个 pipeline 或在特定阶段执行任务时所在的 agent。换句话说,Jenkins master 根据此 agent 部分决定将任务分配到哪个 agent 上执行。agent 部分必须在 pipeline 块内的顶层定义,而 stage 块内的定义是可选的。

any

文章开头的示例中的 agent 部分是这样写的:

pipeline {
    agent any
    ...

agent any 告诉 Jenkins master 任何可用的 agent 都可以执行。
agent 部分的定义可以放在阶段中,用于指定该 stage 执行时的 agent。

pipeline {
    agent any // 不能省略
    stages {
        stage('Build') {
            agent any
            steps {
                echo 'Build'
            }
        }
    }
}

注意:pipeline 块内的 agent 部分是必需的,不能省略。

label

当 pipeline 需要在 JDK8 环境下进行构建时,就需要通过标签来指定 agent。代码如下:

pipeline {
    agent {
        label 'jdk8'
    }
    ...
}

当然,前提是存在打上 jdk8 标签的节点。

事实上,上述 agent 部分其实是如下 agent 部分的缩写:

agent {
    node {
        label 'jdk8'
    }
}

有些构建任务是需要在 JDK8 及 Windows 环境下执行的。也就是说,我们需要过滤同时具有 windowsjdk8 标签的 agent。可以这样写:

agent {
    label 'windows' && 'jdk8'
}

使用 && 代表并且关系。

none

以上介绍的是如何分配 agent,其实还可以指定不分配 agent,写法很简单:agent none,指的是不分配任何 agent。

没有真正遇到过使用场景,可能就很难想象在什么时候使用 agent。如果希望每个 stage 都运行在指定的 agent 中,那么 pipeline 就不需要指定 agent 了。

示例如下:

pipeline {
    agent none
    stages {
        stage('Example Build') {
            agent { label 'mvn' }
            steps {
                echo 'Hello, build'
            }
        }
        
        stage('Example Test') {
            agent { label 'test' }
            steps {
                echo 'Hello, test'
            }
        }
    }
}

自定义工作路径

agent 部分允许我们对工作目录进行自定义。node 除了 label 选项,还提供了另一个选项 --customWorkspace,自定义工作目录,写法如下:

agent {
    node {
        label 'jdk8'
        customWorkspace '/var/lib/custom'
    }
}

customWorkspace 选项除了写绝对路径,还可以写相对于默认工作目录路径的相对路径。

post 部分

post 部分定义在 pipeline 块下与 stages 同级,它包含的是在整个 pipeline 或阶段完成后一些附加的步骤。post 部分是可选的,所以并不包含在 pipeline 最简结构中。但这并不代表它作用不大。根据 pipeline 或阶段的完成状态,post 部分分成多种条件块,包括:

  • always:不论当前完成状态是什么,都执行。
  • changed:只要当前完成状态与上一次完成状态不同就执行。
  • fixed:上一次完成状态为失败或不稳定(unstable),当前完成状态为成功时执行。
  • regression:上一次完成状态为成功,当前完成状态为失败、不稳定或中止(aborted)时执行。
  • aborted:当前执行结果是中止状态时(一般为人为中止)执行。
  • failure:当前完成状态为失败时执行。
  • success:当前完成状态为成功时执行。
  • unstable:当前完成状态为不稳定时执行。
  • cleanup:清理条件块。不论当前完成状态是什么,在其他所有条件块执行完成后都执行。

post 部分可以同时包含多种条件块,例:

post {
    success {
        echo 'pipeline post success'
    }

    always {
        echo 'pipeline post always'
    }

    failure {
        mail to: 'zze@exp.com', subject: 'exec failed'
    }
}

指令

指令可以被理解为对 Jenkins pipeline 基本结构的补充。

Jenkins pipeline 支持的指令有:

  • environment:用于设置环境变量,可定义在 stagepipeline 部分。

  • tools:可定义在 pipelinestage 部分。它会自动下载并安装我们指定的工具,并将其加入 PATH 变量中。

  • input:定义在 stage 部分,会暂停 pipeline,提示你输入内容。

  • options:用于配置 Jenkins pipeline 本身的选项,比如 options {retry(3)} 指当 pipeline 失败时再重试 3 次。options 指令可定义在 stagepipeline 部分。

  • parallel:并行执行多个 step。在 pipeline 插件 1.2 版本后,parallel 开始支持对多个阶段进行并行执行。

  • parameters:与 input 不同,parameters 是执行 pipeline 前传入的一些参数。

  • triggers:用于定义执行 pipeline 的触发器。

  • when:当满足 when 定义的条件时,阶段才执行。

在使用指令时,需要注意的是每个指令都有自己的“作用域”。如果指令使用的位置不正确,Jenkins 将会报错。

配置 pipeline

options 指令用于配置整个 Jenkins pipeline 本身的选项。根据具体的选项不同,可以将其放在 pipeline 块或 stage 块中。

接下来我们介绍常用的几个选项。

  • buildDiscrder:保存最近构建记录的数量。当 pipeline 执行完成后。会在硬盘上保存制品和构建执行日志,如果长时间不清理会占用大量空间,设置此选顶后会自动清理。此选项只能在 pipeline 下的 options 中使用,例:
options {
    buildDiscarder(logRotator(numToKeepStr: '10', daysToKeepStr: '3'))
}
  • checkoutToSubdirectory:Jenkins 从版本控制库拉取源码时,默认检出到工作空间的根目录中,此选项可以指定检出到工作空间的子目录中。示例如下:
options {
    checkoutToSubdirectory('subdir')
}
  • disableConcurrentBuilds:同一个 pipeline,Jenkins 默认是可以同时执行多次的。此选项是为了禁止 pipeline 同时执行。示例如下:
options {
    disableConcurrentBuilds()
}
  • newContainerPerStageagent 为 docker 时,指定在同一个 Jenkins 节点上,每个 stage 都分别运行在一个新的容器中,而不是所有 stage 都运行在同一个容器中。
options {
    newContainerPerStage()
}
  • retry:当发生失败时进行重试,可以指定整个 pipeline 的重试次数,需要注意的是,这个次数是指总次数,包括第 1 次失败,以下例子总共会执行 4 次。当使用 retry 这项时,options 可以被放在 stage 块中。
options {
    retry(4)
}
  • timeout:如果 pipeline 执行时间过长,超出了我们设置的 timeout 时间,Jenkins 将中止 pipeline。以下例子中以小时为单位,还可以 SECONDS(秒)、MINUTES(分钟)为单位。当使用 timeout 选项时,options 可以被放在 stage 块中。
options {
    timeout(time: 10, unit: 'HOURS')
}

使用脚本

Jenkins pipeline 专门提供了一个 script 步骤,你能在 script 步骤中像写 Groovy 代码一样写 pipeline 逻辑。比如分别在不同的浏览器上跑测试:

pipeline {
    agent any

    stages {
        stage('Example') {
            steps {
                script {
                    def browsers = ['chrome', 'firefox']
                    for (int i = 0; i < browsers.size(); ++i) {
                        echo "Testing the ${browsers[i]} browser"
                    }
                }
            }
        }
    }
}

可以看出,在 script 块中的其实就是 Groovy 代码。大多数时候,我们是不需要使用 script 步骤的。如果在 script 步骤中写了大量的逻辑则说明你应该把这些逻辑拆分到不同的阶段,或者放到共享库中。共享库是一种扩展 Jenkins pipeline 的技术,大部分时候我们都不会用到,这里就不多说。

另外,你可能已经注意到,这样串行的测试方法是低效的,而应该在不同的浏览器上并行跑测试。

内置基础步骤

文件目录相关

  • deleteDir:删除当前目录,deleteDir 是一个无参步骤,删除的是当前工作目录。通常它与 dir 步骤一起使用,用于删除指定目录下的内容。
  • dir:切换到目录,默认 pipeline 工作在工作空间目录下,dir 步骤可以让我们切换到其他目录。使用方法如下:
dir('/var/logs') {
    deleteDir()
}
  • fileExists:判断文件是否存在,fileExists('/tmp/a.jar') 判断 /tmp/a.jar 文件是否存在。如果参数是相对路径,则判断在相对当前工作目录下该文件是否存在,结果返回布尔类型。

  • isUnix:判断是否为类 UNIX 系统,如果当前 pipeline 运行在一个类 UNIX 系统上,则返回 true

  • pwd:确认当前目录,pwdLinuxpwd 命令一样,返回当前所在目录。它有一个布尔类型的可选参数 tmp,如果参数值为 true,则返回与当前工作空间关联的临时目录。

  • writeFile:将内容写入指定文件中,writeFile 支持的参数有:

    • file:文件路径,可以是绝对路径,也可以是相对路径。
    • text:要写入的文件内容。
    • encoding(可选):目标文件的编码。如果留空,则使用操作系统默认的编码。如果写的是 Base64 的数据,则可以使用 Base64 编码。

    示例如下:

script {
    // amVua2lucyBib29r 是 jenkins book 进行 Base64 编码后的值
    writeFile(file: 'base64File', text: "amVua2lucyBib29r", encoding: 'Base64')
    def content = readFile(file: 'base64File', encoding: 'UTF-8')
    echo content // jenkins book
}
  • stash:保存临时文件,stash 步骤可以将一些文件保存起来,以便被同一次构建的其他步骤或阶段使用。如果整个 pipeline 的所有阶段在同一台机器上执行,则 stash 步骤是多余的。所以,通常需要 stash 的文件都是要跨 Jenkins node 使用的。stash 步骤会将交件存储在 tar 文件中,对于大文件的 stash 操作将会消耗 Jenkins master 的计算资源。Jenkins 官方文档推荐,当文件大小为 5~100MB 时,应该考虑使用其他替代方案。
    stash 步骤的参数列表如下:

    • name:字符串类型,保存文件的集合的唯一标识。
    • allowEmpty:布尔类型,允许 stash 内容为空。
    • excludes:字符串类型,将哪些文件排除。如果排除多个文件,则使用逗号分隔。留空代表不排除任何文件。
    • includes:字符串类型,stash 哪些文件,留空代表当前文件夹下的所有文件。
    • useDefaultExcludes:布尔类型,如果为 true,则代表使用 Ant 风格路径默认排除文件列表。

    除了 name 参数,其他参数都是可选的。excludesincludes 使用的是 Ant 风格路径表达式。

  • unstash:取出之前 stash 的文件,unstash 步骤只有一个 name 参数,即 stash 时的唯一标识。通常 stashunstash 步骤同时使用。以下是完整示例(stash 步骤在 golang-executor 标签标记的节点上执行,而 unstash 步骤在 java-executor 标签标记的节点上执行。)。

pipeline {
    agent none
    stages {
        stage('stash') {
            agent {
                label 'golang-executor'
            }

            steps {
                writeFile file: 'a.txt', text: 'hello zze'
                stash(name: 'abc', includes: 'a.txt')
            }
        }

        stage('unstash') {
            agent {
                label 'java-executor'
            }

            steps {
                script {
                    unstash('abc')
                    def content = readFile('a.txt')
                    echo "${content}" // hello zze
                }
            }
        }
    }
}

命令相关步骤

  • sh:执行 shell 命令,sh 步骤支持的参数有:

    • script:将要执行的 shell 脚本,通常在类 UNIX 系统上可以是多行脚本。
    • encoding:脚本执行后输出日志的编码,默认值为脚本运行所在系统的编码。
    • returnStatus :布尔类型,默认脚本返回的是状态码,如果是一个非零的状态码,则会引发 pipeline 执行失败。如果 returnStatus 参数为 true。则不论状态码是什么, pipeline 的执行都不会受影响。
    • returnStdout:布尔类型,如果为 true。则任务的标准输出将作为步骤的返回值,而不是打印到构建日志中(如果有错误,则依然会打印到日志中),除了 script 参数,其他参数都是可选的。

    returnStatusreturnStdout 参数一般不会同时使用,因为返回值只能有一个。如果同时使用,则只有 returnStatus 参数生效。

  • batpowershell 步骤:bat 步骤执行的是 Windows 的批处理命令。powershell 步骤执行的是 PowerShel 脚本,支持 3+ 版本。这两个步骤支持的参数与 sh 步骤的一样,这里就不重复介绍了。

其它步骤

  • error:主动报错,中止当前 pipeline。error 步骤的执行类似于抛出一个异常。它只有一个必需参数:message。通常省略该参数:error("there's an error")

  • tool:使用预定义的工具,如果在 Global Tool Confguration(全局工具配置)中配置了工具,那么可以通过 tool 步骤得到工具路径。tool 步骤支持的参数有:

    • name:工具名称。
    • type(可选):工具类型,指该工具安装类的全路径类名。每个插件的 type 值都不一样,而且绝大多数插件的文档根本不写 type 值。除了到该插件的源码中查找,还有一种方法可以让我们快速找到 type 值,就是前往 Jenkins pipeline 代码片段生成器中生成该 tool 步骤的代码即可。
  • timeout:代码块超时时间。为 timeout 步骤闭包内运行的代码设置超时时间限制。如果超时,将抛出一个 org.jenkinsci.workflow.steps.FlowInterruptedException 异常。timeout 步骤支持如下参数:

  • time:整型,超时时间。

  • unit(可选):时间单位,支持的值有 NANOSECONDSMICROSECONDSMILLISECONDSSECONDSMINUTES(默认)、HOURSDAYS

  • activity(可选):布尔类型,如果值为 true,则只有当日志没有活动后,才真正算作超时。

  • waitUntil:等待条件满足。不断重复 waitUntil 块内的代码,直到条件为 truewaitUlntil 不负责处理块内代码的异常,遇到异常时直接向外抛出。waitUntil 步骤最好与 timeout 步骤一起使用,避免死循环。示例如下:

steps {
    script {
        def r = sh script: 'curl https://www.zze.xyz', returnStatus: true
        return (r==0)
    }
}
  • retry:重复执行块。执行 N 次闭包内的脚本。如果其中某次执行抛出异常,则只中止本次执行,并不会中止整个 retry 的执行。同时,在执行 retry 的过程中,用户是无法中止 pipeline 的。
steps {
    retry(20){
        script {
            sh script: 'curl https://www.zze.xyz', returnStatus: true
        }
    }
}
  • sleep:让 pipeline 休眠一段时间。sleep 步骤可用于简单地暂停 pipeline,其支持的参数有:

    • time:整型,休眠时间。
    • unit(可选),时间单位,支持的值有 NANOSECONDSMICROSECONDSMILLISECONDSSECONDS(默认)、MINUTESHOURSDAYS

    示例如下:

steps {
    sleep(120) // 休眠 120 秒
    sleep(time: '2', unit: 'MINUTES') // 休眠 2 分钟
}

环境变量

内置变量

在 pipeline 执行时,Jenkins 通过一个名为 env 的全局变量,将 Jenkins 内置环境变量暴露出来。其使用方法有多种,示例如下:

stage('Example') {
    steps {
        echo "Running ${env.BUILD_NUMBER} on ${JENKINS_URL}" // 方法一
        echo "Running $env.BUILD_NUMBER on $env.JENKINS_URL" // 方法二
        echo "Running ${BUILD_NUMBER} on ${JENKINS_URL}" // 方法三
    }
}

默认 env 的属性可以直接在 pipeline 中引用。所以,以上方法都是合法的。但是不推荐方法三,因为出现变量冲突时,非常难查问题。

那么,env 变量都有哪些可用属性呢?通过访问 <Jenkins master 的地址>/pipeline-syntax/globals 来获取完整列表。在列表中,当一个变量被声明为 For a multibranch project 时,代表只有多分支项目才会有此变量。

下面我们简单介绍几个在实际工作中经常用到的变量。

  • BUILD_NUMBER:构建号,累加的数字。在打包时,它可作为制品名称的一部分,比如 server-2.jar
  • BRANCH_NAME:多分支 pipeline 项目支持。当需要根据不同的分支做不同的事情时就会用到,比如通过代码将 release 分支发布到生产环境中、master 分支发布到测试环境中。
  • BUILD_URL:当前构建的页面 URL。如果构建失败,则需要将失败的构建链接放在邮件通知中,这个链接就可以是 BUILD_URL
  • GIT_BRANCH:通过 git 拉取的源码构建的项目才会有此变量。

在使用 env 变量时,需要注意不同类型的项目,env 变量所包含的属性及其值是不一样的。比如普通 pipeline 任务中的 GIT_BRANCH 变量的值为 origin/master,而在多分支 pipeline 任务中 GIT_BRANCH 变量的值为 master

所以,在 pipeline 中根据分支进行不同行为的逻辑处理时,需要留意。

小技巧。在调试 pipeline 时,可以在 pipeline 的开始阶段加一句:sh printenv,将 env 变量的属性值打印出来。这样可以帮助我们避免不少问题。

自定义环境变量

当 pipeline 变得复杂时,我们就会有定义自己的环境变量的需求。声明式 pipeline 提供了 environment 指令,方便自定义变量。比如:

pipeline {
    agent any
    environment {
        CC = 'cccc'
    }
    stages {
        stage('Example') {
            environment {
                DD = 'dddd'
            }

            steps {
                sh 'printenv | egrep "CC|DD"'
            }
        }
    }
}

另外,environment 指令可以在 pipeline 中定义,代表变量作用域为整个 pipeline;也可以在 stage 中定义,代表变量只在该阶段有效。怛是这些变量都不是跨 pipeline 的,比如 pipeline a 访问不到 pipeline b 的变量。在 pipeline 之间共享变量可以通过参数化 pipeline 来实现。

在实际工作中,还会遇到一个环境变量引用另一个环境变量的情况。在 environment 中可以这样定义:

environment {
    __server_name = 'mail-server'
    __version = "${BUILD_NUMBER}"
    __artifact_name = "${__server_name}-${__version}.jar"
}

值得注意的是,如果在 environment 中定义的变量与 env 中的变量重名,那么被重名的变量的值会被覆盖掉。

env 中的变量都是 Jenkins 内置的,或者是与具体 pipeline 相关的。有时候,我们需要定义一些全局的跨 pipeline 的自定义变量。进入 系统管理 -> 系统配置 -> 全局属性 页,勾选 环境变量 复选框,单击 新增 按钮,在输入框中输入变量名和变量值即可,如下图所示:

image.png

自定义全局环境变量会被加入 env 属性列表中,所以,使用自定义全局环境变量与使用 Jenkins 内置变量的方法无异:${env.name}

when 指令的用法

when 指令允许 pipeline 根据给定的条件,决定是否执行阶段内的步骤。when 指令必须至少包含一个条件。

常用条件

  • branch:判断当前阶段的构建是否属于指定的分支,一般仅在多分支任务中使用。
stage('deploy to test') {
    when {
        branch 'master'
    }

    steps {
        echo 'deploy to test'
    }
}

stage('deploy to prod') {
    when {
        branch 'prod'
    }

    steps {
        echo 'deploy to prod'
    }
}
  • changelog:如果版本控制库的 changelog 符合正则表达式,则执行。
when {
    changelog '.*\\[DEPENDENCY] .+$'
}
  • changeset:如果版本控制库的变更集合中包含一个或多个文件符合给定的 Ant 风格路径表达式,则执行。
when {
    changeset '**/*.js'
}
  • environment:如果环境变量的值与给定的值相同,则执行。
when {
    environment name: 'DEPLOY_TO', value: 'prod'
}
  • equals:如果期望值与给定的值相同,则执行。
when {
    equals expected: 2, actual: currentBuild.Number
}
  • expression:如果 Groovy 表达式返回的是 true,则执行(当表达式返回的是字符串时,它必须转换成布尔类型或 null,否则,所有的字符串都被当作 true 处理)。
when {
    expression {
        // 支持与、或操作
        // return A && B
        // return A || B
        // 支持函数结果返回值
        // return readFile('pom.xml').contains('component')
        // 支持正则表达式
        // return token ==~ /(?i)(Y|YES|T|TRUE|ON|RUN)/
        return env.BRANCH_NAME != 'master'
    }
}
  • buildingTag:如果此次 pipeline 所执行的代码被打了 tag,则执行。
when {
    buildingTag()
}
  • tag:如果 pipeline 所执行的代码被打了 tag,且 tag 名称符合规则,则执行。tag 支持 patterncomparator 参数,pattern 用于指定标签名的匹配规则,``comparator用于指定匹配的模式,comparator` 支持的值如下:
    • EQUALS:简单的文本比较。
    • GLOB(默认值):Ant 风格路径表达式。由于是默认值,所以使用时一般省略。
    • REGEXP:正则表达式。
when {
    tag 'release-*'
}
// 完整写法
when {
    tag pattern: 'release-*', comparator: 'GLOB'
}

如果 tag 的参数为空,即 tag(),则表示不论 tag 名称是什么都执行,与 buildingTag() 的效果相同。

  • beforeAgent:布尔值,在默认情况下,阶段内所有的代码都将在指定的 Jenkins agent 上执行。when 指令提供了一个 beforeAgent 选项,当它的值为 true 时,只有符合 when 条件时才会进入该 Jenkins agent。这样就可以避免没有必要的工作空间的分配,也就不需要等待可用的 Jenkins agent 了。

多条件组合判断

以上介绍的都是单条件判断,when 指令还可以进行多条件组合判断。

  • allOf:所有条件都必须符合。下例表示当分支为 master 且环境变量 DEPLOY_TO 的值为 production 时,才符合条件。
when {
    allOf {
        branch 'master';
        environment name: 'DEPLOY_TO', value: 'production'
    }
}

注意,多条件之间使用分号分隔。

  • anyOf:其中一个条件为 true,就符合。下例表示 master 分支或 staging 分支都符合条件。
when {
    anyOf {
        branch 'master';
        branch 'staging'
    }
}

参数化

参数化 pipeline 是指可以通过传参来决定 pipeline 的行为。参数化让写 pipeline 就像写函数,而函数意味着可重用、更抽象。所以,通常使用参数化 pipeline 来实现一些通用的 pipeline。

在 Jenkins pipeline 中定义参数使用的是 parameters 指令,其只允许被放在 pipeline 块下。看如下示例:

pipeline {
    agent any

    parameters {
        booleanParam(defaultValue: true, description: '', name: 'userFlag')
    }
    stages {
        stage('test') {
            steps {
                echo "flag: ${userFlag}"
            }
        }
    }
}

booleanParam 方法用于定义一个布尔类型的参数。booleanParam 方法接收三个参数。

  • defaultValue:默认值。
  • description:参数的描述信息。
  • name:参数名。

在定义了 pipeline 的参数后,如何使用呢?

被传入的参数会放到一个名为 params 的对象中,在 pipeline 中可直接使用。params.userFlag 就是引用 parameters 指令中定义的 userFlag 参数。值得注意的是,在 Jenkins 新增此 pipeline 后,至少要手动执行一次,它才会被 Jenkins 加载生效。生效后,在执行项目时,就可以设置参数值了,

为了满足不同的应用场景,参数化 pipeline 支持多种参数类型,包括:

  • string:字符串类型。
parameters {
    string(name: 'DEPLOY_ENV', defaultValue: 'staging', description: '')
}
  • text:多行文本类型,换行使用 \n
parameters {
    text(name: 'DEPLOY_TEXT', defaultValue: 'one\ntwo\three\n', description: '')
}
  • booleanParam:布尔类型。
parameters {
    booleanParam(name: 'DEBUG_BUILD', defaultValue: false, description: '')
}
  • choice:选择参数类型,使用 \n 来分隔多个选项。
parameters {
    choice(name: 'CHOICES', choices: 'dev\ntest\nprod', description: '请选择部署的环境')
}
  • password:密码类型。
parameters {
    password(name: 'PASSWORD', defaultValue: 'SECRET', description: '请输入密码')
}

使用凭证

Jeakin 默认支持以下凭证类型:Secret textUsername with passwordSecret fileSSH Username with private keyCertificate

添加凭证后,安装 Credentials Binding Plugin插件,通过其提供的 withCredentias 步骤就可以在 pipeline 中使用凭证了。

withCredentials

  • Secret text:是一串需要保密的文本,比如 GitLab 的 API token。
withCredentials([string(credentialsId: 'test-secret-text', variable: 'secret_text')]) {
    echo "${secret_text}"
}
  • Username with password:用户名和密码凭证。
withCredentials([usernamePassword(credentialsId: "harbor-auth", passwordVariable: 'password', usernameVariable: 'username')]) {
    echo "${password}"
    echo "${username}"
}
  • Secret file:需要保密的文本文件。使用 Secret file 时,Jenkins 会将文件复制到一个临时目录中,再将文件路径设置到一个变量中。构建结束后,所复制的 Secret file 会被删除。
withCredentials([file(credentialsId: "k8s-token", variable: 'k8s_token_path')]) {
    script {
        sh("cp ${k8s_token_path} .")
    }
}
  • SSH Username with private key:一对 SSH 用户名和密钥。
withCredentials([sshUserPrivateKey(credentialsId: "admin-private-key", keyFileVariable: 'key_path', usernameVariable: 'admin_username', passphraseVariable: 'key_text')]) {
    echo "${key_path}"
    echo "${admin_username}"
    echo "${key_text}"
}

withDockerRegistry

withDockerRegistry 可自动获取 credentialsId 对应的凭证完成 Docker login 操作,在其闭包内的语句都会在认证后的上下文执行。

withDockerRegistry(credentialsId: "${harborCredentialsId}", url: "http://${registryAddr}") {
    sh """
    docker build -t ${imageName} .
    docker push ${imageName}
    """
}

使用该步骤需要额外安装 Docker pipeline 插件。

明文打印凭证

默认情况下在 withCredentials 内部输出凭证信息是加密后的 ***,如果想要明文输出凭证信息,则需要在 withCredentials 外部定义一个变量接收凭证变量,然后在外部即可明文输出凭证内容:

script {
    def hack = ''
    withCredentials([usernamePassword(credentialsId: "harbor", passwordVariable: 'password', usernameVariable: 'username')]) {
        echo "${password}"
        echo "${username}"
        hack = "${password}"
    }
    echo "${hack}"
}

并行构建

并行阶段

pipeline {
    agent none
    stages {
        stage('Run Tests') {
            failFast true
            parallel {
                stage('Test on Chrome') {
                    agent { label 'chrome' }
                    steps {
                        echo 'Chrome UI 测试'
                    }
                }
                
                stage('Test on Filefox') {
                    agent { label 'firefox' }
                    steps {
                        echo 'Firefox UI 测试'
                    }
                }
                
                stage('Test on IE') {
                    agent { label 'ie' }
                    steps {
                        echo 'IE UI 测试'
                    }
                }
            }
        }
    }
}

位于 parallel 块下的阶段都将并行执行,而且并行阶段还可以被分到不同的 Jenkins agent 上执行,在默认情况下,Jenkins pipeline 要等待 parallel 块下所有的阶段都执行完成,才能确定结果。如果希望所有并行阶段中的某个阶段失败后,就让其他正在执行的阶段都中止,那么只需要在与 parallel 块同级的位置加人 failFast: true 就可以了。

并行步骤

stage('并行构建') {
    steps {
        parallel {
                jdk8: {
                    echo "jdk8 build"
                },
                jdk9: {
                    echo "jdk9 build"
                }
        }
    }
}

除了写法的不同,表面上看并行阶段与并行步骤并没有太大的区别。但是它们有一个关键的区别。并行阶段运行在不同的 executor 上,而并行步骤运行在同一个 executor 上,这样看来其实就是并行与并发的区别。并行步骤本质上是并发步骤。

在不同分支上并行

pipeline {
    agent any
    stages {
        stage('Parallel Stage') {
            failFast true
            parallel {
                stage('Branch master') {
                    when { branch 'master' }
                    agent any
                    steps {
                        echo 'On Branch master'
                    }
                }
                
                stage('Branch dev') {
                    when { branch 'dev' }
                    agent any
                    steps {
                        echo 'On Branch dev'
                    }
                }
                
                stage('Branch staging') {
                    when { branch 'staging' }
                    agent any
                    stages {
                        stage('嵌套 staging 1') {
                            steps {
                                echo 'staging 1'
                            }
                        }
                        
                        stage('嵌套 staging 2') {
                            steps {
                                echo 'staging 2'
                            }
                        }
                    }
                }
            }
        }
    }
}

我们注意到在并行阶段 Branch staging 下又出现一个 stages 部分。是的,阶段是可以嵌套的。但是可以嵌套多少层呢?Jenkins 的文档并没有明确说明。建议不要超过三层,因为在同一个 Jenkins pipeline中实现过于复杂的逻辑,说明 Jenkins pipeline 的职责不够单一,需要进行拆分。

综合示例

def registryAddr = params.isRelease?"registry.cn-shenzhen.aliyuncs.com":"10.41.1.12:8081"
def gitCredentialsId='jenkins'
def harborCredentialsId=params.isRelease?'aliyun-docker-registry':'harbor'
def gitUrl='http://git.zze.com/git/zze-monitor.git'
def gitConfigBranch=params.isRelease?'production':'master'
// 镜像标签
def imageTag = "${createTag()}"
def prj = params.isRelease?"zze-monitor":"${JOB_NAME}"
// 离线镜像地址
def imgsDir = "/root/images/${prj}/${imageTag}/imgs"
// 离线 chart 地址
def chartsDir = "/root/images/${prj}/${imageTag}/charts"

def createTag() {
    def image_tag = "${params.branchTag}_${new Date().format('yyyyMMddHHmmss')}_${BUILD_NUMBER}"
    if(params.isRelease){
        image_tag = image_tag + "_release"
    }
    return image_tag
}

pipeline {
    agent {
        label 'golang-executor'
    }
    
    options {
        buildDiscarder(logRotator(numToKeepStr: '10', daysToKeepStr: '3'))
        // disableConcurrentBuilds()
        retry(1)
        timeout(time: 60, unit: 'MINUTES')
    }
    
    parameters { 
        booleanParam(name: 'isDeploy', defaultValue: true, description: '立即部署')
        booleanParam(name: 'isRelease', defaultValue: false, description: '打离线包')
        gitParameter(name: 'branchTag', 
                     type: 'PT_BRANCH_TAG',
                     branchFilter: 'origin/(.*)',
                     defaultValue: 'func_saas_202101_dev',
                     selectedValue: 'DEFAULT',
                     sortMode: 'DESCENDING_SMART',
					 description: '选择要构建的分支或标签')
        extendedChoice( 
            name: 'serviceSelectStr', 
            defaultValue: '', 
            description: '选择要构建的服务', 
            type: 'PT_CHECKBOX', 
            value: 'zze-cron-job,zze-monitor-admin,zze-monitor-agent,zze-monitor-alarm,zze-monitor-core,zze-monitor-login,zze-monitor-platform,zze-monitor-transfer,zze-monitor-websocket,zze-monitor-judge'
        )
    }

    environment {
        GOPATH = "${env.WORKSPACE}/"
    }
    
    stages {
        stage('拉取代码') {
            options { retry(5) }
            steps {
                checkout([$class: 'GitSCM', branches: [[name: "${params.branchTag}"]], doGenerateSubmoduleConfigurations: false, userRemoteConfigs: [[credentialsId: "${gitCredentialsId}", url: "${gitUrl}"]]])
                sh 'mkdir -p configs'
                dir('configs') {
                    git branch: "${gitConfigBranch}", credentialsId: 'jenkins', url: 'https://git.zze.com/zhangxin/config-server.git'
                }
            }
        }
        
        stage('并行构建') {
            steps {
                script{
                    if(params.isRelease) {
                        sh("mkdir -p ${imgsDir} ${chartsDir} && cp ${env.WORKSPACE}/configs/global.json ${chartsDir}/global.json")
                    }
                    
                    def jobMap = [: ]
                    def serviceSelectLst = params.serviceSelectStr.split(',')
                    for (service in serviceSelectLst){
                        def currentService = service
                        jobMap.put(currentService, {
                            def imageName = "${registryAddr}/${prj}/${currentService}:${imageTag}"
                            def helmInstallOpts = "-f ${env.WORKSPACE}/configs/global.json --set image.domain=${registryAddr}/${prj} --set image.repository=${currentService} --set image.tag=${imageTag}"
                            // def helmInstallOpts="--set image.domain=${registryAddr}/${prj} --set image.repository=${currentService} --set image.tag=${imageTag}"
                            
                            stage("${currentService} 编译") {
                                dir("server/${currentService}") {
                                    sh "go build -o build/docker/${currentService}"
                                }
                            }

                            stage("${currentService} 构建镜像") {
                                dir("server/${currentService}") {
                                    withDockerRegistry(credentialsId: "${harborCredentialsId}", url: "http://${registryAddr}") {
                                        sh """
                                        cd build
                                        docker build -f dockerfile -t "${imageName}" .
                                        docker push ${imageName}
                                        """
                                    }
                                }
                                echo "构建完成的镜像地址:${imageName}"
                            }
                            
                            if(params.isRelease) {
                                stage("${currentService} 打离线包") {
                                    sh """
                                    # docker save -o ${imgsDir}/${currentService}.dockerimg ${imageName}
                                    cp -r server/${currentService}/build/helmchart ${chartsDir}/${currentService}
                                    echo 'helm install -f global.json --set imagePullSecrets=zze-aliyun-repo --set image.domain=${registryAddr}/${prj} --set image.repository=${currentService} --set image.tag=${imageTag} ${currentService} ./${currentService}' >> ${chartsDir}/install.sh
                                    """
                                }
                            }

                            if(params.isDeploy) {
                                stage("${currentService} 部署到 Kubernetes") {
                                    sleep 3
                                    dir("server/${currentService}/build") {
                                        sh """
                                        helm list -q | grep '${currentService}' && helm upgrade ${helmInstallOpts} ${currentService} ./helmchart/ || helm install ${helmInstallOpts} ${currentService} ./helmchart/
                                        """
                                    }
                                }
                            }
                        }) 
                    }
                    parallel(jobMap)
                }
            }
        }
    }

    post {
        always {
            sh '''
            helm list && sleep 10
            until [ $(kubectl get pod -n zze | grep -i 'imagepull' | wc -l) -eq 0 ];do
                kubectl get pod -n zze
                kubectl get pod -n zze | grep -i 'imagepull' | awk '{print $1}' | xargs -i kubectl delete pod {} -n zze
                sleep 5
            done
            '''
        }
    }
}
9

评论区