| 阅读 | 共 1962 字,阅读约
概述
jenkins在ci、cd领域如此火热,独霸一方,得益于丰富的插件生态。而作为开发者你的,是否好奇jenkins的插件机制是如何实现的
开发一个插件
配置mvn
编辑~/.m2/settings.xml文件,让mvn执行时能下载到jenkins相关插件
1<settings>
2 <pluginGroups>
3 <pluginGroup>org.jenkins-ci.tools</pluginGroup>
4 </pluginGroups>
5
6 <profiles>
7 <!-- Give access to Jenkins plugins -->
8 <profile>
9 <id>jenkins</id>
10 <activation>
11 <activeByDefault>true</activeByDefault> <!-- change this to false, if you don't like to have it on per default -->
12 </activation>
13 <repositories>
14 <repository>
15 <id>repo.jenkins-ci.org</id>
16 <url>https://repo.jenkins-ci.org/public/</url>
17 </repository>
18 </repositories>
19 <pluginRepositories>
20 <pluginRepository>
21 <id>repo.jenkins-ci.org</id>
22 <url>https://repo.jenkins-ci.org/public/</url>
23 </pluginRepository>
24 </pluginRepositories>
25 </profile>
26 </profiles>
27 <mirrors>
28 <mirror>
29 <id>repo.jenkins-ci.org</id>
30 <url>https://repo.jenkins-ci.org/public/</url>
31 <mirrorOf>m.g.o-public</mirrorOf>
32 </mirror>
33 </mirrors>
34</settings>x
初始化项目
1# 初始化项目
2mvn -U archetype:generate -Dfilter=io.jenkins.archetypes:
3# 会弹出选择哪种项目,我们选择4,hello-world这种插件
44
5# 然后是选择版本,输入4,选择最新的版本
64
7# 输入artifact和version
8com.kinnylee
9jenkins-plugin-hellowlrld
10# 进入项目
11cd xxx
12# 编译打包
13mvn hpi:run
14
模板脚手架
pom文件的parent
1<parent>
2 <groupId>org.jenkins-ci.plugins</groupId>
3 <artifactId>plugin</artifactId>
4 <version>3.4</version>
5 <relativePath />
6 </parent>
原理
Jenkins 插件开发归功于有一系列扩展点。 开发人员可以对其进行扩展自定义实现一些功能。
扩展点
扩展点是jenkins某个方面的接口或者抽象类。接口定义了需要实现的方法,插件实现对应的方法。常见的扩展点有:
- SCM:源码管理扩展点
- svn
- git
- Builder:构建步骤的扩展点
- execute shell
- Trigger:触发的扩展点
- Publisher:构建完成后需要执行的步骤
Descriptor 静态内部类
是一个类的描述者,用于指明这是一个扩展点的实现。 Jenkins 通过这个描述者才能知道我们写的插件。 每一个描述者静态类都需要被 @Extension 注解, Jenkins 内部会扫描 @Extenstion 注解来获取注册了哪些插件。
代码分析
src/main/java/io.jenkins.plugin.sample/HelloWorldBuilder
1// 插件继承扩展点,这里的扩展点是Builder扩展点
2public class HelloWorldBuilder extends Builder implements SimpleBuildStep {
3
4 private final String name;
5 private boolean useFrench;
6
7 // 通过这个注解实现前端页面和后台数据绑定
8 @DataBoundConstructor
9 public HelloWorldBuilder(String name) {
10 this.name = name;
11 }
12
13 public String getName() {
14 return name;
15 }
16
17 public boolean isUseFrench() {
18 return useFrench;
19 }
20 // 通过这个注解实现前端页面和后台数据绑定
21 @DataBoundSetter
22 public void setUseFrench(boolean useFrench) {
23 this.useFrench = useFrench;
24 }
25
26 @Override
27 public void perform(Run<?, ?> run, FilePath workspace, Launcher launcher, TaskListener listener) throws InterruptedException, IOException {
28 if (useFrench) {
29 listener.getLogger().println("Bonjour, " + name + "!");
30 } else {
31 listener.getLogger().println("Hello, " + name + "!");
32 }
33 }
34
35 @Symbol("greet")
36 // 这个注解描述了插件信息,供给jenkins扫描
37 @Extension
38 public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
39 // 校验前端数据的合法性
40 public FormValidation doCheckName(@QueryParameter String value, @QueryParameter boolean useFrench)
41 throws IOException, ServletException {
42 if (value.length() == 0)
43 return FormValidation.error(Messages.HelloWorldBuilder_DescriptorImpl_errors_missingName());
44 if (value.length() < 4)
45 return FormValidation.warning(Messages.HelloWorldBuilder_DescriptorImpl_warnings_tooShort());
46 if (!useFrench && value.matches(".*[éáàç].*")) {
47 return FormValidation.warning(Messages.HelloWorldBuilder_DescriptorImpl_warnings_reallyFrench());
48 }
49 return FormValidation.ok();
50 }
51
52 // 该方法返回Builder在jenkins中是否可用
53 @Override
54 public boolean isApplicable(Class<? extends AbstractProject> aClass) {
55 return true;
56 }
57
58 // 前端页面显示的名称
59 @Override
60 public String getDisplayName() {
61 return Messages.HelloWorldBuilder_DescriptorImpl_DisplayName();
62 }
63
64 }
65
66}
src/main/resources/io.jenkins.plugins.sample/HelloWorldBuilder/config.jelly
1<?jelly escape-by-default='true'?>
2<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">
3 <f:entry title="${%Name}" field="name">
4 <f:textbox />
5 </f:entry>
6 <f:advanced>
7 <f:entry title="${%French}" field="useFrench"
8 description="${%FrenchDescr}">
9 <f:checkbox />
10 </f:entry>
11 </f:advanced>
12</j:jelly>
部分源码分析
入口函数
jenkins/core/src/main/java/hudson/Main.java
1public class Main {
2
3 // main入口函数
4 public static void main(String[] args) {
5 try {
6 // 调用run函数
7 System.exit(run(args));
8 } catch (Exception e) {
9 e.printStackTrace();
10 System.exit(-1);
11 }
12 }
13
14 // run函数
15 public static int run(String[] args) throws Exception {
16 // 获取JENKINS_HOME参数,得到路径
17 String home = getHudsonHome();
18 ...
19 // 核心函数
20 return remotePost(args);
21 }
22
23 private static String getHudsonHome() {
24 String home = EnvVars.masterEnvVars.get("JENKINS_HOME");
25 if (home!=null) return home;
26 return EnvVars.masterEnvVars.get("HUDSON_HOME");
27 }
28
29 /**
30 * Run command and send result to {@code ExternalJob} in the {@code external-monitor-job} plugin.
31 * Obsoleted by {@code SetExternalBuildResultCommand} but kept here for compatibility.
32 */
33 public static int remotePost(String[] args) throws Exception {
34 String projectName = args[0];
35
36 String home = getHudsonHome();
37 if(!home.endsWith("/")) home = home + '/'; // make sure it ends with '/'
38
39 // check for authentication info
40 String auth = new URL(home).getUserInfo();
41 if(auth != null) auth = "Basic " + new Base64Encoder().encode(auth.getBytes(StandardCharsets.UTF_8));
42
43 {// check if the home is set correctly
44 HttpURLConnection con = open(new URL(home));
45 if (auth != null) con.setRequestProperty("Authorization", auth);
46 con.connect();
47 if(con.getResponseCode()!=200
48 || con.getHeaderField("X-Hudson")==null) {
49 System.err.println(home+" is not Hudson ("+con.getResponseMessage()+")");
50 return -1;
51 }
52 }
53
54 URL jobURL = new URL(home + "job/" + Util.encode(projectName).replace("/", "/job/") + "/");
55
56 {// check if the job name is correct
57 HttpURLConnection con = open(new URL(jobURL, "acceptBuildResult"));
58 if (auth != null) con.setRequestProperty("Authorization", auth);
59 con.connect();
60 if(con.getResponseCode()!=200) {
61 System.err.println(jobURL + " is not a valid external job (" + con.getResponseCode() + " " + con.getResponseMessage() + ")");
62 return -1;
63 }
64 }
65
66 // get a crumb to pass the csrf check
67 String crumbField = null, crumbValue = null;
68 try {
69 HttpURLConnection con = open(new URL(home +
70 "crumbIssuer/api/xml?xpath=concat(//crumbRequestField,\":\",//crumb)'"));
71 if (auth != null) con.setRequestProperty("Authorization", auth);
72 String line = IOUtils.readFirstLine(con.getInputStream(),"UTF-8");
73 String[] components = line.split(":");
74 if (components.length == 2) {
75 crumbField = components[0];
76 crumbValue = components[1];
77 }
78 } catch (IOException e) {
79 // presumably this Hudson doesn't use CSRF protection
80 }
81
82 // write the output to a temporary file first.
83 File tmpFile = File.createTempFile("jenkins","log");
84 try {
85 int ret;
86 try (OutputStream os = Files.newOutputStream(tmpFile.toPath());
87 Writer w = new OutputStreamWriter(os, StandardCharsets.UTF_8)) {
88 w.write("<?xml version='1.1' encoding='UTF-8'?>");
89 w.write("<run><log encoding='hexBinary' content-encoding='"+Charset.defaultCharset().name()+"'>");
90 w.flush();
91
92 // run the command
93 long start = System.currentTimeMillis();
94
95 List<String> cmd = new ArrayList<>(Arrays.asList(args).subList(1, args.length));
96 Proc proc = new Proc.LocalProc(cmd.toArray(new String[0]),(String[])null,System.in,
97 new DualOutputStream(System.out,new EncodingStream(os)));
98
99 ret = proc.join();
100
101 w.write("</log><result>"+ret+"</result><duration>"+(System.currentTimeMillis()-start)+"</duration></run>");
102 } catch (InvalidPathException e) {
103 throw new IOException(e);
104 }
105
106 URL location = new URL(jobURL, "postBuildResult");
107 while(true) {
108 try {
109 // start a remote connection
110 HttpURLConnection con = open(location);
111 if (auth != null) con.setRequestProperty("Authorization", auth);
112 if (crumbField != null && crumbValue != null) {
113 con.setRequestProperty(crumbField, crumbValue);
114 }
115 con.setDoOutput(true);
116 // this tells HttpURLConnection not to buffer the whole thing
117 con.setFixedLengthStreamingMode((int)tmpFile.length());
118 con.connect();
119 // send the data
120 try (InputStream in = Files.newInputStream(tmpFile.toPath())) {
121 org.apache.commons.io.IOUtils.copy(in, con.getOutputStream());
122 } catch (InvalidPathException e) {
123 throw new IOException(e);
124 }
125
126 if(con.getResponseCode()!=200) {
127 org.apache.commons.io.IOUtils.copy(con.getErrorStream(), System.err);
128 }
129
130 return ret;
131 } catch (HttpRetryException e) {
132 if(e.getLocation()!=null) {
133 // retry with the new location
134 location = new URL(e.getLocation());
135 continue;
136 }
137 // otherwise failed for reasons beyond us.
138 throw e;
139 }
140 }
141 } finally {
142 tmpFile.delete();
143 }
144 }
145
146 /**
147 * Connects to the given HTTP URL and configure time out, to avoid infinite hang.
148 */
149 private static HttpURLConnection open(URL url) throws IOException {
150 HttpURLConnection c = (HttpURLConnection)url.openConnection();
151 c.setReadTimeout(TIMEOUT);
152 c.setConnectTimeout(TIMEOUT);
153 return c;
154 }
155
156 /**
157 * Set to true if we are running unit tests.
158 */
159 public static boolean isUnitTest = false;
160
161 /**
162 * Set to true if we are running inside {@code mvn jetty:run}.
163 * This is also set if running inside {@code mvn hpi:run} since plugins parent POM 2.30.
164 */
165 public static boolean isDevelopmentMode = SystemProperties.getBoolean(Main.class.getName()+".development");
166
167 /**
168 * Time out for socket connection to Hudson.
169 */
170 public static final int TIMEOUT = SystemProperties.getInteger(Main.class.getName()+".timeout",15000);
171}
172
前端页面代码
hudson/model/view/index.jelly
- sidepanel.jelly
- task-top.jelly
- task-new.jelly
- task-bottom.jelly
- view-index-top.jelly
- main.jelly
1<?jelly escape-by-default='true'?>
2<st:compress xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
3 <l:layout title="${it.class.name=='hudson.model.AllView' ? '%Dashboard' : it.viewName}${not empty it.ownerItemGroup.fullDisplayName?' ['+it.ownerItemGroup.fullDisplayName+']':''}" norefresh="${!it.automaticRefreshEnabled}">
4 <j:set var="view" value="${it}"/> <!-- expose view to the scripts we include from owner -->
5 <st:include page="sidepanel.jelly" />
6 <l:main-panel>
7 <st:include page="view-index-top.jelly" it="${it.owner}" optional="true">
8 <!-- allow the owner to take over the top section, but we also need the default to be backward compatible -->
9 <div id="view-message">
10 <div id="systemmessage">
11 <j:out value="${app.systemMessage!=null ? app.markupFormatter.translate(app.systemMessage) : ''}" />
12 </div>
13 <t:editableDescription permission="${it.CONFIGURE}"/>
14 </div>
15 </st:include>
16
17 <j:set var="items" value="${it.items}"/>
18 <st:include page="main.jelly" />
19 </l:main-panel>
20 <l:header>
21 <!-- for screen resolution detection -->
22 <l:yui module="cookie" />
23 <script>
24 YAHOO.util.Cookie.set("screenResolution", screen.width+"x"+screen.height);
25 </script>
26 </l:header>
27 </l:layout>
28</st:compress>