一.简介
当大量使用pipeline后,内置功能并不能照顾到所有需求,这时候需要扩展pipeline。
pipeline本质就是一个Groovy脚本。所以,可以在pipeline中定义函数,并使用Groovy语言自带的脚本特性。我们定义了createVersion函数,并使用了Date类
def createVersion(String BUILD_NUMBER) {
return new Date().format( 'yyMM' ) + "-${BUILD_NUMBER}"
}
pipeline {
agent any
stages {
stage('Build') {
steps {
echo "${createVersion(BUILD_NUMBER)}"
}
}
}
}
日志如下:
还有一种更优雅的写法,将变量定义在environment部分
def createVersion(String BUILD_NUMBER) {
return new Date().fromat( 'yyMM' ) + "-${BUILD_NUMBER}"
}
pipeline {
agent any
environment {
_version = createVersion(BUILD_NUMBER)
}
stages {
stage('Build') {
steps {
echo "${_version}"
}
}
}
}
如果在一个Jenkinsfile定义一个函数,倒是无伤大雅。但是如果再20个Jenkinsfile中重复定义这个函数20遍,就有问题了。
二.共享库扩展
Jenkins pipeline提供了“共享库”(Shared library)技术,可以将重复代码定义在一个独立的代码控制仓库中,其他的Jenkins pipeline加载使用它。类似编程中的模块包(实际就是),可以引用其它方法,直接在当前pipeline使用。
创建共享库项目,目录结构如下
将代码推送到git仓库中,进入Jenkins的Manage Jenins-》Configure System -》Global Pipeline Libraries配置页面
配置项:
- Name :共享库的唯一标识,在Jenkinsfile中会使用到。. Default version :默认版本。可以是分支名、tag标签等。
- Load implicitly:隐式加载。如果勾选此项,将自动加载全局共享库,在Jenkinsfile中不需要显式引用,就可以直接使用。
- Allow default version to be overridden :如果勾选此项,则表示允许“Default version”被Jenk-insfile中的配置覆盖。
- lnclude@Library changes in job recent changes:如果勾选此项,那么共享库的最后变更信息会跟项目的变更信息一起被打印在构建日志中。
.- Retrieval method:获取共享库代码的方法。我们选择Modern SCM”选项,进而选择使用Git仓库。
提示:除了可以使用Git仓库托管共享库代码,还可以使用SVN仓库托管。使用不同的代码仓库托管,“Default version”的值的写法不一样。本书只介绍Git仓库托管方式。
共享库使用
在pipeline里调用
@Library( 'global-shared-library')_
pipeline {
agent any
stages {
stage('Build') {
steps {
sayHello( "world")
}
}
}
}
在Jenkins pipeline的顶部,使用@Library指定共享库。注意,global-shared-library就是我们在上一个步骤中定义的共享库标识符。
引入共享库后,我们可以直接在Jenkins pipeline中使用vars目录下的sayHello,和Jenkins pipeline的普通步骤的使用方式无异。
至此,一个共享库的完整定义和基本使用就介绍完了。总结下来就四步:
1.按照共享库约定的源码结构,实现自己的逻辑。
2.将共享库代码托管到代码仓库中。
3.在Jenkins全局配置中定义共享库,以让Jenkins知道如何获取共享库代码。
4.在Jenkinsfile中使用@Library引用共享库。
使用@Library注解可以指定共享库在代码仓库中的版本。
@Library('global-shared-library@<version>')_
<version>
可以是:
- 分支,如@Library ( 'global-shared-library@dev')。
- tag标签,如@Library ( 'global-shared-library@release1.0' )
- git commit id,如@Library ( 'global-shared-library@e88d44e73fea304905dc00a1af 2197d945aa1a36')。
因为Jenkins支持同时添加多个共享库,所以@Library注解还允许我们同时引入多个共享库,如:@Library ( ['global-shared-library' , 'otherlib@abc1234])。
需要注意的是,Jenkins处理多个共享库出现同名函数的方式是先定义者生效。也就是说,如果global-shared-library与otherlib存在同名的sayHello,而@Library引入时global-shared-library在otherlib前,那么就只有global-shared-library的sayHello生效。
共享库结构
回顾目录
首先看vars目录。
放在vars目录下的是可以是从pipeline直接调用的全局变量,变量的文件名即为在pipline中调用的函数名,文件名为驼峰式的。
使用vars目录下的全局变量可以调用Jenkins pipeline的步骤。正如sayHello.groovy脚本,直接使用echo步骤
def call(String name = 'human') {
echo "Hello, ${name}."
}
当我们在Jenkins中写sysHello("world")时,它实际调用的是sysHello.groovy文件中的call函数。
call函数还支持接收闭包(Closure),下例中,我们定义了一个mvn全局变量。
// vars/mvn.groovy
def call(mvnExec) {
configFileProvider([configFile(fileId:'maven-global-settings', variable:'MAVEN_GLOBAL_ENV')]) {
mvnExec("${MAVEN_GLOBAL_ENV}")
}
}
以上call函数里的内容就是将configFileProvider啰嗦的写法封装在mvn变量中。这样我们就可以更简洁的执行mvn命令了。
@Libray('global-shared-library@master') _
pipeline {
agent any
tools {
maven 'mvn-3.5.4
}
stages {
stage('Build') {
steps {
mvn { settings ->
sh "mvn -s ${settings} clean install"
}
}
}
}
}
接着我们来看src目录
src目录是一个标准的java源码架构,目录中的类被称为库类Library class。而@Library('global-shared-library@dev')中的代表一次性静态加载src目录下的所又代码到classpath中。
Utils.groovy代码如下
package codes.showme
class Utils implements Serializable {
def getVersion(String BUILD_NUMBER, String GIT_COMMIT){
return new Date().format( 'yyMM' ) + "-${BUILD_NUMBER}" + "-${GIT_COMMIT}"
}
}
提示:Utils实现了Serializable接口,是为了确保当pipeline被Jenkins挂起后能正确恢复。
在使用src目录中的类时,需要使用全包名。同时,因为写的是Groovy代码,所以还需要使用script指令抱起来。
@Library(['global-shared-library']) _
pipeline {
agent any
stages {
stage('Build') {
steps {
script{
def util = new codes.showme.Utils()
def v = util.getVersion("${BUILD_NUMBER}", "${GIT_COMMIT}")
echo "${v}"
}
}
}
}
}
src目录中的类,还可以使用Groovy的@Grab注解,自动下载第三方依赖包
package codes.showme
@Grab(group='org.apache.commons', module='commons-lang3', version='3.6')
import org.apache.commons.lang3.StringUtils
class Utils implements Serializable {
def isAlphanumeric(String aString){
return StringUtils.isAlphanumeric(aString)
}
}
不推荐大量使用@Grab,因为会带来维护困难的问题
pipeline模板
声明式pipeline在1.2版本后,可以在共享库中定义pipeline。通过此特性,我们可以定义pipeline的模板,根据不同的语言执行不同的pipeline。共享库代码如下:
// vars/generatePipeline.groovy
def cal1(String lang) {
if (lang== 'go') {
pipeline {
agent any
stages {
stage('set GOPATH') {
steps {
echo "GOPATH is ready"}
}
}
}
}
}
} else if(lang == 'java ') {
pipeline {
agent any
stages {
stage('clean install') {
steps {
sh "mvn clean install"
}
}
}
}
}
}
使用时,Jenkinsfile就2行:
@Library(['global-shared-library'])_
generatePipeline('go ')
如果大多是项目的Jenkinsfile是标准化的,可以用模板方式
一些小问题
共享库方式,如果这个jenkinsfile在远程代码仓上,会把这些jenkinsfile都下载到job的工作目录下面
这样可能有些pipeline脚本名如果和项目名或者文件名重复的话,就会出问题。
例如git clone 下载的代码是和pipeline名字一致,就会导致下载失败
三.共享库例子
使用公共变量
1.变量文件
src/codes/showme/GlobalVars.groovy
//存储环境变量
package codes.showme //这里指定文件所在目录位置
class GlobalVars { //名称要和文件名一致
static String gitlab_url = "http://10.0.15.1"
static String script_deploy_edas = "/jen_script/deploy-edas.sh"
static String script_ding_notice = "/jen_script/dingding.py"
}
2.假如使用jenkinsfile直接调用
@Library('pipeline-library-demo')_
import codes.showme.GlobalVars
pipeline {
agent any
stages {
stage('pull') {
steps {
echo "${GlobalVars.gitlab_url}"
}
}
}
}
3.假如在共享库vars下的pipeline模板中用,先在jenkinsfile里引用共享库
jenkinsfile
@Library('pipeline-library-demo')_
PipelineTemp()
vars/PipelineTemp.groovy
import codes.showme.GlobalVars
def call() {
pipeline {
agent any
stages {
stage('pull') {
steps {
script {
echo "${GlobalVars.gitlab_url}"
}
}
}
}
}
}
使用共享库的src方法
1.编写一个方法,用于处理操作的
src/codes/showme/DefExample.groovy
package codes.showme
//方法作用就是传参,然后打印这个参数
def readname(String BUILD_NUMBER) {
println(BUILD_NUMBER)
}
return this //如果方法里没写return则这里要加这行
2.进行调用
jenkinsfile
@Library('pipeline-fuction') _
pipeline {
agent any
stages {
stage('Example') {
steps {
script {
def defex = new codes.showme.DefExample()
defex.readname("xxx")
}
}
}
}
}
3.如果放到pipeline模板里中则
def call() {
def defex = new codes.showme.DefExample()
pipeline {
agent any
stages {
stage('Example') {
steps {
script {
defex.readname("xxxxx")
}
}
}
}
}
}
使用共享库的vars方法
1.编写,里面只能有一个call方法
vars/Ceshi.groovy
def call() {
println("xxxxxxxxxxxxxxxxxx")
}
2.调用,在jenkinsfile和pipeline模板中都是直接使用即可
@Library('pipeline-fuction') _
pipeline {
agent any
stages {
stage('pull') {
steps {
Ceshi()
}
}
}
}
四.插件实现pipeline
根据Jenkins插件的用法,将插件开发分成:通过界面使用插件和通过代码使用插件。
生成Jenkins插件代码骨架,非常简单,使用mvn命令就可以了。
mvn archetype:generate \
-DarchetypeGroupId=io.jenkins.archetypes \
-DarchetypeArtifactId=hello-word-plugin \
-DarchetypeVersion=1.4 \
-DartifactId=ansible-try \
-Dversion=1.0 \
-DinteractiveMode=false
HelloWorldBuilder: pipeline步骤具体实现的类
index.jelly: 在插件管理页面显示的HTML内容。
生成的代码骨架架构如下
HelloWorldBuilder : pipeline步骤具体实现的类
index.jelly :在插件管理页面显示的HTML内容
<?jelly escape-by-default='true'?>
<div>
TODO
</div>
pom.xml:其name属性值就是插件管理页面中的插件名称。Jenkins插件代码骨架的默认值为
<name>TODO Plugin</name>
HelloWorldBuilderTest:单元测试类
接下来,我们来看HelloWorldBuilder类
public class HelloWorldBuilder extends Builder implements SimpleBuildStep {
private final String name;
private boolean useFrench;
@DataBoundConstructor
public HelloWorldBuilder(String name) {
this.name = name;
}
public String getName() { return name; }
public boolean isUseFrench() { return useFrench; }
@DataBoundSetter
public void setUseFrench(boolean useFrench) {
this.useFrench = useFrench;
}
@Override
public void perform(Run<?, ?> run,
filePath workspace,
Launcher launcher,
TaskListener listener) throws InterruptedException, IOException {
//插件的执行逻辑
}
@Override
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.BUILD;
}
@Symbol("greet")
@Extension
public static final class DescriptorImpl extends BuildStepMonitor<Builder> {
//省略
}
}
- HelloWorldBuilder:需要继承Builder,并实现SimpleBuildStep接口的perform方法。
- @DataBoundConstructor:标识插件类的构造函数,name属性为插件默认属性,也是调用插件时的必要参数
- @DataBoundSetter:标识插件属性的setter方法。
- getRequiredMonitorService:返回BuildStepMonitor枚举类型,BuildStepMonitor决定了perform方法的执行机制。BUILD的执行机制为
public boolean perform(BuildStep bs,AbstractBuild,Launcher launcher,BuildListener listener)
throws IOException, InterruptedException{
if (bs instanceof Describable) {
CheckPoint.COMPLETED.block(listener, ((Describable) bs).getDescriptor().getDisplayName());
} else {
CheckPoint.COMPLETED.block();
}
}
return bs.perform(build,launcher,listener);
}
可以看出,在执行插件perform方法前,还做了不少工作。BuildStepMonitor还提供了其他值,具体可以看其源码。
- Descriptorlmpl 内部静态类,用于高速Jenkins关于此插件的元数据。
- @Extension:扩展点注解,Jenkins通过此注解自动发现扩展点,并将扩展点加入扩展点列表(ExtensionList)中。
- @Symbol 一个扩展点的唯一标识,被定义在扩展点上,可以理解为在pipeline中使用插件时所引用的函数名。本次扩展点为greet
mvn hpi:run
如果写好一个插件,启动Jenkins,然后手动将Jenkins安装,测试。调整后,再卸载,安装,测试,会很慢
所以,Jenkins插件开发的代码骨架,默认提供了用于Jenkins插件开发的Maven插件:maven-hpi-plugin。通过该插件,只需要实现插件代码,然后执行mvn hpi:run,就可以启动一个安装了插件的Jenkins实例,直接访问jenkins即可。
进入Jenkins插件管理页面,可以在已安装列表中找到正在开发的插件。
如果需要对插件进行调整,也只是停止该Jenkins实例,再启动就行了。
如果进行单点调试,则结合IDE,使用Debug模式执行mvn hpi:run即可
greet步骤
Jenkins插件已经准备好了,现在开始使用插件。
pipeline {
agent any
stages {
stage('Build') {
steps {
greet 'build'
}
}
}
post {
always {
greet "always"
}
}
}
greet是我们通过@Symbol定义的插件扩展点名称。可以将这个名称理解为一个函数名。而使用@DataBoundConstructor注解的构造参数、@DataBoundSetter注解的setter方法,可以理解为这个函数的参数。
所以,greet "build"更完整的写法为:gree(name:"build", useFrench:false)。
值得注意的是,虽然插件继承的是Builder类,但是也可以直接在post部分使用。
插件全局配置
插件全局配置的作用在于简化了使用步骤的参数设置。就像在使用mail步骤时,不可能每次调用都带上邮箱服务器的配置,而且还有利于保持配置的一致性。
1.加入配置类
package io.jenkins.plugins.sample;
// 继承于GlobalConfiguration,@Extension注解一个都不能少
@Extension
public class HellowordGlobalConfig extends 继承于GlobalConfiguration {
//自定义配置项,记得增加getter和setter方法
private String language;
public String getLanguage() { return language; }
public void setLanguage(String language) { this.language = language; }
//当用户打开全局页面时,从配置文件中加载配置
public HellowordGlobalConfigW() {
load();
}
@Overrid
protected XmlFile getConfigFile() {
Jenkins j = Jenkins.getInstance();
if (j == null) {return null;}
//一般将每个插件的全局配置文件保存在JENKINS_HOME目录下
file rootDir = j.getRootDir();
//插件全局配置文件名,每个插件各管各的全局配置
File xmlFile = new File(rootDir, "jenkins.plugins.sample.HelloWorld.xml");
return new XmlFile(xmlFile);
}
//工具方法,方便取HelloWorldGlobalConfig的值
public static HellowordGlobalConfig get() {
return GlobalConfiguration.all()
.get(HellowordGlobalConfig.class);
}
//当用户在界面中保存配置时,将配置保存到全局配置文件中
@Override
public boolean configure(StaplerRequest req, JSONObject json) {
req.bindJSON(this, json);
save();
return true;
}
}
2.加入全局配置页面。在src/main/resources目录中,根据类路径创建HelloWorldGlob-alConfig子目录。结构如下:
HelloWorldGlobalConfig下的config.jelly内容如下:
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:section title="Hello world global config">
<f:entry title="Language" field="language">
<f:textbox />
</f:entry>
</f:section>
</j:jelly>
进入系统设置页面,就可以找到我们的插件配置
3.使用配置 在HelloWorldBuilder类中通过HelloWorldGlobalConfig.get() .getLanguage() 拿到全局配置language的值。
@Override
public void perform(Run<?, ?> run, FilePath workspace, Launcher launcher, TaskListener listener) throws InterruptedExcaption, IOExcaption {
String language = HelloWorldGlobalConfig.get().getLanguage();
listener.getLogger().println(" Hello, " + name + "!," + language);
}
以下几种扩展pipeline的方式各有各的应用场景。
在Jenkins pipeline中自定义函数:简单、直观,但是容易产生重复代码。此种方式适合解决特定pipeline的问题。不推荐经常使用。
通过共享库扩展:在脚本中使用Jenkins现有的步骤,非常简单。此种方式适合对现有啰嗦的pipeline写法进行抽象,还适合做pipeline模板。
通过Jenkins插件扩展:适合需要高度定制化步骤的应用场景。