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}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章