Jenkins 插件開發之旅:兩天內從 idea 到發佈(上篇)

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文介紹了筆者首個 Jenkins 插件開發的旅程, 包括從產生 idea 開始,然後經過插件定製開發, 接着申請將代碼託管到 jenkinsci GitHub 組織, 最後將插件發佈到 Jenkins 插件更新中心的過程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鑑於文章篇幅過長,將分爲上下兩篇進行介紹。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"從一個 idea 說起"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前幾天和朋友聊天時,聊到了 Maven 版本管理領域的 SNAPSHOT 版本依賴問題, 這給他帶來了一些困擾,消滅掉歷史遺留應用的 SNAPSHOT 版本依賴並非易事。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"類似問題也曾經給筆者帶來過困擾,在最初沒能去規避問題, 等到再想去解決問題時卻發現困難重重,牽一髮而動全身, 導致這個問題一直被擱置,而這也給筆者留下深刻的印象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"等到再次制定 Maven 規範時,從一開始就考慮 強制禁止 SNAPSHOT 版本依賴發到生產環境。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏是通過在 Jenkins 構建時做校驗實現的。 因爲沒有找到提供類似功能的 Jenkins 插件, 目前這個校驗通過 shell 腳本來實現的, 具體的做法是在 Jenkins 任務中 Maven 構建之前增加一個 Execute shell 的步驟, 來判斷 pom.xml 中是否包含 SNAPSHOT 關鍵字,如果包含,該次構建狀態將被標記爲失敗。 腳本內容如下:"}]},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":"#!/bin/bash\nif [[ ` grep -R --include=\"pom.xml\" SNAPSHOT .` =~ \"SNAPSHOT\" ]]; \nthen echo \"SNAPSHOT check failed\" && grep -R --include=\"pom.xml\" SNAPSHOT . && exit 1; \nelse echo \"SNAPSHOT check success\"; \nfi"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"恰好前不久在看 Jenkins 插件開發文檔, 那何不通過 Jenkins 插件的方式實現它呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"於是筆者開始了首個 Jenkins 插件開發之旅。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"插件開發過程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Jenkins 是由 Java 語言開發的最流行的 CI/CD 引擎。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"說起 Jenkins 強大的開源生態,自然就會說到 Jenkins 插件。 Jenkins 插件主要用來對 Jenkins 的功能進行擴展。 目前 Jenkins 社區有"},{"type":"link","attrs":{"href":"https://plugins.jenkins.io/","title":null},"content":[{"type":"text","text":"上千個插件"}]},{"type":"text","text":", 用戶可以根據自己的需求選擇合適的插件來定製 Jenkins 。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"插件開發準備"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"插件開發需要首先安裝 JDK 和 Maven,這裏不做進一步說明。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"創建一個插件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Jenkins 爲插件開發提供了 Maven 原型。 打開一個命令行終端,切換到你想存放 Jenins 插件源代碼的目錄,運行如下命令:"}]},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":"mvn -U archetype:generate -Dfilter=io.jenkins.archetypes:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個命令允許你使用其中一個與 Jenkins 相關的原型生成項目。"}]},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":"$ mvn -U archetype:generate -Dfilter=io.jenkins.archetypes:\n......\nChoose archetype:\n1: remote -> io.jenkins.archetypes:empty-plugin (Skeleton of a Jenkins plugin with a POM and an empty source tree.)\n2: remote -> io.jenkins.archetypes:global-configuration-plugin (Skeleton of a Jenkins plugin with a POM and an example piece of global configuration.)\n3: remote -> io.jenkins.archetypes:hello-world-plugin (Skeleton of a Jenkins plugin with a POM and an example build step.)\nChoose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): : 3\nChoose io.jenkins.archetypes:hello-world-plugin version:\n1: 1.1\n2: 1.2\n3: 1.3\n4: 1.4\nChoose a number: 4: 4\n......\n[INFO] Using property: groupId = unused\nDefine value for property 'artifactId': maven-snapshot-check\nDefine value for property 'version' 1.0-SNAPSHOT: :\n[INFO] Using property: package = io.jenkins.plugins.sample\nConfirm properties configuration:\ngroupId: unused\nartifactId: maven-snapshot-check\nversion: 1.0-SNAPSHOT\npackage: io.jenkins.plugins.sample\n Y: : Y"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"筆者選擇了 "},{"type":"codeinline","content":[{"type":"text","text":"hello-world-plugin"}]},{"type":"text","text":" 這個原型, 並在填寫了一些參數,如artifactId、version 後生成了項目。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以使用 "},{"type":"codeinline","content":[{"type":"text","text":"mvn verify"}]},{"type":"text","text":" 命令驗證是否可以構建成功。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"構建及運行插件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"Maven HPI Plugin"}]},{"type":"text","text":" 用於構建和打包 Jenkins 插件。 它提供了一種便利的方式來運行一個已經包含了當前插件的 Jenkins 實例:"}]},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":"mvn hpi:run"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這將安裝一個 Jenkins 實例,可以通過 "},{"type":"codeinline","content":[{"type":"text","text":"http://localhost:8080/jenkins/"}]},{"type":"text","text":" 來訪問。 等待控制檯輸出如下內容,然後打開 Web 瀏覽器並查看插件的功能。"}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"INFO: Jenkins is fully up and running"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Jenkins 中創建一個自由風格的任務,然後給它取個名字。 然後添加 “Say hello world” 構建步驟,如下圖所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/03/0301ae99d62070d16fb169650d3b64dc.png","alt":"say hello world","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"輸入一個名字,如:Jenkins ,然後保存該任務, 點擊構建,查看構建日誌,輸出如下所示:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"Started by user anonymous\nBuilding in workspace /Users/mrjenkins/demo/work/workspace/testjob\nHello, Jenkins! \nFinished: SUCCESS"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"定製開發插件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Jenkins 插件開發歸功於有一系列擴展點。 開發人員可以對其進行擴展自定義實現一些功能。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏有幾個重要的概念需要做下說明:"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"擴展點( ExtensitonPoint )"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"擴展點是 Jenkins 系統某個方面的接口或抽象類。 這些接口定義了需要實現的方法,而 Jenkins 插件需要實現這些方法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"筆者所寫的插件需要實現 Builder 這個擴展點。 代碼片段如下:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public class MavenCheck extends Builder {}"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Descriptor 靜態內部類"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Descriptor 靜態內部類是一個類的描述者,用於指明這是一個擴展點的實現, Jenkins 通過這個描述者才能知道我們寫的插件。 每一個描述者靜態類都需要被 @Extension 註解, Jenkins 內部會掃描 @Extenstion 註解來獲取註冊了哪些插件。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代碼片段如下:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@Extension\npublic static final class DescriptorImpl extends BuildStepDescriptor {\n public DescriptorImpl() {\n load();\n }\n\n @Override\n public boolean isApplicable(Class extends AbstractProject> aClass) {\n return true;\n }\n\n @Override\n public String getDisplayName() {\n return \"Maven SNAPSHOT Check\";\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 DesciptorImpl 實現類中有兩個方法需要我們必須要進行重寫: isApplicable() 和 getDisplayName() 。 isApplicable() 這個方法的返回值代表這個 Builder 在 Jenkins Project 中是否可用, 我們可以將我們的邏輯寫在其中,例如做一些參數校驗, 最後返回 true 或 false 來決定這個 Builder 是否可用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"getDisplayName() 這個方法返回的是一個 String 類型的值, 這個名稱被用來在 web 界面上顯示。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"數據綁定"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前端頁面的數據要和後臺服務端進行交互,需要進行數據綁定。 前端 "},{"type":"codeinline","content":[{"type":"text","text":"config.jelly"}]},{"type":"text","text":" 頁面代碼片段如下:"}]},{"type":"codeblock","attrs":{"lang":"html"},"content":[{"type":"text","text":"\n\n \n \n \n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如上所示,需要在 config.jelly 中包含需要傳入的參數配置信息的選擇框,field 爲 check ,這樣可以在 Jenkins 進行配置,然後通過 DataBoundConstructor 數據綁定的方式,將參數傳遞到 Java 代碼中。 服務端 Java 代碼片段如下:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@DataBoundConstructor\npublic MavenCheck(boolean check) {\n this.check = check;\n}"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"核心邏輯"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"筆者所寫的插件的核心邏輯是檢查 Maven pom.xml 文件是否包含 SNAPSHOT 版本依賴。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Jenkins 是 Master/Agent 架構, 這就需要讀取 Agent 節點的 workspace 的文件, 這是筆者在寫插件時遇到的一個難點。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Jenkins 強大之處在於它的生態,目前有上千個插件, 筆者參考了 "},{"type":"link","attrs":{"href":"https://plugins.jenkins.io/text-finder","title":null},"content":[{"type":"text","text":"Text-finder Plugin"}]},{"type":"text","text":" 的源碼, 並在參考處添加了相關注釋,最終實現了插件要實現的功能。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"詳細代碼可以查看 "},{"type":"link","attrs":{"href":"https://github.com/jenkinsci/maven-snapshot-check-plugin","title":null},"content":[{"type":"text","text":"jenkinsci/maven-snapshot-check-plugin"}]},{"type":"text","text":" 代碼倉庫。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"分發插件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用 "},{"type":"codeinline","content":[{"type":"text","text":"mvn package"}]},{"type":"text","text":" 命令可以打包出後綴爲 hpi 的二進制包, 這樣就可以分發插件,將其安裝到 Jenkins 實例。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"插件使用說明"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以下是對插件的使用簡要描述。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果勾選了下面截圖中的選擇框, Jenkins 任務在構建時將會檢查 pom.xml 中是否包含 SNAPSHOT 。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f6/f6f1879a3bdc482f64f251029cb96c99.png","alt":"maven-snapshot-check-plugin-usage","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果檢查到的話,則會將該次構建狀態標記爲失敗。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d2/d263e486dbc49e72bf47729d9a77486f.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文章上篇主要介紹了從產生 idea 到插件開發完成的過程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼插件在開發完成後是如何將它託管到 Jenkins 插件更新中心讓所有用戶都可以看到的呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文章下篇將對這個過程進行介紹,敬請期待!"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"參考"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://wiki.jenkins.io/display/JENKINS/Plugin+tutorial","title":null},"content":[{"type":"text","text":"Plugin tutorial"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://jenkins.io/doc/developer/tutorial/prepare/","title":null},"content":[{"type":"text","text":"Preparing for Plugin Development"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://jenkins.io/doc/developer/tutorial/create/","title":null},"content":[{"type":"text","text":"Create a Plugin"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://jenkins.io/doc/developer/tutorial/run/","title":null},"content":[{"type":"text","text":"Build and Run the Plugin"}]}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章