Jenkins is an open-source CI/CD solution that software developers use for building, improving, and deploying applications. It is not the only solution of this type, but the most popular one. One of its advantages is enhancing functionality with plugins. There are numerous ready enhancements developed by different companies. Besides, developers can create their own extensions and add them to Jenkins.
The current article will review the Jenkins Plugin implementation process and its stages:
- Prepare the environment
- Implement the plugin’s UI
- Implement the plugin’s business logic
- Implement validation
- JUnit test
Let’s start.
Prepare the environment
Before starting the Jenkins plugin development, make sure to have the necessary environment ready:
- JDK 8 is installed on your machine and the JAVA_HOME environment variable is set (this variable contains the path to JDK);
- Apache Maven is installed;
- An IDE that allows developing Jenkins plugins on Java is installed (e.g., IntelliJ IDEA).
Implement the plugin’s UI (front-end)
The plugin’s UI is configured through the Jelly file. Jelly is a specialized markup language that visually resembles XML and is widely used for writing plugins in Java.
The Jelly file is created within the project’s resources and is usually named config.jelly. To make sure that Java will properly link this file to implement the plugin, the UI folder created in the project’s resources must have the same name as the Java file containing the plugin’s business logic.
For example, if the plugin’s logic is implemented in the src/main/java/org/myorganization/MyAction.java class, the UI should be implemented in the src/main/resources/org/myorganization/MyAction/config.jelly file.
As Jelly files are directly connected with the Java classes, you can call methods from these classes in Jelly. To reference the Java files they’re connected with, Jelly files use it or instance keyword – “${it.[member of classes]}”.
Suppose there is a method defined in a Java class:
public String getMyString()
{
return "Hello Jenkins!";
}
To call this method in Jelly, we need to do the following:
<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">
${it.getMyString()}
</j:jelly>
Depending on the plugin type, other objects can also be predefined:
- app – the Jenkins instance
- instance – the Java class object that is currently configured in the configuration page
- descriptor – the descriptor object that corresponds to the instance class
- h – the hudson.Functions instance that contains various useful functions.
Also, when you write UI in Jelly files, you can use resource constants. To do this, create a config.properties resource file next to the Jelly file. When this is done, the resource constants will be available for use in the ${%resourceName} format. For example:
<?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:section title="${%MySection}">
<f:block>${%MySectionDesc}</f:block>
<f:textbox />
</f:section>
</j:jelly>
There is the help functionality in Jenkins too. An HTML file with a ‘help-’ prefix (e.g., help-FIELD.html) is formed in the web app folder of the plugin’s project. To connect it with a specific field, the help=”path to help file” statement is used.
For example:
<?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:section title="${%MySection}">
<f:entry title="${%MyField}" field="myField" help="/plugin/MyJenkinsPlugin/resources/io/jenkins/plugins/sample/MyBuilder/help-myField.html">
<f:textbox />
</f:entry>
</f:section>
</j:jelly>
Also, when you’re designing UI, keep in mind that you won’t need to constantly restart the Jenkins server. Instead, just change the *.jelly file in your IDE and request the page again.
You may also find the following resources useful when designing UI in Jelly:
Implement the business logic (back-end)
We need to implement the business logic of the Jenkins plugin. It can be done with the Build step implementation. The approach will be similar for other step types (pre-build, post-build, publisher).
To implement any plugin that creates a Build step on the output, we need to first implement the class that inherits the Builder abstract class.
The Builder encapsulates the basic logic of any build step:
public class MyStepBuilder extends Builder {
When implementing a plugin, the UI configuration part (Jelly file) and the Java file are directly related. All required plugin parameters should be set via a constructor. To do this, mark the constructor with the @DataBoundConstructor annotation and pass all required parameters to it as arguments.
private final String param1, param2;
@DataBoundConstructor
public MyStepBuilder (String param1, String param2)
{
this.param1 = param1;
this.param2 = param2;
}
Next, we need to define the getters for the declared fields. This is done to make sure that the UI can get the necessary values in the case of us modifying an already created step configuration.
public String getParam1()
{
return param1;
}
Optional parameters are defined via the setters. For the UI to have access to them, they should be marked with the @DataBoundSetter annotation.
@DataBoundSetter
public void setOptionalParam (String optionalParam) {
this.optionalParam = optionalParam;
}
Now, let’s look at an implementation of the perform method. The build step actions are executed directly within it:
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener)
{
// some code
}
In this method, we have access to:
- build– the entity that provides us with the build settings and various info about it.
- launcher – the Jenkins executable environment entity.
- listener – the entity that allows us to access the logger and control the build results.
We should also note a few additional redefinition methods that may prove useful. For example, we can use the prebuild method to provide additional environment validation before building.
The getRequiredMonitorService() method serves for synchronization with other builds in the scope of the task. This can be useful in the process of integration with internal tools that don’t support parallel use. The method can return one of the following values:
- BuildStepMonitor.BUILD – if the step requires the absence of unfinished builds
- BuildStepMonitor.STEP – if the step requires the absence of similar unfinished steps in other builds
- BuildStepMonitor.NONE – if synchronization is not required
Implement validation
We have to validate the environment before building. In addition to that, the prebuild method allows us to validate parameter values as they are set.
This is achieved by implementing the descriptor inside of the Builder class. The descriptor should implement the BuildStepDescriptor abstract class. The @Extension annotation connects it with the Builder class. For example:
@Extension
public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
Inside, you can set additional parameters for the step by redefining the methods. For example, DisplayText and Help can be set like this:
@Override
public String getDisplayName()
{
return "Name for our plugin";
}
@Override
public String getHelpFile()
{
return "/plugin/MyJenkinsPlugin/resources/io/jenkins/plugins/sample/help-myplugin.html";
}
To validate the specific field with the descriptor, create a public method with the following signature:
FormValidation doCheck[FieldNameInCamelCase](@QueryParameter String value)
By implementing the logic of checking a specific field’s value, we achieve validation directly when the user configures the step. For example:
public FormValidation doCheckParam1(@QueryParameter String value)
{
if (value.length() == 0)
return FormValidation.error(Messages.MyBuilder_DescrImpl_errors_missingParam1());
return FormValidation.ok();
}
This particular implementation of the doCheck method will cause the warning message if the user leaves the param1 field empty. You won’t need to write any additional code in the Jelly file – Jenkins will connect fields with the validator itself.
Also, the descriptor contains many other methods (load, save, configure, etc.). It can be helpful to redefine them when implementing the build settings validation on the stage of configuration.
JUnit test coverage
In writing Jenkins plugins, you should cover all developed steps by JUnit tests extensively. An example of such tests is provided with the sample plugin by Maven.
Two entities/moqs that are very useful in test coverage:
- JenkinsRule – moq of the Jenkins executable environment
- FreeStyleProject – moq of the Jenkins project
Using them, we can easily test if the connection between the Jelly file and Java is configured properly:
@Rule
public JenkinsRule jenkins = new JenkinsRule();
private final String param1 = "param1", param2 = "param2";
@Test
public void testConfigRoundtrip() throws Exception
{
FreeStyleProject project = jenkins.createFreeStyleProject();
project.getBuildersList().add(new MyStepBuilder(param1, param2));
project = jenkins.configRoundtrip(project);
MyStepBuilder myStepBuilder = new MyStepBuilder(param1, param2);
jenkins.assertEqualDataBoundBeans(myStepBuilder, project.getBuildersList().get(0));
}
If the binding is incorrect, the two following results are possible:
- An exception will occur (e.g., the constructor accepts a different set of parameters)
- The object’s end state will not correspond to the reference state
Conclusion
Jenkins plugins are extremely helpful in many cases. It is quite easy to learn how to make one, and we hope that this article helps you to master the job. In any case, practice will polish your skills, and numerous plugins-making tutorials will support you on the way.
Tags: jenkins Last modified: September 24, 2021