Jump to: navigation, search

Hudson-ci/writing-first-hudson-plugin

Developing your first Hudson Plugin

Introduction

Hudson is a popular Open Source Continuous Integration (CI) tool written purely in Java. Apart from being an Open Source product, the popularity of Hudson is due to its extensible nature using Plugins and the Plugin developer echo system. Plugins allow users and developers to do everything from customizing the way builds are done, results are displayed and notified and integration with Application LifeCycle Management systems such as SCM, Testing and Analysis tools etc. More than 400 Hudson Plugins, supporting various aspects of Continuous Integration, are available for free install. This article is for beginners who are interested in understanding the fundamentals of Hudson Plugin Development.

Hudson provides a series of extension points that allow developers to extend Hudson’s functionality. The Hudson HPI (Hudson Plug-in Interface) tool, a Maven plugin, helps developers to create, build, run and debug plugins that contains one or more extensions to these extension points. This article will cover:

  • Creating Hudson plug-in using Hudson HPI tool
  • Extending a Hudson Extension Point to provide custom implementation
  • Writing configuration for the Extension Point
  • How to add Global Configuration for the Extension Point

Creating and running the Hudson Plugin

This part of the article explains how to create a Hudson Plugin using a Hudson HPI tool, run the Plugin project and view it in action in the Hudson Test Server.

Tip: Hudson extensively uses maven, another popular Open Source software project management 
and comprehension tool. Maven can be downloaded from http://maven.apache.org/.

Generating the Plugin skeleton

The very first step is to create the Plugin skeleton. Hudson comes with a tool to generate the minimal sources. The simple command is

 mvn hpi:create

This command tells maven to create the Hudson Plugin skeleton source using the Hudson HPI tool. Maven downloads all the required software to execute the command and prompts the user needs

Enter the groupId of your plugin: org.sample.hudson
Enter the artifactId of your plugin: sample-plugin
Tip: If maven throws the following error

[ERROR] No plugin found for prefix 'hpi' in the current project ..

then the following entry need to be at ~/.m2/settings.xml file

<pluginGroups>

  <pluginGroup>org.jvnet.hudson.tools</pluginGroup>
</pluginGroups>

The name of the generated folder depends on the artifactId provided. It will have the following layout

pom.xml - Maven POM file which is used to build your plugin
src/main/java - Java source files of the plugin
src/main/resources - Jelly view files of the plugin
src/main/webapp - Static resources of the plugin, such as images and HTML files.

Building and running the Plugin Project

The Hudson Plugin project created is minimal but a complete maven project. It can be built and run with out any modification using maven. The Plugin project is built with the command

mvn package

The package command tells maven to build the project and create the HPI package that can be installed directly to a Hudson server.

The skeleton Plugin project has a sample Extension, which is fully functional. It is possible to run the project and see the result of the extension added by this skeleton Hudson Plugin. The Plugin Project is run with the command

mvn hpi:run

Since the Plugin Project is a maven project, the configuration of the project is defined in the POM.xml. The maven goal hpi:run is responsible for several of the tasks including starting the Jetty Server, adding Hudson as a Web Application to that Server and installing the Plugin to Hudson . The “work” sub-folder in the Plugin Project folder is used as Hudson Home. The “work/plugins” folder, contains list of .hpi files corresponding to various bundled plugins. The only notable difference is a .hpl which corresponds to the Plugin Project being run. It is a simple text file which contains metadata describing all the files (classes, jars and resources) associated with the currently built Hudson Plugin. This file is generated by the HPI tool every time the plugin project is run using hpi:run. Hudson knows how to interpret this file and load the entire plugin with out packaging the plugin to a .hpi package. This makes it easy to debug during development time.

Once the Plugin Project is run successfully, and the Jetty Server fully started, Hudson Main page can be viewed using a browser window and typing the URL

http://localhost:8080

Examining the sample extension

As seen above, the hpi:run command installs the currently developed Plugin to Hudson which is added to the Jetty server as a Web Application. This Plugin

  • Adds an extension to the Hudosn builder interface. This sample custom builder called HelloBuilder does not do anything fancy, but simply prints out the text “Hello <name>” in the build console log.
  • Provides UI to configure the HelloBuilder Extension.

It is easy to see the HelloBuilder in action by creating a simple Hudson project, say “TestProject” and configuring it. The HelloBuilder is available as a builder in the Build section of the configuration page. It can be added as a builder to the TestProject from the drop down menu. Once the HelloBuilder is set as the builder for the project, the Build section displays the HelloBuilder as one of the Builder. The task of the HelloBuilder is to say “Hello <name>”. Input a name in the provided in the name TextBox.

Since HelloBuilder is set as the only builder of the TestProject project, when a build is started, HelloBuilder will be asked to perform its task. The only task of the HelloBuilder is to print out the message “Hello <name>” to the console log. Once a build of the TestProject is completed, the result of HelloBuilder can be viewed in the build console output.

Extending an Extension Point

The HelloBuilder Extension

This part of the article will look in to some of the code that extends the Builder Extension Point to understand

  • How to extend and extension Point.
  • How to implement the methods to extend the functionality encapsulated by the extension point

Hudson provides the concept of Extension Points and Extensions to facilitate contribution of functionalites to the core platform by plugins. Extension points are interfaces that encapsulate entry points to extend certain services or functionality of a service provided by the core platform.

Amongst various services provided by Hudson, the foremost is building a job. A job, which is a build-able project consists of several configurable area and build steps. Some of the build steps are

  • SCM checkout - Based on SCM type, source code is checked out
  • Pre-build - Invoked to indicate that the build is starting
  • Build wrapper - Prepare an environment for the build.
  • Builder runs - Actual building like calling Ant, Make, etc. happen.
  • Recording - Record the output from the build, such as test results.
  • Notification - Send out notifications, based on the results determined so far.

Builders are responsible for building jobs. The Extension Point provided by Hudson to contribute to this builder run step is aptly called Builder. Hudson comes bundled with two of the most popular builders - Ant and Maven. They are in fact Extensions to the Builder Extension Point. So it is possible for a plugin to provide its own Builder Extension as one of the Builders of the Job. Several external plugins exist for other popular builders such as make, gradle, rake etc. HelloBuilder, our example Builder Extension is a contrived example to understand how extensions are built. Far more sophisticated Builder Extensions are possible uisng the Builder Extension Point. Let us examine the source to understand how the extension mechanism works.

Tip: Even though in this article the sample Extension is referred  as  HelloBuilder,
the Java Class corresponding to it is called HelloWorldBuilder.java.

Examining HelloBuilder Extension

In order for Hudson to understand a class as an Extension, it must

  • Extend a class that advertise itself as an Extension Point
  • Implement the required abstract methods to extend the functionality
  • Tell Hudson that the particular class is an Extension

Looking at the source HelloWorldBuilder.java one would notice, the class HelloWorldBuilder extends the class Builder which is the Extension Point for the Builder interface.

public class HelloWorldBuilder extends Builder {

The Builder class itself is a subclass of BuildStep, which defines the abstract method that needs to be implemented by the Extensions to contribute to the builder interface. The abstract method needed to be implemented by any Builder Extension is

public boolean perform(AbstractBuild<?, ?> ab, Launcher launcher, BuildListener bl) 
                                             throws InterruptedException, IOException;

BuildStep.perform(..) overridden by HelloBuilder will be called by Hudson to include the BuildStep functionality extended by HelloBuilder extension.

Finally to tell Hudson, the class is an Extension to some Extension Point, it must be annotated with the annotation @Extension. The annotation @Extension at the inner class DescriptorImpl tells Hudson the class is an Extension.

@Extension  
public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {

Examining the abstract method BuildStep.perform(..)

The BuildStep.perform(..) abstract method gives access to three object

  • Build - Object representing the build of the job being performed. Build in turn give access to important model objects like
    • Project - The buildable Job
    • Workspace - The folder where the build happens
    • Result - Result of the build until this build step
  • Launcher - Launcher which is used to launch the build of this job
  • BuildListener - An interface to communicate the status of the build steps being performed in this Builder and send any console message from this Build Step to Hudson

HelloBuilder uses the BuildListener model object to print the Hello message to the console. in the code

@Override
public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) {
 
 if(getDescriptor().useFrench())
 listener.getLogger().println("Bonjour, "+name+"!");
 else
 listener.getLogger().println("Hello, "+name+"!");
 return true;
}

listener.getLogger() gets the Logger from the Build Listener Object whose output goes to the console. The code simply prints “Hello <name>” via the logger.

Modifying HelloBuilder perform(..) method

Since the BuildStep.perform(..) method gives access to the Launcher object it is easy to

  • Use the launcher to execute an external executable
  • Send the result of execution to the console

This is done by adding the following code to the BuildStep.perform(..) method

List<Cause> buildStepCause = new ArrayList();
 buildStepCause.add(new Cause() {
 public String getShortDescription() {
 return "Build Step started by Hello Builder";
 }
});
 listener.started(buildStepCause);
 
 ArgumentListBuilder args = new ArgumentListBuilder();
 if (launcher.isUnix()) {
 args.add("/bin/ls");
 args.add("-la");
 } else {
 args.add("dir"); //Windows
 }
 String homeDir = System.getProperty("user.home");
 args.add(homeDir);
 try {
 int r;
 r = launcher.launch().cmds(args).stdout(listener).join();
 if (r != 0) {
 listener.finished(Result.FAILURE);
 return false;
 }
 } catch (IOException ioe) {
 ioe.printStackTrace(listener.fatalError("Execution" + args + "failed"));
 listener.finished(Result.FAILURE);
 return false;
 } catch (InterruptedException ie) {
 ie.printStackTrace(listener.fatalError("Execution" + args + "failed"));
 listener.finished(Result.FAILURE);
 return false;
 }
 
 listener.finished(Result.SUCCESS);


Running the Plugin Project again shows the console output as.

Started by user anonymous
Hello, winston!
Build Step started by Hello Builder
$ /bin/ls -la /Users/winstonp
total 320
drwxr-xr-x 16 winstonp staff 544 Nov 10 2010 Adobe MAX
drwx------+ 31 winstonp staff 1054 Aug 31 14:54 Desktop
..
Finished: SUCCESS 


If there is an error, the exception corresponding to the error is displayed as

Started by user anonymous
Hello, winston!
Build Step started by Hello Builder
$ "/bin/ls -la" /Users/winstonp
FATAL: Execution[/bin/ls -la, /Users/winstonp]failed
java.io.IOException: Cannot run program "/bin/ls -la": error=2, No such file or directory
 at java.lang.ProcessBuilder.start(ProcessBuilder.java:460)
 at hudson.Proc$LocalProc.<init>(Proc.java:192)
 at hudson.Proc$LocalProc.<init>(Proc.java:164)
 ...
Finished: FAILURE 

Analyzing HelloBuilder perform(..) method

The code added to perform(..) is contrived, but explains some of the important concepts

When a build step is started or stopped let Hudson know about it. This is done via the Job Build Listener interface.

listener.started(buildStepCause);
 ..
 ..
listener.finished(Result.SUCCESS);
 

This is important for two reasons.

  • Hudson heuristically shows the progress of the overall build of the job
  • When a build step fails, Hudson must stop the overall progress of the build and marks the build as FAILED. This is done by sending a message to Hudson about the status of the build via BuildListener.

Use the Launcher interface to launch your external executable. Send the console outputs of your execution to Hudson

int r;
r = launcher.launch().cmds(args).stdout(listener).join();
if (r != 0) {
 listener.finished(Result.FAILURE);
 return false;
} 

Launcher correctly launch the application in the Master or Slave node the job is running. Always use the return status of the Launcher to find out if the execution was successful. The standard output of the Launcher is hooked to the listener. This sends console output of the execution to Hudson. This is how the output of the command to list the User Directory is displayed in the build console.

Notify Hudson of any failure of the Build Step

} catch (IOException ioe) {
 ioe.printStackTrace(listener.fatalError("Execution" + args + "failed"));
 listener.finished(Result.FAILURE);
 return false;
} 

The StackTrace of the exception is send to Hudson via

Exception.printStackTrace(lister.fatalError(..)). 

Summary

Hudson Continuous Integration Server is a popular Open Source project and recently became a Technology project at Eclipse Foundation. It has an eco system of Plugin developers developing plugins for various aspects of this Continuous Integration System. The Hudson Plugin Development Environment provides a rich set of Extension Points for Plugin Developers to develop their custom plugins. This article explored the fundamentals of the Plugin Development Environment explaining various steps involved in developing a simple plugin using Hudson HPI tool.