首發(fā)于 Jenkins 中文社區(qū)
本文介紹了筆者首個(gè) Jenkins 插件開(kāi)發(fā)的旅程,包括從產(chǎn)生 idea 開(kāi)始,然后經(jīng)過(guò)插件定制開(kāi)發(fā),接著申請(qǐng)將代碼托管到 jenkinsci GitHub 組織,最后將插件發(fā)布到 Jenkins 插件更新中心的過(guò)程。
鑒于文章篇幅過(guò)長(zhǎng),將分為上下兩篇進(jìn)行介紹。
從一個(gè) idea 說(shuō)起
前幾天和朋友聊天時(shí),聊到了 Maven 版本管理領(lǐng)域的 SNAPSHOT 版本依賴問(wèn)題,這給他帶來(lái)了一些困擾,消滅掉歷史遺留應(yīng)用的 SNAPSHOT 版本依賴并非易事。
類似問(wèn)題也曾經(jīng)給筆者帶來(lái)過(guò)困擾,在最初沒(méi)能去規(guī)避問(wèn)題,等到再想去解決問(wèn)題時(shí)卻發(fā)現(xiàn)困難重重,牽一發(fā)而動(dòng)全身,導(dǎo)致這個(gè)問(wèn)題一直被擱置,而這也給筆者留下深刻的印象。
等到再次制定 Maven 規(guī)范時(shí),從一開(kāi)始就考慮強(qiáng)制禁止 SNAPSHOT 版本依賴發(fā)到生產(chǎn)環(huán)境。
這里是通過(guò)在 Jenkins 構(gòu)建時(shí)做校驗(yàn)實(shí)現(xiàn)的。因?yàn)闆](méi)有找到提供類似功能的 Jenkins 插件,目前這個(gè)校驗(yàn)通過(guò) shell 腳本來(lái)實(shí)現(xiàn)的,具體的做法是在 Jenkins 任務(wù)中 Maven 構(gòu)建之前增加一個(gè) Execute shell 的步驟,來(lái)判斷 pom.xml 中是否包含 SNAPSHOT 關(guān)鍵字,如果包含,該次構(gòu)建狀態(tài)將被標(biāo)記為失敗。腳本內(nèi)容如下:
#!/bin/bash
if [[ ` grep -R --include="pom.xml" SNAPSHOT .` =~ "SNAPSHOT" ]];
then echo "SNAPSHOT check failed" && grep -R --include="pom.xml" SNAPSHOT . && exit 1;
else echo "SNAPSHOT check success";
fi
恰好前不久在看 Jenkins 插件開(kāi)發(fā)文檔,那何不通過(guò) Jenkins 插件的方式實(shí)現(xiàn)它呢?
于是筆者開(kāi)始了首個(gè) Jenkins 插件開(kāi)發(fā)之旅。
插件開(kāi)發(fā)過(guò)程
Jenkins 是由 Java 語(yǔ)言開(kāi)發(fā)的最流行的 CI/CD 引擎。
說(shuō)起 Jenkins 強(qiáng)大的開(kāi)源生態(tài),自然就會(huì)說(shuō)到 Jenkins 插件。Jenkins 插件主要用來(lái)對(duì) Jenkins 的功能進(jìn)行擴(kuò)展。目前 Jenkins 社區(qū)有上千個(gè)插件,用戶可以根據(jù)自己的需求選擇合適的插件來(lái)定制 Jenkins 。
插件開(kāi)發(fā)準(zhǔn)備
插件開(kāi)發(fā)需要首先安裝 JDK 和 Maven,這里不做進(jìn)一步說(shuō)明。
創(chuàng)建一個(gè)插件
Jenkins 為插件開(kāi)發(fā)提供了 Maven 原型。打開(kāi)一個(gè)命令行終端,切換到你想存放 Jenins 插件源代碼的目錄,運(yùn)行如下命令:
mvn -U archetype:generate -Dfilter=io.jenkins.archetypes:
這個(gè)命令允許你使用其中一個(gè)與 Jenkins 相關(guān)的原型生成項(xiàng)目。
$ mvn -U archetype:generate -Dfilter=io.jenkins.archetypes:
......
Choose archetype:
1: remote -> io.jenkins.archetypes:empty-plugin (Skeleton of a Jenkins plugin with a POM and an empty source tree.)
2: remote -> io.jenkins.archetypes:global-configuration-plugin (Skeleton of a Jenkins plugin with a POM and an example piece of global configuration.)
3: remote -> io.jenkins.archetypes:hello-world-plugin (Skeleton of a Jenkins plugin with a POM and an example build step.)
Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): : 3
Choose io.jenkins.archetypes:hello-world-plugin version:
1: 1.1
2: 1.2
3: 1.3
4: 1.4
Choose a number: 4: 4
......
[INFO] Using property: groupId = unused
Define value for property 'artifactId': maven-snapshot-check
Define value for property 'version' 1.0-SNAPSHOT: :
[INFO] Using property: package = io.jenkins.plugins.sample
Confirm properties configuration:
groupId: unused
artifactId: maven-snapshot-check
version: 1.0-SNAPSHOT
package: io.jenkins.plugins.sample
Y: : Y
筆者選擇了 hello-world-plugin
這個(gè)原型,并在填寫(xiě)了一些參數(shù),如artifactId、version 后生成了項(xiàng)目。 可以使用 mvn verify
命令驗(yàn)證是否可以構(gòu)建成功。
構(gòu)建及運(yùn)行插件
Maven HPI Plugin
用于構(gòu)建和打包 Jenkins 插件。它提供了一種便利的方式來(lái)運(yùn)行一個(gè)已經(jīng)包含了當(dāng)前插件的 Jenkins 實(shí)例:
mvn hpi:run
這將安裝一個(gè) Jenkins 實(shí)例,可以通過(guò)http://localhost:8080/jenkins/
來(lái)訪問(wèn)。等待控制臺(tái)輸出如下內(nèi)容,然后打開(kāi) Web 瀏覽器并查看插件的功能。
INFO: Jenkins is fully up and running
在 Jenkins 中創(chuàng)建一個(gè)自由風(fēng)格的任務(wù),然后給它取個(gè)名字。然后添加 "Say hello world" 構(gòu)建步驟,如下圖所示:
輸入一個(gè)名字,如:Jenkins ,然后保存該任務(wù),點(diǎn)擊構(gòu)建,查看構(gòu)建日志,輸出如下所示:
Started by user anonymous
Building in workspace /Users/mrjenkins/demo/work/workspace/testjob
Hello, Jenkins!
Finished: SUCCESS
定制開(kāi)發(fā)插件
Jenkins 插件開(kāi)發(fā)歸功于有一系列擴(kuò)展點(diǎn)。開(kāi)發(fā)人員可以對(duì)其進(jìn)行擴(kuò)展自定義實(shí)現(xiàn)一些功能。
這里有幾個(gè)重要的概念需要做下說(shuō)明:
擴(kuò)展點(diǎn)( ExtensitonPoint )
擴(kuò)展點(diǎn)是 Jenkins 系統(tǒng)某個(gè)方面的接口或抽象類。
這些接口定義了需要實(shí)現(xiàn)的方法,而 Jenkins 插件需要實(shí)現(xiàn)這些方法。
筆者所寫(xiě)的插件需要實(shí)現(xiàn) Builder 這個(gè)擴(kuò)展點(diǎn)。
代碼片段如下:
public class MavenCheck extends Builder {}
Descriptor 靜態(tài)內(nèi)部類
Descriptor 靜態(tài)內(nèi)部類是一個(gè)類的描述者,用于指明這是一個(gè)擴(kuò)展點(diǎn)的實(shí)現(xiàn),Jenkins 通過(guò)這個(gè)描述者才能知道我們寫(xiě)的插件。每一個(gè)描述者靜態(tài)類都需要被 @Extension 注解,Jenkins 內(nèi)部會(huì)掃描 @Extenstion 注解來(lái)獲取注冊(cè)了哪些插件。
代碼片段如下:
@Extension
public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
public DescriptorImpl() {
load();
}
@Override
public boolean isApplicable(Class<? extends AbstractProject> aClass) {
return true;
}
@Override
public String getDisplayName() {
return "Maven SNAPSHOT Check";
}
}
在 DesciptorImpl 實(shí)現(xiàn)類中有兩個(gè)方法需要我們必須要進(jìn)行重寫(xiě):
isApplicable() 和 getDisplayName() 。isApplicable() 這個(gè)方法的返回值代表這個(gè) Builder 在 Jenkins Project 中是否可用,我們可以將我們的邏輯寫(xiě)在其中,例如做一些參數(shù)校驗(yàn),最后返回 true 或 false 來(lái)決定這個(gè) Builder 是否可用。
getDisplayName() 這個(gè)方法返回的是一個(gè) String 類型的值,這個(gè)名稱被用來(lái)在 web 界面上顯示。
數(shù)據(jù)綁定
前端頁(yè)面的數(shù)據(jù)要和后臺(tái)服務(wù)端進(jìn)行交互,需要進(jìn)行數(shù)據(jù)綁定。
前端 config.jelly
頁(yè)面代碼片段如下:
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<f:entry title="check" field="check">
<f:checkbox />
</f:entry>
</j:jelly>
如上所示,需要在 config.jelly 中包含需要傳入的參數(shù)配置信息的選擇框,field 為 check ,這樣可以在 Jenkins 進(jìn)行配置,然后通過(guò)DataBoundConstructor 數(shù)據(jù)綁定的方式,將參數(shù)傳遞到 Java 代碼中。服務(wù)端 Java 代碼片段如下:
@DataBoundConstructor
public MavenCheck(boolean check) {
this.check = check;
}
核心邏輯
筆者所寫(xiě)的插件的核心邏輯是檢查 Maven pom.xml 文件是否包含 SNAPSHOT 版本依賴。
Jenkins 是 Master/Agent 架構(gòu),
這就需要讀取 Agent 節(jié)點(diǎn)的 workspace 的文件,
這是筆者在寫(xiě)插件時(shí)遇到的一個(gè)難點(diǎn)。
Jenkins 強(qiáng)大之處在于它的生態(tài),目前有上千個(gè)插件,
筆者參考了 Text-finder Plugin 的源碼,
并在參考處添加了相關(guān)注釋,最終實(shí)現(xiàn)了插件要實(shí)現(xiàn)的功能。
詳細(xì)代碼可以查看 jenkinsci/maven-snapshot-check-plugin 代碼倉(cāng)庫(kù)。
分發(fā)插件
使用 mvn package
命令可以打包出后綴為 hpi 的二進(jìn)制包,
這樣就可以分發(fā)插件,將其安裝到 Jenkins 實(shí)例。
插件使用說(shuō)明
以下是對(duì)插件的使用簡(jiǎn)要描述。
如果勾選了下面截圖中的選擇框,
Jenkins 任務(wù)在構(gòu)建時(shí)將會(huì)檢查 pom.xml 中是否包含 SNAPSHOT 。
如果檢查到的話,則會(huì)將該次構(gòu)建狀態(tài)標(biāo)記為失敗。
總結(jié)
文章上篇主要介紹了從產(chǎn)生 idea 到插件開(kāi)發(fā)完成的過(guò)程。 那么插件在開(kāi)發(fā)完成后是如何將它托管到 Jenkins 插件更新中心讓所有用戶都可以看到的呢? 兩天后的文章下篇將對(duì)這個(gè)過(guò)程進(jìn)行介紹,敬請(qǐng)期待!
參考
作者:王冬輝