devops [持續交付實踐] pipeline:pipeline 使用之 Shared Libraries

隨着pipeline交付流水線在團隊中的推廣,使用pipeline腳本的job也迅速增加。雖然我們已經基於公司的技術棧特點做了一個儘可能通用的pipeline腳本樣例,讓搭建者只需要修改幾個賦值參數就可以在自己的項目中應用,初衷是希望所有人能理解pipeline中的過程,但也發現一些比較麻煩的問題,比如有些人不熟悉具體的腳本拿來隨意刪改導致各種錯誤,還有就是我們在pipeline腳本中增加一些新功能時又需要通知所有的pipeline維護人員去修改,過程非常糾結。
這時候就意味着我們需要用到pipline的共享庫功能(Shared Libraries)了,在各種項目之間共享pipeline核心實現,以減少冗餘並保證所有job在構建的時候會調用最新的共享庫代碼 。
這篇我們就介紹下pipeline的這個黑科技:Shared Libraries

目錄結構

Shared Library通過庫名稱、代碼檢索方法(如SCM)、代碼版本三個要素進行定義,庫名稱儘量簡潔,因爲它會在腳本中被調用,在編寫 Shared Library的時候,我們需要遵循固定的代碼目錄結構。
Shared Library代碼目錄結構如下:


src目錄就是標準的Java源目錄結構。執行Pipeline時,該目錄將添加到類路徑中。
vars目錄託管定義可從Pipeline訪問的全局腳本(一般我們可以在這裏編寫標準化腳本)。通常,每個.groovy文件的基本名稱應使用駝峯(camelCased)模式,.txt(如果存在)可以包含格式化處理的文檔。
resources目錄允許libraryResource從外部庫中使用步驟來加載相關聯的非Groovy文件。目前內部庫不支持此功能。

定義全局庫

這裏只介紹全局 Shared Library的方式,通過Manage Jenkins » Configure System » Global Pipeline Libraries 的方式可以添加一個或多個共享庫。
這些庫將全局可用,系統中的任何Pipeline都可以利用這些庫中實現的功能。並且通過配置SCM的方式,可以保證在每次構建時獲取到指定Shared Library的最新代碼。

動態加載庫

從2.7版本起,Pipeline: Shared Groovy Libraries plugin插件提供了一個新的參數“library”,用於在腳本中加載(non-implicit)庫
如果只需要加載全局變量/函數(從vars/目錄中),語法非常簡單:
此後腳本中可以訪問該庫中的任何全局變量。

library 'my-shared-library'

採用此方式從src/目錄中引用類也是可以的,不過只能動態地使用庫類(無類型檢查),從library步驟的返回值通過指定名稱訪問它們。比如static可以使用類似Java的語法來調用方法:

library('my-shared-library').com.mycorp.pipeline.Utils.someStaticMethod()

使用該library步驟時,您還可以指定一個版本,該指定版本將會覆蓋默認版本。

library 'my-shared-library@master'

Shared Libraries實戰

我們在https://testerhome.com/topics/10010已經介紹了一個項目基本樣例,可以看到過程已經非常複雜(實際上我們後來還加了很多更復雜的功能),讓普通業務工程師管理起來確實有點困難。
通過參數化處理後,除了一些各項目的業務變量,整個過程在所有項目都是通用的,完全適合採用共享庫的方式進行改造,屏蔽腳本的複雜度。
在改造之前我們勾畫了兩種思路:pipeline模塊庫和模版庫(姑且這麼叫吧)。
1.模塊庫方式
模塊庫的方式,其實就是考慮把各個stage的實現通過函數化的方式抽象出來,比如獲取代碼的stage實現我們就抽象出codeFetch(),單元測試的 stage我們就抽象出unitTest().
特點:業務工程師負責維護pipeline的初始賦值和整體結構,靈活度高,可自主裁剪stage場景
不足:整體結構還是比較複雜,需要維護的共享腳本比較多,無法對交付流水線過程進行統一管理,Declarative Pipeline只支持script部分腳本的共享庫。
pipeline代碼樣例:

#!groovy
library 'weiyi-pipeline-library'
pipeline {
agent any
parameters {
//repoBranch參數
string(name:'repoBranch', defaultValue: 'master', description: 'git分支名稱')
//服務器選擇
choice(name: 'server',choices:'192.168.1.107,9090\n192.168.1.60,9090', description: '測試服務器列表選擇(IP,JettyPort,Name,Passwd)')
string(name:'dubboPort', defaultValue: '31100', description: '測試服務器的dubbo服務端口')
//單元測試代碼覆蓋率要求,各項目視要求調整參數
string(name:'lineCoverage', defaultValue: '20', description: '單元測試代碼覆蓋率要求(%),小於此值pipeline將會失敗!')
//若勾選在pipelie完成後會郵件通知測試人員進行驗收
booleanParam(name: 'isCommitQA',description: '是否在pipeline完成後,郵件通知測試人員進行人工驗收',defaultValue: false )
}
//環境變量,初始確定後一般不需更改
tools {
maven 'maven3'
jdk 'jdk8'
}
.......
//pipeline的各個階段場景
stages {
stage('代碼獲取') {
steps {
codeFetch()
}
}
stage('單元測試') {
steps {
unitTest()
}
}
}
}

共享庫代碼:

// vars/codeFetch.groovy 
def call() { echo "starting fetch code......" }

2.模版庫方式
Declarative 1.2(released in late September, 2017),開始支持整條Declarative Pipeline作爲共享庫,使用條件如下:

Only entire pipeline`s can be defined in shared libraries as of this time. This can only be done in `vars/*.groovy, and only in a callmethod. Only one Declarative Pipeline can be executed in a single build, and if you attempt to execute a second one, your build will fail as a result.

特點:可以將整條declarative pipeline作爲共享庫讓各個項目調用,業務工程師只需要維護初始化賦值參數即可。
不足:公司技術棧不統一的話,pipeline模版庫的適配能力需要比較強(比如可能會出現虛擬機/docker共存,gradle/maven共存等多種情況),可能需要定義多個模版庫,不過這些問題通過groovy代碼邏輯上應該都可以控制。
pipeline代碼樣例(敏感信息隱藏):

#!groovy
library 'weiyi-pipeline-library'
def map = [:]
/*參數化變量,運行時可選擇*/
//git分支名稱
map.put('repoBranch','master')
//測試服務器列表選擇(IP,JettyPort,Name,Passwd)
map.put('server','192.168.1.107,9090\n192.168.1.60,9090')
//測試服務器的dubbo服務端口
map.put('dubboPort','31100')
//單元測試代碼覆蓋率要求,各項目視要求調整參數
map.put('lineCoverage','20')

/*環境變量,初始確定後一般不需更改*/
map.put('maven','maven3')
map.put('jdk','jdk8')

/*常量參數,初始確定後一般不需更改*/
map.put("isDocker",false)
//項目gitlab代碼地址
map.put('REPO_URL','****')
//git服務全系統只讀賬號,無需修改
map.put('CRED_ID','****')
//pom.xml的相對路徑
map.put('POM_PATH','pom.xml')
//生成war包的相對路徑
map.put('WAR_PATH','rpc/war/target/*.war')
//測試人員郵箱地址
map.put('QA_EMAIL','***')
//接口測試job名稱
map.put('ITEST_JOBNAME','Guahao_InterfaceTest_ExpertPatient')

pipelineCall("maven",map)

共享庫代碼:

#!groovy
def call(String type,Map map) {
if (type == "maven") {
pipeline {
agent any
//參數化變量,目前只支持[booleanParam, choice, credentials, file, text, password, run, string]這幾種參數類型,其他高級參數化類型還需等待社區支持
parameters {
//固定設置三類pipeline場景
choice(name:'scene',choices:"scene1:完整流水線\nscene2:代碼檢查\nscene3:測試部署", description: '場景選擇,默認運行完整流水線,如果只做開發自測可選擇代碼檢查,如果只做環境部署可選擇測試部署')
//repoBranch參數後續替換成git parameter不再依賴手工輸入,JENKINS-46451
string(name:'repoBranch', defaultValue: "${map.repoBranch}", description: 'git分支名稱')
//服務器相關參數採用了組合方式,避免多次選擇
choice(name: 'server',choices:"${map.server}", description: '測試服務器列表選擇')
string(name:'dubboPort', defaultValue: "${map.dubboPort}", description: '測試服務器的dubbo服務端口')
//單元測試代碼覆蓋率要求,各項目視要求調整參數
string(name:'lineCoverage', defaultValue: "${map.lineCoverage}", description: '單元測試代碼覆蓋率要求(%),小於此值pipeline將會失敗!')
//若勾選在pipelie完成後會郵件通知測試人員進行驗收
booleanParam(name: 'isCommitQA', defaultValue: false, description: '是否在pipeline完成後,郵件通知測試人員進行人工驗收')
}
//環境變量,初始確定後一般不需更改
tools {
maven "${map.maven}"
jdk "${map.jdk}"
}
//常量參數,初始確定後一般不需更改
environment{
REPO_URL="${map.REPO_URL}"
//git服務全系統只讀賬號,無需修改
CRED_ID="${map.CRED_ID}"
//pom.xml的相對路徑
POM_PATH="${map.POM_PATH}"
//生成war包的相對路徑
WAR_PATH="${map.WAR_PATH}"
//測試人員郵箱地址
QA_EMAIL="${map.QA_EMAIL}"
//接口測試job名稱
ITEST_JOBNAME="${map.ITEST_JOBNAME}"
}

options {
disableConcurrentBuilds()
timeout(time: 1, unit: 'HOURS')
//保持構建的最大個數
buildDiscarder(logRotator(numToKeepStr: '10'))
}
post{

}
//pipeline的各個階段場景
stages {
stage('代碼獲取') {
steps {
//一些初始化操作
script {
//根據param.server分割獲取參數
def split=params.server.split(",")
serverIP=split[0]
jettyPort=split[1]
serverName=split[2]
serverPasswd=split[3]
//場景選擇
println params.scene
//單元測試運行場景
isUT=params.scene.contains('scene1:完整流水線') || params.scene.contains('scene2:代碼檢查')
println "isUT="+isUT
//靜態代碼檢查運行場景
isCA=params.scene.contains('scene1:完整流水線') || params.scene.contains('scene2:代碼檢查')
println "isCA="+isCA
//部署測試環境運行場景
isDP=params.scene.contains('scene1:完整流水線') || params.scene.contains('scene3:測試部署')
println "isDP="+isDP
//第三方庫安全性檢查
isDC=params.scene.contains('scene1:完整流水線')
println "isDC="+isDC
//接口測試運行場景
isIT=params.scene.contains('scene1:完整流水線')
println "isIT="+isIT
try{
wrap([$class: 'BuildUser']){
userEmail="${BUILD_USER_EMAIL},${QA_EMAIL}"
user="${BUILD_USER_ID}"
}
}catch(exc){
userEmail="${QA_EMAIL}"
user="system"
}
echo "starting fetchCode from ${REPO_URL}......"
// Get some code from a GitHub repository
git credentialsId:CRED_ID, url:REPO_URL, branch:params.repoBranch
}
}
}
stage('單元測試') {
when {
expression
{return isUT }
}

}
................................................................................................以下省略幾百行
else if (type == "gradle"){
pipeline {
agent any
................................................................................................繼續省略幾百行
}

通過這種方式,整個pipeline腳本的實現和複雜度就被封裝到Shared Library中。而且如果我要在原來的流水線基礎上,新增一個stage的代碼比如安全測試,或者需要對原有代碼邏輯進行修改,只需要庫開發人員修改共享庫的功能,相關場景的項目在map賦值時增加相應的賦值參數即可。

後記

Shared Libraries的方式抽象了各種項目之間共享的代碼(甚至整條完整的pipeline),有效降低了業務工程師使用pipeline腳本的複雜度。
同時通過外部源代碼控制(SCM)的方式,可以保證最新提供的庫代碼功能可被所有pipeline項目即時使用,在大團隊推廣和協作過程中可起到非常重要的作用。

主帖直達:https://testerhome.com/topics/9977

  </div>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章