来到新公司不久,主管安排一个jenkins 插件开发的小需求给我,让我练练手,之前从未接触过相关内容,一切从0开始,做了一个月,基本完成需求上的功能,期间遇到不少困难,记录做以总结。
现阶段网上相关的指导还是比较匮乏,我个人觉得比较好的方法是:参考已有插件的源码!

需求大致是这样的:
点击进入某次历史编译,将本次上传至Artifactory的文件copy 到Artifactory中的release目录下,目的是可以选择性的选择某次编译生成的文件copy到release目录下供测试的同事进行测试。

1、 开发环境的搭建
包括本地JDK、maven、eclipse等,参考链接:
https://jenkins.io/doc/developer/tutorial/prepare/
其中遇到的问题有:

  • 公司的网络需要代理访问外网:(没有外网访问问题的可以忽略)
    https://blog.csdn.net/u010531676/article/details/54343845
    maven/conf/setting.xml中添加:

          <proxies>
            <proxy>
              <active>true</active>
              <protocol>https</protocol>
              <username>username</username>
              <password>password</password>
              <host>company host</host>
              <port>888</port>
              <nonProxyHosts>www.google.com|*.somewhere.com</nonProxyHosts>
            </proxy>
          </proxies> 
    
    
    

2、 创建一个空的plugin工程,调试以及生成插件

先是根据官方文档创建了一个空的工程,当时很费解,为什么我的工程里面没有HelloWorldBuilder.java文件?网上一些教程里面都是创建后就有这个文件的,因为是小白,这个东西当时纠结了好几个小时,后面尝试后才知道官方文档给出的是:

mvn archetype:generate -Dfilter=io.jenkins.archetypes:empty-plugin

默认为空的工程
可以使用:

mvn -U archetype:generate -Dfilter=io.jenkins.archetypes:

然后选择是空的工程还是hello_world工程,新手建议先通过hello_world工程了解工程结构,真正写项目建议基于empty-plugin。

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:global-shared-library (Uses the Jenkins Pipeline Unit mock library to test the usage of a Global Shared Library)
4: remote -> io.jenkins.archetypes:hello-world-plugin (Skeleton of a Jenkins plugin with a POM and an example build step.)
5: remote -> io.jenkins.archetypes:scripted-pipeline (Uses the Jenkins Pipeline Unit mock library to test the logic inside a Pipeline script.)
Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): : 4

调试插件:mvn hpi:run 访问http://localhost:8080/jenkins 可查看插件效果
生成插件:mvn package 插件为hpi格式,会保存在target/目录下,也可以手动安装hpi文件查看插件效果

参考链接:
https://wiki.jenkins.io/display/JENKINS/Plugin+tutorial
https://jenkins.io/doc/developer/tutorial/create/

3、 jenkins plugin 目录结构
这块网上的说明相对还是比较多的,引用网上给出的:

  • pom.xml - Maven POM 文件,用于配置插件的设定,包括插件所依赖的架包、JDK版本、插件名称和描述等。
  • src/main/java - 插件的 Java 源文件
  • src/main/resources - 插件的 Jelly 视图文件
  • src/main/webapp - 插件的静态资源,如图片或 HTLM 等,本次项目中没有使用到。

4、Plugin UI之configure/General中添加参数视图

jenkins插件的UI(界面)是通过与java文件一一对应的jelly文件去体现的,举个例子:
在官方给的helloworld工程中:

src/main/java/org/sample/HelloWorldBuilder.java
src/main/resources/org/sample/HelloWorldBuilder/config.jelly
两者是一一对应的,其中config.jelly用于工程相关参数配置,如果换成global.jelly则用于全局参数配置

一个jenkins build 的过程一般包括:

  • Source Code Management
  • Build Triggers
  • Build Environment
  • Build
  • Build Environment

pipeline job往往以上过程全部在pipeline code中去实现,本次需求是要求在pipeline运行结束后执行copy操作,继承类似Builder,Recorder等构建中的扩展类是不能满足需求的。
后来把目标放在了jenkins的configure中的General 上,其中的选项可以和构建中无关。通过已有插件的源码,找到了JobProperty 类。效果如下:
图一
java部分代码部分如下:

package io.jenkins.plugins.sample;

import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;

import hudson.Extension;
import hudson.model.Job;
import hudson.model.JobProperty;
import hudson.model.JobPropertyDescriptor;
import net.sf.json.JSONObject;

public class MyJobProperty extends JobProperty<Job<?, ?>> {
	private final String yourname;
	
	@DataBoundConstructor    //构造函数需要添加DataBoundConstructor标记
	public MyJobProperty(String yourname) {
		this.yourname = yourname;
	}
	
	
	public String getYourname() {//和jelly中的field="yourname"相关联
		return yourname;
	}


	@Extension   //扩展标记
	public static final class DescriptorImpl extends JobPropertyDescriptor { 
	//Descriptor 及其各种延伸的Descriptor ,例如:JobPropertyDescriptor,BuildStepDescriptor等,
	//往往继承该类需要加上@Extension 用来告诉jenkins是JobPropertyDescriptor的扩展,需要创建对应的instance对象,已经对参数的校验。

		@Override
		public JobProperty<?> newInstance(StaplerRequest req, JSONObject formData) throws FormException {
		//满足某种条件后创建MyJobProperty 对象
			MyJobProperty jp = req.bindJSON(MyJobProperty.class, formData.getJSONObject("myjobproperty"));
			if (jp == null) {
				return null;
			}
			return jp;
		}

		@Override
		public boolean isApplicable(Class<? extends Job> jobType) {
			//是否对所有项目类型可用
			return super.isApplicable(jobType);
		}

		@Override
		public String getDisplayName() {
		//本例中并未用到,在例如Builder类型的插件,添加构建过程的名称
			return "MyJobProperty";
		}

	}
}

jelly部分代码如下:

<?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:optionalBlock name="myjobproperty" title="click and input your name" checked="${!empty(instance)}">
      <f:entry title="your name " field="yourname">
        <f:textbox />
      </f:entry>
  </f:optionalBlock>
</j:jelly>

5、Plugin UI之主面板和侧边栏部分
Action类可以在jenkins中的主面板中添加视图效果,其中的重写方法为侧边栏图标、名称、以及url名称

@Override
public String getIconFileName() {

	return  "document.png";
}

@Override
public String getDisplayName() {
	return "MyJobProperty";
}

@Override
public String getUrlName() {
	return "myjobproperty";
}
这里需要注意的是getUrlName() 返回值不能有空格,我在插件调试过程中出现了侧边栏不能点击的问题,就是因为返回值中有空格导致的。

jenkins本身提供了很多Action接口,例如:BuildBadgeAction(可以在build history中添加文字或者图片标签),RootAction(入口为jenkins的根目录)等,可以根据自己的需要实现对应的接口。

Action与build相关联一般有两种方式:
首先需要在action中的构造函数中添加Run/AbstractBuild/WorkflowRun作为传入参数,然后使用下列方法使二者关联。
第一种:addAction()

示例1:
    @Override
    public boolean prebuild(AbstractBuild<?,?> build, BuildListener listener) {
        build.addAction(new MyBuildAction(build));
        return true;
    }

第二种:ActionFactory类
在MyAction类中添加内部类MyActionFactory

示例2:
	@Extension
	public static class MyActionFactory extends TransientActionFactory<WorkflowRun> {
	//本次需求中是要兼容pipeline类型,使用了WorkflowRun
		@Override
		public Class<WorkflowRun> type() {
			return WorkflowRun.class;
		}

		@Override
		public Collection<? extends Action> createFor(WorkflowRun target) {
			MyJobProperty prop = target.getParent().getProperty(MyJobProperty.class);
			if (prop == null || target.getResult() != Result.SUCCESS) {
				return Collections.emptySet();
			} else {
				return Collections.singleton(new MyAction(prop, target));
			}
		}
	}
	示例1和示例2不是同一个例子,本次项目使用的是示例2中方式,项目中用到了类似MyJobProperty 中的方法和参数,所以将MyJobProperty 作为了传入参数,具体需要根据需求定义参数。

具体界面还是使用jelly文件去实现,这里需要注意的是:
如果Action中有按钮点击事件,该如何实现?

jelly代码:
   //action中的值需要和java代码一一对应,action="submit"则java中为doSubmit方法
    <f:form action="submit" method="POST">
        <f:bottomButtonBar>
            <f:submit value="click"/>
        </f:bottomButtonBar>
    </f:form>

java代码部分:
@RequirePOST
public void doSubmit(StaplerRequest req, StaplerResponse rsp) throws Exception {
	JSONObject form = req.getSubmittedForm();
	name = Util.fixEmpty(form.getString("name")); // 取feild中的值
}

如果要在编译历史中加入标记,该如何处理?

自定义Action类实现BuildBadgeAction,在对应的resource目录下新增badge.jelly文件

MyAction.java code:
public class MyAction implements BuildBadgeAction {
	****
}

badge.jelly code:
<?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">
<j:if test="${it.isclick}">       <--! it.xxx 表示MyAction类中的getXxx()或者isXxx()方法的返回值 -->
       <a href="www.baidu.com">Baidu</a>
</j:if>
</j:jelly>

至此,界面的效果差不多是这样:
pipeline job在configure/General选择MyJobProperty,填好相关参数后保存退出,在build history中选择某次编译成功的条目,进入后可以看到侧边栏图标,点击侧边栏图标可以显示Action主界面,点击自定义按钮后执行业务逻辑,同时该build history中该次条目上添加标记。

6、其他注意点
1)点击按钮后刷新页面

		run.replaceAction(this);
		rsp.sendRedirect("");

2)字符串如果有跨行,如何在jenkins主界面能跨行显示
在jelly文件中如果使用 <h1 />或者 <p />等标签,在jenkins上不能正常显示跨行,需要使用<pre />标签

    <pre >
        '''
        nihao
        hello
        '''
    </pre >

3)Action显示系统侧边栏

  	MyAction.java code:
  
    public Run getOwner() {
    	return run;
    }
    
    index.jelly code:
    
    <st:include page="sidepanel.jelly" it="${it.owner}"/>

4)configure/General中参数选中后不能保存,退出再进去后需要重新填写
原因:jelly文件中的feild名称和java文件中不匹配导致

5)侧边栏图标没有点击效果
原因:getUrlName的返回值不能有空格

6)pom.xml文件如何添加需要依赖项?
如果需要依赖A,并且有A的源码,可以先查看A的pom.xml文件,依次找到groupId,artifactId,version,然后在自己的pom.xml文件的dependencies标签中添加。

<dependencies>	
	<dependency>
		<groupId>org.apache.httpcomponents</groupId>
		<artifactId>httpclient</artifactId>
		<version>4.5.5</version>
	</dependency>
</dependencies>

7)如何下载已有插件源码?
Jenkins-插件管理-搜索需要的插件-点击插件名-点击github

8)jelly文件中一些关键字的含义

  • app Jekins实例
  • instance 正在被配置的对象
  • descriptor 对应于instance的Descriptor对象
  • h hudson.Functions的实例,一个全局的工具类,提供静态工具方法
  • it 当前UI所属的模型对象

9)Node下workspace 获取,FilePath类型,没有上下文的情况下,即非构建中。

FilePath path = FilePathUtils.find(String nodename, String workspace);

7、Jenkins插件各个类或接口的含义
1)EnvironmentContributor
用来提供环境变量,重写buildEnvironmentFor函数,job可以获取JobProperty
例如:pipeline中echo ${MY_ENV} 会得到"I am local.prop"

@Extension
public class MyEnvVarsContributor extends EnvironmentContributor {

	@Override
	public void buildEnvironmentFor(Job j, EnvVars envs, TaskListener listener)
			throws IOException, InterruptedException {
		MyJobProperty jb = (MyJobProperty) j.getProperty(MyJobProperty.class);
		if (jb != null) {
			envs.put("MY_ENV", "I am local.prop");
		}
		super.buildEnvironmentFor(j, envs, listener);
	}

}

2)JobProperty
配置中General部分,也可以用来设置参数
常见获取方式:
MyJobProperty prop = run.getParent().getProperty(MyJobProperty.class);

3)BuildBadgeAction
实现该接口的类配合badge.jelly文件可以在build history中显示标签

4)Descriptor
Descriptor 及其各种延伸的Descriptor ,例如:JobPropertyDescriptor,BuildStepDescriptor等
往往继承该类需要加上@Extension 用来告诉jenkins是JobPropertyDescriptor的扩展,需要创建对应的instance对象。
Descriptor 种类:https://javadoc.jenkins.io/hudson/model/Descriptor.html

5)AbstractStepDescriptorImpl
AbstractStepDescriptorImpl 是pipeline step的扩展,可以定义一个插件的function名称,例如:
继承之后重写以下两个方法:

		@Override
        public String getFunctionName() {//方法名,可以在pipeline语句中调用,其中的参数是
							//继承AbstractStepImpl类的构造方法中传入。
            return "publishHTML";
        }

        @Override
        public String getDisplayName() {
            return "Publish HTML reports";
        }

6)Execution
例如:AbstractSynchronousNonBlockingStepExecution。继承后会重写run方法,里面是pipeline 中调用方法的具体内容。继承AbstractStepDescriptorImpl 的类会在其构造函数中调用关联Execution ,并调用run方法。例如:

public DescriptorImpl() {
    super(PublishHTMLStepExecution.class);
}

7)EnvVars(待补充)
8)ActionFactory(待补充)
9)Context(待补充)

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐