「Jenkins Pipeline」- 如何编写共享库

更新日期:2020年03月10日

共享库开发

任何 Groovy 有效的代码都可以。例如不同的数据结构、工具方法:

// src/org/foo/Point.groovy
package org.foo

// point in 3D space
class Point {
  float x,y,z
}

在类中访问步骤

方法一、“在类外部”

在库类中(src/),不能直接调用步骤(比如 ssh、git 等等)。然而他们可以实现方法,需要在封闭的类的范围外部,在其中调用步骤:

// src/org/foo/Zot.groovy
package org.foo

def checkOutFrom(repo) {
  git url: "git@github.com:jenkinsci/${repo}"
}

return this

然后在 Pipeline Script 中调用:

def z = new org.foo.Zot()
z.checkOutFrom(repo)

这种方法有局限性;例如,它阻止父类的声明。

方法二、使用 this 关键字

另外可以通过 this 关键字将步骤传递到类中。可以在构造器中,也可以是一个方法:

// src/org/foo/Utilities.grovvy
package org.foo
class Utilities implements Serializable {
  def steps

  Utilities(steps) {
    this.steps = steps
  }

  def mvn(args) {
    steps.sh "${steps.tool 'Maven'}/bin/mvn -o ${args}"
  }
}

在类上保存状态时,像上面那样,类必须实现 Serializable 接口,这保证使用该类的 Pipeline 可以在 Jenkins 中休眠和恢复。

调用定义的类:

@Library('utils') 
import org.foo.Utilities

def utils = new Utilities(this)
node {
  utils.mvn 'clean package'
}

在类中使用全局变量

如果要使用全局变量(env),应该明确的“传入类”(使用构造器)或“传入方法”(使用方法参数)中。手段是类似的:

package org.foo
class Utilities {
  static def mvn(script, args) {
    script.sh "${script.tool 'Maven'}/bin/mvn -s ${script.env.HOME}/jenkins.xml -o ${args}"
  }
}

上面的代码将环境变量传入静态的方法中,然后在下面的脚本化Pipeline中调用:

@Library('utils') import static org.foo.Utilities.*
node {
  mvn this, 'clean package'
}

注意,不建议将env中的参数取出来挨个传入函数中。

定义全局变量(vars/)

在内部,在 vars/ 中的脚本“按需要”实例化为单例。

在全局变量中定义方法

为方便起见,这允许在单个.groovy文件中定义多个方法。例如:

// vars/log.groovy
def info(message) {
    echo "INFO: ${message}"
}

def warning(message) {
    echo "WARNING: ${message}"
}

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

// In Jenkinsfile
@Library('utils') _

log.info 'Starting'
log.warning 'Nothing to do!'

在全局变量中定义变量

注意,如果您希望在全局中使用某个字,用于保存状态,请将其注解为:

@groovy.transform.Field
def yourField = [:]

def yourFunction....

调用全局变量

声明式的 Pipeline 不允许在 script 块外进行对象的方法调用(JENKINS-42360),需要放在 script 中使用:

// In Jenkinsfile
@Library('utils') _

pipeline {
    agent none
    stage ('Example') {
        steps {
            // log.info 'Starting' // 该命令会失败,因为它在 script 块之外
            script { // 需要使用 script 块来访问全局变量
                log.info 'Starting'
                log.warning 'Nothing to do!'
            }
        }
    }
}

// 定义在共享库中的变量要在 Global Variables Reference (在 Pipeline Syntax 中)中显
// 示,前提是Jenkins在一次成功的Pipeline运行中加载并使用该库


// 最佳实践:避免在全局变量中保留状态
// 避免定义“与方法交互或保留状态的”全局变量。应该使用“静态类”或“实例化类的局部变量”

创建自定义步骤

共享库还可以定义全局变量,其行为类似于内置步骤(例如 sh、git 等等)。在共享库中定义的全局变量必须使用所有全小写或驼峰命名,以便通Pipeline 正确加载。

例如,要定义sayHello()步骤,应创建vars/sayHello.groovy文件,并应实现call()方法。call()方法允许以类似于步骤的方式调用全局变量:

// vars/sayHello.groovy
def call(String name = 'human') {
    // Any valid steps can be called from this code, just like in other
    // Scripted Pipeline
    echo "Hello, ${name}."
}

然后 Pipeline 将能够引用和调用此变量:

sayHello 'Joe'
sayHello() /* invoke with default arguments */

如果使用块调用,则call()方法将接收 Closure 对象。应明确定义类型,以阐明该步骤的意图,例如:

// vars/windows.groovy
def call(Closure body) {
    node('windows') {
        body()
    }
}

然后 Pipeline 可以使用此变量,如同接受块的任何内置步骤一样:

windows {
    bat "cmd /?"
}

定义更加结构化的 DSL

如果你有很多大致相似的 Pipeline,全局变量机制提供了便利的工具,用于构建一个捕获相似性的更高级别的DSL。

例如,所有 Jenkins Plugin 都以相同的方式构建和测试,因此我们可以编写一个名为 buildPlugin 的步骤:

// vars/buildPlugin.groovy
def call(Map config) {
    node {
        git url: "https://github.com/jenkinsci/${config.name}-plugin.git"
        sh 'mvn install'
        mail to: '...', subject: "${config.name} plugin build", body: '...'
    }
}

假设脚本已作为全局共享库或文件夹级共享库加载,最后产生的 Jenkinsfile 将变得非常简单:

// Jenkinsfile (Scripted Pipeline)
buildPlugin name: 'git'

还有一个使用 Groovy 的 Closure.DELEGATE_FIRST 的“构建器模式”技巧,它允许 Jenkinsfile 看起来更像配置文件而不是程序,但这更复杂且容易出错,不建议使用。

使用第三方库

可以从信任的库代码中使用第三方的 Java 库,一般可以在 Maven Central 中找到,从受信库代码中使用@Grab注解。

有关详细信息,请参阅 Grape 文档,但只需输入:

@Grab('org.apache.commons:commons-math3:3.4.1')
import org.apache.commons.math3.primes.Primes

void parallelize(int count) {
  if (!Primes.isPrime(count)) {
    error "${count} was not prime"
  }
  // …
}

在默认情况下,第三方库会被缓存,在 Jenkins 主节点的~/.groovy/grapes/中。

加载资源(resources)

外部库可使用libraryResource来加载resources/中的资源。参数是相对路径,类似于在 Java 中的路径:

def request = libraryResource 'com/mycorp/pipeline/somelib/request.json'

该文件作为字符串加载,适合与传入确定API中,或者使用writeFile保存到工作目录中。

建议使用唯一的包结构来保存文件,防止和其他的类库冲突。

预先测试库修改

如果发现使用不受信任的库在构建中出现错误,只需单击“Replay”链接以尝试编辑其一个或多个源文件,并查看生成的构建是否按预期运行。对结果感到满意后,请从构建的状态页面中按照 diff 链接,将 diff 应用到库存储库并提交。

(即使为库请求的版本是分支,而不是像标记这样的固定版本,Replay版本将使用与原始版本完全相同的版本:库源码不会再次检出。)

目前,受信任的库不支持 Replay 。Replay 期间当前也不支持修改资源文件。

定义声明式流水

从2017年9月下旬发布的 Declarative 1.2 开始,也可以在共享库中定义 Declarative Pipelines 。这是一个示例,它将执行不同的声明性管道,具体取决于构建号是奇数还是偶数:

// vars/evenOrOdd.groovy
def call(int buildNumber) {
  if (buildNumber % 2 == 0) {
    pipeline {
      agent any
      stages {
        stage('Even Stage') {
          steps {
            echo "The build number is even"
          }
        }
      }
    }
  } else {
    pipeline {
      agent any
      stages {
        stage('Odd Stage') {
          steps {
            echo "The build number is odd"
          }
        }
      }
    }
  }
}

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

// Jenkinsfile
@Library('my-shared-library') _

evenOrOdd(currentBuild.getNumber())

到目前为止,只能在共享库中定义整个 Pipeline 。这只能在 vars/*.groovy 中完成,并且只能在 call() 中完成。在单个构建中只能执行单个 Declarative Pipeline ,如果您尝试执行第二个,那么构建将因此失败。

参考文献



Backlinks: Software Engineering:Continuous Delivery:Jenkins Pipeline:6.Extending with Shared Libraries

ToC

共享库开发

在类中访问步骤

方法一、“在类外部”

方法二、使用 this 关键字

在类中使用全局变量

定义全局变量(vars/)

在全局变量中定义方法

在全局变量中定义变量

调用全局变量

创建自定义步骤

定义更加结构化的 DSL

使用第三方库

加载资源(resources)

预先测试库修改

定义声明式流水

参考文献