This is a guest post by Chris Price.
Chris is a software engineer at Puppet, and has been
spending some time lately on automating performance testing using the latest
Jenkins features.
In this blog post, I’m going to attempt to provide some step-by-step notes on
how to refactor an existing Jenkins plugin to make it compatible with the new
Jenkins Pipeline jobs. Before we get to the fun stuff, though, a little
background.
How’d I end up here?
Recently, I started working on a project to automate some performance tests for
my company’s products. We use the awesome Gatling load
testing tool for these tests, but we’ve largely been handling the testing very
manually to date, due to a lack of bandwidth to get them automated in a clean,
maintainable, extensible way. We have a years-old Jenkins server where we use
the gatling jenkins
plugin to track the
history of certain tests over time, but the setup of the Jenkins instance was
very delicate and not easy to reproduce, so it had fallen into a state of
disrepair.
Over the last few days I’ve been putting some effort into getting things more
automated and repeatable so that we can really maximize the value that we’re
getting out of the performance tests. With some encouragement from the fine
folks in the #jenkins IRC channel, I ended up exploring
the JobDSL
plugin and the new Pipeline jobs. Combining those two
things with some Puppet code to provision a Jenkins server via the
jenkins puppet module gave me
a really nice way to completely automate my Jenkins setup and get a seed job in
place that would create my perf testing jobs. And the Pipeline job format is
just an awesome fit for what I wanted to do in terms of being able to easily
monitor the stages of my performance tests, and to make the job definitions
modular so that it would be really easy to create new performance testing jobs
with slight variations.
So everything’s going GREAT up to this point. I’m really happy with how it’s
all shaping up. But then… (you knew there was a "but" coming, right?) I
started trying to figure out how to add the
Gatling Jenkins
plugin to the Pipeline jobs, and kind of ran into a wall.
As best as I could tell from my Googling, the plugin was probably going to
require some modifications in order to be able to be used with Pipeline jobs.
However, I wasn’t able to find any really cohesive documentation that
definitively confirmed that or explained how everything fits together.
Eventually, I got it all sorted out. So, in hopes of saving the next person a
little time, and encouraging plugin authors to invest the time to get their
plugins working with Pipeline, here are some notes about what I learned.
Spoiler: if you’re just interested in looking at the individual git commits that
I made on may way to getting the plugin working with Pipeline, have a look at
this github
branch.
Creating a pipeline step
The main task that the Gatling plugin performs is to archive Gatling reports
after a run. I figured that the end game for this exercise was that I was going
to end up with a Pipeline "step" that I could include in my Pipeline scripts, to
trigger the archiving of the reports. So my first thought was to look for an
existing plugin / Pipeline "step" that was doing something roughly similar, so
that I could use it as a model. The Pipeline "Snippet Generator" feature
(create a pipeline job, scroll down to the "Definition" section of its
configuration, and check the "Snippet Generator" checkbox) is really helpful for
figuring out stuff like this; it is automatically populated with all of the
steps that are valid on your server (based on which plugins you have installed),
so you can use it to verify whether or not your custom "step" is recognized, and
also to look at examples of existing steps.
Looking through the list of existing steps, I figured that the archive step
was pretty likely to be similar to what I needed for the gatling plugin:
So, I started poking around to see what magic it was that made that archive
step show up there. There are some mentions of this in the
pipeline-plugin
DEVGUIDE.md and the
workflow-step-api-plugin
README.md, but the real breakthrough for me was finding the definition of the
archive step in the workflow-basic-steps-plugin source
code.
With that as an example, I was able to start poking at getting a
gatlingArchive step to show up in the Snippet Generator. The first thing that
I needed to do was to update the gatling-plugin project’s pom.xml to depend
on a recent enough version of Jenkins, as well as specify dependencies on the
appropriate pipeline
plugins
Once that was out of the way, I noticed that the archive step had some tests
written for it, using what looks to be a pretty awesome test API for pipeline
jobs and plugins. Based on those archive
tests,
I added
a
skeleton for a test for the gatlingArchive step that I was about to write.
Then, I moved on to
actually
creating the step. The meat of the code was this:
public class GatlingArchiverStep extends AbstractStepImpl {
@DataBoundConstructor
public GatlingArchiverStep() {}
@Extension
public static class DescriptorImpl extends AbstractStepDescriptorImpl {
public DescriptorImpl() { super(GatlingArchiverStepExecution.class); }
@Override
public String getFunctionName() {
return "gatlingArchive";
}
@NonNull
@Override
public String getDisplayName() {
return "Archive Gatling reports";
}
}
}
Note that in that commit I also added a config.jelly file. This is how you
define the UI for your step, which will show up in the Snippet Generator. In
the case of this Gatling step there’s really not much to configure, so my
config.jelly is basically empty.
With that (and the rest of the code from that commit) in place, I was able to
fire up the development Jenkins server (via mvn hpi:run, and note that you
need to go into the "Manage Plugins" screen on your development server and
install the Pipeline plugin once before any of this will work) and visit the
Snippet Generator to see if my step showed up in the dropdown:
GREAT SUCCESS!
This step doesn’t actually do anything yet, but it’s recognized by Jenkins and
can be included in your pipeline scripts at that point, so, we’re on our way!
The step metastep
The step that we created above is a first-class DSL addition that can be used in
Pipeline scripts. There’s another way to make your plugin work usable from a
Pipeline job, without making it a first-class build step. This is by use of the
step"metastep", mentioned in the pipeline-plugin
DEVGUIDE.
When using this approach, you simply refactor your Builder or Publisher to
extend SimpleBuildStep, and then you can reference the build step from the
Pipeline DSL using the step method.
In the Jenkins GUI, go to the config screen for a Pipeline job and click on the
Snippet Generator checkbox. Select 'step: General Build Step' from the
dropdown, and then have a look at the options that appear in the 'Build Step'
dropdown. To compare with our previous work, let’s see what "Archive the
artifacts" looks like:
From the snippet generator we can see that it’s possible to trigger an Archive
action with syntax like:
step([$class: 'ArtifactArchiver', artifacts: 'foo*', excludes: null])
This is the "metastep". It’s a way to trigger any build action that implements
SimpleBuildStep, without having to actually implement a real "step" that
extends the Pipeline DSL like we did above. In many cases, it might only make
sense to do one or the other in your plugin; you probably don’t really need
both.
For the purposes of this tutorial, we’re going to do both. For a couple of reasons:
Why the heck not? :) It’s a good demonstration of how the metastep stuff
works.
Because implementing the "for realz" step will be a lot easier if the Gatling
action that we’re trying to call from our gatlingArchive() syntax is using the
newer Jenkins APIs that are required for subclasses of SimpleBuildStep.
GatlingPublisher is the main build action that we’re interested in using in
Pipeline jobs. So, with all of that in mind, here’s our next goal: get
step([$class: 'GatlingPublisher', …) showing up in the Snippet Generator.
The javadocs for the SimpleBuildStep
class
have some notes on what you need to do when porting an existing Builder or
Publisher over to implement the SimpleBuildStep interface. In all
likelihood, most of what you’re going to end up doing is to replace occurrences
of AbstractBuild with references to the Run class, and replace occurrences
of AbstractProject with references to the Job class. The APIs are pretty
similar, so it’s not too hard to do once you understand that that’s the game.
There is some discussion of this in the pipeline-plugin
DEVGUIDE.
For the Gatling plugin, my
initial
efforts to port the GatlingPublisher over to implement SimpleBuildStep only
required the AbstractBuild → Run refactor.
After making these changes, I fired up the development Jenkins server, and, voila!
So, now, we can add a line like this to a Pipeline build script:
step([$class: 'GatlingPublisher', enabled: true])
And it’ll effectively be the same as if we’d added the Gatling "Post-Build
Action" to an old-school Freestyle project.
Well… mostly.
Build Actions vs. Project Actions
At this point our modified Gatling plugin should work the same way as it always
did in a Freestyle build, but in a Pipeline build, it only partially works.
Specifically, the Gatling plugin implements two different "Actions" to surface
things in the Jenkins GUI: a "Build" action, which adds the Gatling icon to the
left sidebar in the GUI when you’re viewing an individual build in the build
history of a job, and a "Project" action, which adds that same icon to the left
sidebar of the GUI of the main page for a job. The "Project" action also adds a
"floating panel" on the main job page, which shows a graph of the historical
data for the Gatling runs.
In a Pipeline job, though, assuming we’ve added a call to the metastep, we’re
only seeing the "Build" actions. Part of this is because, in the last round of
changes that I linked, we only modified the "Build" action, and not the
"Project" action. Running the metastep in a Pipeline job has no visible effect
at all on the project/job page at this point. So that’s what we’ll tackle next.
The key thing to know about getting "Project" actions working in a Pipeline job
is that, with a Pipeline job, there is no way for Jenkins to know up front what
steps or actions are going to be involved in a job. It’s only after the job
runs once that Jenkins has a chance to introspect what all the steps were. As
such, there’s no list of Builders or Publishers that it knows about up front to
call getProjectAction on, like it would with a Freestyle job.
This is where
SimpleBuildStep.LastBuildAction
comes into play. This is an interface that you can add to your Build actions,
which give them their own getProjectActions method that Jenkins recognizes and
will call when rendering the project page after the job has been run at least
once.
So, effectively, what we need to do is to
get
rid of the getProjectAction method on our Publisher class, modify the Build
action to implement SimpleBuildStep.LastBuildAction, and encapsulate our
Project action instances in the Build action.
The build action class now constructs an instance of the Project action and
makes it accessible via getProjectActions (which comes from the
LastBuildAction interface):
public class GatlingBuildAction implements Action, SimpleBuildStep.LastBuildAction {
public GatlingBuildAction(Run build, List sims) {
this.build = build;
this.simulations = sims;
List projectActions = new ArrayList<>();
projectActions.add(new GatlingProjectAction(build.getParent()));
this.projectActions = projectActions;
}
@Override
public Collection getProjectActions() {
return this.projectActions;
}
}
After making these changes, if we run the development Jenkins server, we can see
that after the first successful run of the Pipeline job that calls the
GatlingPublisher metastep, the Gatling icon indeed shows up in the sidebar on
the main project page, and the floating box with the graph shows up as well:
Making our DSL step do something
So at this point we’ve got the metastep syntax working from end-to-end, and
we’ve got a valid Pipeline DSL step ( gatlingArchive()) that we can use in our
Pipeline scripts without breaking anything… but our custom step doesn’t
actually do anything. Here’s the part where we tie it all together… and it’s
pretty easy! All we need to do is to make our step "Execution" class
instantiate a Publisher and call perform on
it.
As per the
notes
in the pipeline-plugin DEVGUIDE, we can use the @StepContextParameter
annotation to inject in the objects that we need to pass to the Publisher’s
perform method:
public class GatlingArchiverStepExecution extends AbstractSynchronousNonBlockingStepExecution {
@StepContextParameter
private transient TaskListener listener;
@StepContextParameter
private transient FilePath ws;
@StepContextParameter
private transient Run build;
@StepContextParameter
private transient Launcher launcher;
@Override
protected Void run() throws Exception {
listener.getLogger().println("Running Gatling archiver step.");
GatlingPublisher publisher = new GatlingPublisher(true);
publisher.perform(build, ws, launcher, listener);
return null;
}
}
After these changes, we can fire up the development Jenkins server, and hack up
our Pipeline script to call gatlingArchive() instead of the metastep
step([$class: 'GatlingPublisher', enabled: true]) syntax. One of these is
nicer to type and read than the other, but I’ll leave that as an exercise for
the reader.
Fin
With that, our plugin now works just as well in the brave new Pipeline world as
it did in the olden days of Freestyle builds. I hope these notes save someone
else a little bit of time and googling on your way to writing (or porting) an
awesome plugin for Jenkins Pipeline jobs!
Links
Jenkins Pipeline Overview
Pipeline Plugin Developer Guide
Jenkins Source Code
Workflow Step API Plugin
Workflow Basic Steps Plugin