Tutorial: Developing complex plugins for Jenkins

Introduction

Jenkins is the most popular Continuous Integration and Continuous Delivery (CI/CD) server. Jenkins is used for managing complex CI/CD pipelines that support building, deploying and automating software. Jenkins is a critical component of DevOps implementations, especially in larger software teams. Every team has different needs and CI/CD is a process that needs heavy customization. Jenkins has an ecosystem of thousands of plugins and new ones are being added all the time.

Recently, I needed to develop a complex Jenkins plug-in for a customer in the containers & DevOps space. In this process, I realized that there is lack of good documentation on Jenkins plugin development and good information is very hard to find. That’s why I decided to write this blog to share my knowledge on Jenkins plugin development.

Topics covered in this Blog

  1. Setting up the development environment

  2. Jenkins plugin architecture: Plugin classes and understanding of the source code.

  3. Complex tasks: Tasks like the integration of REST API in the plugin and exposing environment variables through source code.

  4. Plugin debugging and deployment

So let’s start, shall we?

1. Setting up the development environment

I have used Ubuntu 16.04 for this environment, but the steps remain identical for other flavors. The only difference will be in the commands used for each operating system.

Let me give you a brief list of the requirements:

  1. Compatible JDK: Jenkins plugin development is done in Java. Thus a compatible JDK is what you need first. JDK 6 and above are supported as per the Jenkins documentation.

  2. Maven: Installation guide. I know many of us don’t like to use Maven, as it downloads stuff over the Internet at runtime but it’s required. Check this to understand why using Maven is a good idea.

  3. Jenkins: Check this Installation Guide. Obviously, you would need a Jenkins setup - can be local on hosted on a server/VM.

  4. IDE for development: An IDE like Netbeans, Eclipse or IntelliJ IDEA is preferred. I have used Netbeans 8.1 for this project.

Before going forward, please ensure that you have the above prerequisites installed on your system. Jenkins does have official documentation for setting up the environment - Check this. If you would like to use an IDE besides Netbeans, the above document covers that too.

Let’s start with the creation of your project. I will explain with Maven commands and with use of the IDE as well.

First, let's start with the approach of using commands.

It may be helpful to add the following to your ~/.m2/settings.xml (Windows users will find them in %USERPROFILE%\.m2\settings.xml):

This basically lets you use short names in commands e.g. instead of org.jenkins-ci.tools:maven-hpi-plugin:1.61:create, you can use hpi:create. hpi is the packaging style used to deploy the plugins.

Create the plugin

$ mvn -U org.jenkins-ci.tools:maven-hpi-plugin:create


This will ask you a few questions, like the groupId (the Maven jargon for the package name) and the artifactId (the Maven jargon for your project name), then create a skeleton plugin from which you can start with. This command should create the sample HelloWorldBuilder plugin.

Command Explanation:

  • -U means that Maven should update all relevant Maven plugins (check for plugin updates).
  • hpi: this prefix specifies that the Jenkins HPI Plugin is being invoked, a plugin that supports the development of Jenkins plugins.
  • create is the goal which creates the directory layout and the POM for your new Jenkins plugin and it adds it to the module list.

Source code tree would be like this:

Your Project Name
    Pom.xml
      Src
          Main
              Java
                  package folder(usually consist of groupId and artifactId)
                      HelloWorldBuilder.java
              Resources
                  Package folder/HelloWorldBuilder/jelly files

Run “mvn package” which compiles all sources, runs the tests and creates a package - when used by the HPI plugin it will create an *.hpi file.

Building the Plugin:

Run mvn install in the directory where pom.xml resides. This is similar to mvn package command but at the end, it will create your plugins .hpi file which you can deploy. Simply copy the create .hpi file and paste to /plugins folder of your Jenkins setup. Restart your Jenkins and you should see the plugin on Jenkins.

Now let’s see how this can be done with IDE.

With Netbeans IDE:

I have used Netbeans for development(Download). Check with the JDK version. Latest version 8.2 works with JDK 8. Once you install Netbeans, install NetBeans plugin for Jenkins/Stapler development.

You can now create plugin via New Project » Maven » Jenkins Plugin.

This is the same as “mvn -U org.jenkins-ci.tools:maven-hpi-plugin:create” command which should create the simple “HelloWorldBuilder” application.

Netbeans comes with Maven built-in so even if you don’t have Maven installed on your system this should work. But you may face error accessing the Jenkins repo. Remember we added some configuration settings in settings.xml in the very first step. Yes, if you have added that already then you shouldn’t face any problem but if you haven’t added that you can add that in Netbeans Maven settings.xml which you can find at: netbeans_installation_path/java/maven/conf/settings.xml

Now you have your “HelloWorldBuilder” application ready.  This is shown as TODO plugin in Netbeans. Simply run it(F6). This creates the Jenkins instance and runs it on 8080 port. Now, if you already have local Jenkins setup then you need to stop it otherwise this will give you an exception. Go to localhost:8080/jenkins and create a simple job. In “Add Build Step” you should see “Say Hello World” plugin already there.

hello_world.jpg

 

 

 

 

 

Now how it got there and the source code explanation is next.

2. Jenkins plugin architecture and understanding

Now that we have our sample HelloWorldBuilder plugin ready,  let’s see its components.

As you may know, Jenkins plugin has two parts: Build Step and Post Build Step. This sample application is designed to work for Build step and that’s why you see “Say Hello world” plugin in Build step. I am going to cover Build Step itself.

Do you want to develop Post Build plugin? Don’t worry as these two don’t have much difference. The difference is only in the classes which we extend. For Build step, we extend “hudson.tasks.Builder” and for Post Build “hudson.tasks.Recorder” and with Descriptor class for Build step “BuildStepDescriptor<Builder>” for Post Build “BuildStepDescriptor<Publisher>”.

We will go through these classes in detail below:

hudson.tasks.Builder Class:

In brief, this simply tells Jenkins that you are writing a Build Step plugin. A full explanation is here. Now you will see “perform” method once you override this class.

@Override
public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener)

Note that we are not implementing the ”SimpleBuildStep” interface which is there in HelloWorldBuilder source code. Perform method for that Interface is a  bit different from what I have given above. My explanation goes around this perform method.

The perform method is basically called when you run your Build. If you see the Parameters passed you have full control over the Build configured, you can log to Jenkins console screen using listener object. What you should do here is access the values set by the user on UI and perform the plugin activity. Note that this method is returning a boolean, True means build is Successful and False is Build Failed.

Understanding the Descriptor Class:  

You will notice there is a static inner class in your main class named as DescriptorImpl. This class is basically used for handling configuration of your Plugin. When you click on “Configure” link on Jenkins it basically calls this method and loads the configured data.

You can perform validations here, save the global configuration and many things. We will see these in detail as when required. Now there is an overridden method:

@Override
public String getDisplayName() {
return "Say Hello World";
}

That’s why we see “Say Hello World” in the Build Step. You can rename it to what your plugin does.

@Override
public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
// To persist global configuration information,
// set that to properties and call save().
useFrench = formData.getBoolean("useFrench");

// ^Can also use req.bindJSON(this, formData);
//(easier when there are many fields; need set* methods for this, like setUseFrench)
save();
return super.configure(req,formData);
}

This method basically saves your configuration, or you can even get global data like we have taken “useFrench” attribute which can be set from Jenkins global configuration. If you would like to set any global parameter you can place them in the global.jelly file.

Understanding Action class and jelly files:

To understand the main Action class and what it’s purpose is, let’s first understand the jelly files.

There are two main jelly files:  config.jelly and global.jelly.  The global.jelly file is used to set global parameters while config.jelly is used for local parameters configuration. Jenkins uses these jelly files to show the parameters or fields on UI. So anything you write in config.jelly will show up on Jobs configuration page as configurable.

<f:entry title="Name" field="name">
<f:textbox />
</f:entry>

This is what is there in our HelloWorldBuilder application. It simply renders a textbox for entering name.

hello_world_ui.jpg

 

 

Jelly has its own syntax and supports HTML and Javascript as well. It has radio buttons, checkboxes, dropdown lists and so on.

How does Jenkins manage to pull the data set by the user? This is where our Action class comes into the picture. If you see the structure of the sample application, it has a private field as name and a constructor.

@DataBoundConstructor
public HelloWorldBuilder(String name) {
this.name = name;
}

This DataBoundConstructor annotation tells Jenkins to bind the value of jelly fields. If you notice there’s field as “name” in jelly and the same is used here to put the data. Note that, whatever name you set in field attribute of jelly same you should use here as they are tightly coupled.

Also, add getters for these fields so that Jenkins can access the values.

@Override
public DescriptorImpl getDescriptor() {
return (DescriptorImpl)super.getDescriptor();
}

This method gives you the instance of Descriptor class. So if you want to access methods or properties of Descriptor class in your Action class you can use this.

3. Complex tasks:

We now have a good idea on how the Jenkins plugin structure is and how it works. Now let’s start with some complex stuff.

On the internet, there are examples on how to render a selection box(drop-down) with static data. What if you want to load in a dynamic manner? I came with the below solution. We will use Amazon’s publicly available REST API for getting the coupons and load that data in the selection box.

Here, the objective is to load the data in the selection box.I have the response for REST API as below:

"offers" : {
    "AmazonChimeDialin" : {
      "offerCode" : "AmazonChimeDialin",
      "versionIndexUrl" : "/offers/v1.0/aws/AmazonChimeDialin/index.json",
      "currentVersionUrl" : "/offers/v1.0/aws/AmazonChimeDialin/current/index.json",
     "currentRegionIndexUrl" : "/offers/v1.0/aws/AmazonChimeDialin/current/region_index.json"
    },

    "mobileanalytics" : {
      "offerCode" : "mobileanalytics",
      "versionIndexUrl" : "/offers/v1.0/aws/mobileanalytics/index.json",
      "currentVersionUrl" : "/offers/v1.0/aws/mobileanalytics/current/index.json",
      "currentRegionIndexUrl" : "/offers/v1.0/aws/mobileanalytics/current/region_index.json"
    }
}

I have taken all these offers and created one dictionary and rendered it on UI. Thus the user will see the list of coupon codes and can choose anyone of them.

tmp.png

Let’s understand how to create the selection box and load the data into it.

   <f:entry title="select Offer From Amazon" field="getOffer">
   <f:select id="offer-${editorId}" onfocus="getOffers(this.id)"/>
   </f:entry>

This is the code which will generate the selection box on configuration page.  Now you will see here “getOffer” field means there’s field with the same name in the Action class.

When you create any selection box, Jenkins needs doFill{fieldname}Items method in your descriptor class. As we have seen, Descriptor class is configuration class it tries to load the data from this method when you click on the configuration of the job. So in this case, “doFillGetOfferItems” method is required.

After this, selection box should pop up on the configuration page of your plugin.

Now here as we need to do dynamic actions, we will perform some action and will load the data.

As an example, we will click on the button and load the data in Selection Box.

<f:validateButton title="Get Amazon Offers" progress="Fetching Offers..."method="getAmazonOffers"/>

Above is the code to create a button. In method attribute, specify the backend method which should be present in your Descriptor class. So when you click on this button “getAmazonOffers” method will get called at the backend and it will get the data from API.

Now when we click on the selection box, we need to show the contents. As I said earlier, Jelly does support HTML and Javascript. Yes, if you want to do dynamic action use Javascript simply. If you see in selection box code of jelly I have used onfocus() method of Javascript which is pointing to getOffers() function.

Now you need to have this function, define script tag like this.

<script>
 function getOffers(){
 }
</script>

Now here get the data from backend and load it in the selection box. To do this we need to understand some objects of Jenkins.

  1. Descriptor: As you now know, Descriptor is configuration class this object points to. So from jelly at any point, you can call the method from your Descriptor class.

  2. Instance: This is the object currently being configured on the configuration page. Null if it’s a newly added instance. Means by using this you can call the methods from your Action class. Like getters of field attribute.

Now how to use these objects? To use you need to first set them.

<st:bind var="backend" value="${descriptor}"/>

Here you are binding descriptor object to backend variable and this variable is now ready for use anywhere in config.jelly.  Similarly for instance, <st:bind var="backend" value="${instance}"/>.

To make calls use backend.{backend method name}() and it should call your backend method.

But if you are using this from JavaScript then you need use @JavaScriptMethod annotation over the method being called.

We can now get the REST data from backend function in JavaScript and to load the data into the element you can use the document object of JavaScript.

E.g. var selection = document.getElementById(“element-id”); This part is normal Javascript.

So after clicking on “Get Amazon Offers” button and clicking on Selection box it should now load the data.

Multiple Plugin Instance: If we are creating a multiple Build Step plugin then you can create multiple instances of your plugin while configuring it. If you try to do what we have done up till now, it will fail to load the data in the second instance. This is because the same element already exists on the UI with the same id. JavaScript will get confused while putting the data. We need to have a mechanism to create the different ids of the same fields.

I thought of one approach for this. Get the index from backend while configuring the fields and add as a suffix in id attribute.

@JavaScriptMethod
public synchronized String createEditorId() {
return String.valueOf(lastEditorId++);
}

This is the method which just returns the id+1 each time it gets called. You know now how to call backend methods from Jelly.

<j:set var="editorId" value="${descriptor.createEditorId()}" />

In this manner, we set the ID value in variable “editorId” and this can be used while creation of fields.

(Check out the selection box creation code above. I have appended this variable in ID attribute)

Now create as many instances you want in configuration page it should work fine.

Exposing Environment Variables:

Environment variables are needed quite often in Jenkins. Your plugin may require the support of some environment variables or the use of the built-in environment variables provided by Jenkins.

First, you need to create the Envvars object.

EnvVars envVars = new EnvVars();
** Assign it to the build environment.
envVars = build.getEnvironment(listener);
** Put the values which you wanted to expose as environment variable.
envVars.put("offer", getOffer);

If you print this then you will get all the default Jenkins environment variables as well as variables which you have exposed. Using this you can even use third party plugins like “Parameterized Trigger Plugin” to export the current build’s environment variable to different jobs.You can even get the value of any environment variable using this.

4. Plugin Debugging and Deployment:

You have now got an idea on how to write a plugin in Jenkins, now we move on to perform some complex tasks. We will see how to debug the issue and deploy the plugin. If you are using the IDE then debugging is same like you do for Java program similar to setting up the breakpoints and running the project.

If you want to perform any validation on fields, in the configuration class you would need to have docheck{fieldname} method which will return FormValidation object. In this example, we are validating the “name” field from our sample “HelloWorldBuilder” example.

public FormValidation doCheckName(@QueryParameter String value)
throws IOException, ServletException {

if (value.length() == 0)
return FormValidation.error("Please set a name");

 if (value.length() < 4)
return FormValidation.warning("Isn't the name too short?");

return FormValidation.ok();
 }

Plugin deployment:  

We have now created the plugin, how are we going to deploy it? We have created the plugin using Netbeans IDE and as I said earlier if you want to deploy it on your local Jenkins setup you need to use the Maven command mvn install and copy .hpi to /plugins/ folder.

But what if you want to deploy it on Jenkins Marketplace? Well, it’s a pretty long process and thankfully Jenkins has good documentation for it.

In short, you need to have a jenkins-ci.org account. Your public Git repo will have the plugin source code. Raise an issue on JIRA to get space on their Git repo and in this operation, they will have forked your git repo. Finally, release the plugin using Maven. The above document explains well what exactly needs to be done.

Conclusion:

We went through the basics of Jenkins plugin development such as classes, configuration, and some complex tasks.

Jenkins plugin development is not difficult, but I feel the poor documentation is what makes the task challenging. I have tried to cover my understanding while developing the plugin, however, it is advisable to create a plugin only if the required functionality does not already exist.

Below are some  important links on plugin development:

  1. Jenkins post build plugin development: This is a very good blog which covers things like setting up the environment, plugin classes and developing Post build action.

  2. Basic guide to use jelly: This covers how to use jelly files in Jenkins and attributes of jelly. 

You can check the code of the sample application discussed in this blog here. I hope this helps you to build interesting Jenkins plugins. Happy Coding!!


AbhishekPathare.jpg

Abhishek is a passionate software engineer who loves to learn new technologies. He has developed applications in Django and Python and is currently working on Node.js and loving it.
He is learning Japanese as well and watches a lot of anime in his free time. He loves to play sports like basketball and table tennis.