一.簡介
當大量使用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插件擴展:適合需要高度定制化步驟的應用場景。