| 阅读 |,阅读约 4 分钟
| 复制链接:

概述

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:构建完成后需要执行的步骤
    • email

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>