Introducing Variability into Jenkins Plugins

Stephen Connolly's picture

All at the same time, this question has popped up from multiple sources. I think it is an indication that a new wave of plugin developers are seeking to do more complex things in their Jenkins plugins.

So this post aims to help explain for Jenkins Plugin developers how to introduce variability and develop better modelling of configuration within your Jenkins Plugin.

If you want a hint as to how powerful this can give you for your UI, I suggest you take a look at the “Deploy Now” page in the CloudBees Deployer plugin (unfortunately the code base is still closed source… Note to self check up on the status of getting some of the required dependencies public so that I can make the source code public again)

We have a multiple list of different types of “Host service” (which is also an extension point, so other plugins can provide their own “Host service”, e.g. App Engine and Cloud Foundry being two such implementations.

Then each “Host service” has multiple “Applications” (all the one type, but a different type for each “Host service”).

Finally, each “Application” has a single selection for the application source (“first match”, “maven artifact selection”, etc)

Each layer provides its own set of details that are specific to that layer. So for example the Host service asks for one set of options that make sense at the host service level, e.g. a CloudBees RUN@cloud service asks for the account on the CloudBees service to deploy into, on the other hand the App Engine host service asks a different set of questions.

So that is complex… what about if you just want to start off on that road…

Lets say your question is something like: 

Is there a way to have 2 different config.jelly files and control which is displayed based on what I choose from a combo box above the content of those jelly files?

The way I normally handle this type of thing is to use a Describable object tree to map the different types of object that you want to allow the user to configure from.

Normally you do this by extending AbstractDescribableImpl, but you can just add the Describable interface to the root of your existing object tree and add the required methods if otherwise modifying the object tree is too difficult (typically you have a class that must be a superclass of the base of your object tree)

So in your case you will have something like

public abstract class Product extends AbstractDescribableImpl<Product> {
 ... 
}

public abstract class ProductDescriptor extends Descriptor<Product> {
 ... 
} 

And then for each type of product you will then do something like

public class Car extends Product { 
 @DataBoundConstructor public Car(...) { ... }

 ...

 @Extension public static class DescriptorImpl extends ProductDescriptor {
 public String getDisplayName() { return "Car"; }

 ...
 }
}

public class Banana extends Product {
 @DataBoundConstructor public Banana(...) { ... }

 ...
 @Extension public static class DescriptorImpl extends ProductDescriptor {
 public String getDisplayName() { return "Banana"; }

 ...
 }
} 

It is important that each concrete Product subclass has a @DataBoundConstructor annotated constructor and aDescriptorImpl (though the class name can be anything you like, convention is DescriptorImpl) annotated with @Extension

The methods you expose in each class is up to you. Typically you will put the getters for each specific product fields in the specific class, so for example you would have getManufacturer()getFuelType() etc with corresponding backing fields inCar the field validation would go in Car.DescriptorImpl e.g. thedoCheckManufacturer(@QueryParameter String value)doFillFuelTypeItems(), etc methods.

Where there are fields common for all Product instances, you put their getters in the base abstract class and their field validation / autocomplete in the base descriptor class.

For each class you then implement your config.jelly and optional global.jelly.

If it were me, I would have a Product/config.jelly that then included the optional bits from the concrete child class, e.g.

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form">
 <f:entry field="name" title="Product name">
 <f:textbox/>
 </f:entry>
 <st:include page="config-detail.jelly" optional="true" class="${descriptor.clazz}"/>
</j:jelly> 

Then, only if the child class actually has any relevant configuration would I define the child classes config-detail.jelly, e.g.Car/config-detail.jelly would look something like

<?jelly escape-by-default='true'?> 
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form">
 <f:entry field="manufacturer" title="Manufacturer">
 <f:textbox/>
 </f:entry>
 <f:entry field="fuelType" title="FuelType">
 <f:select/>
 </f:entry>
</j:jelly> 

Now at this point we hit the actual trick…

In the class that needs these Product instances, which is also ultimately being instantiated by a @DataBoundConstructor or using the stapler binding against an already constructed object (which means you would need getters and setters for the form fields)

So lets say you have something like

public class ProductsJobProperty extends JobProperty<AbstractProject<?,?>> {
 private final List<Product> products;
 @DataBoundConstructor public ProductsJobProperty(List<Product> products) {
 this.products = products == null ? new ArrayList<Product>() : new ArrayList<Product>(products);
 } 
 public List<Product> getProducts() { return Collections.unmodifiableList(products); }
 ...
 @Extension public static class DescriptorImpl extends JobPropertyDescriptor {
 public String getDisplayName() { return "Products under test"; }
 ...
 public List<ProductDescriptor> getProductDescriptors() {
 // you may want to filter this list of descriptors here, if you are being very fancy
 return Jenkins.getInstance().getDescriptorList(ProductDescriptor.class);
 }
 } 
} 

In other words this is a property of a job that stores the type of products that the job is testing or something like that (this works for any class, just trying to show how to get it with a simple example)

And finally, to get the flexible list of product types in ProductsJobProperty/config.jelly you have something like

<?jelly escape-by-default='true'?> 
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form">
 <f:entry title="Products">
 <f:hetero-list name="products" items="${instance.products}" hasHeader="true"
 descriptors="${descriptor.productDescriptors}"
 targetType="${fully.qualified.class.name.Product.class}" />
 </f:entry>
</j:jelly> 

You may have to play slightly with that tag. And you will need to provide the actual fully qualified class name (or provide a getter for that class from your  ProductsJobProperty.DescriptorImpl

Another option to look at (if you only want one product) is

<?jelly escape-by-default='true'?> 
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form">
 <f:entry title="Product">
 <f:hetero-radio field="product" descriptors="${descriptor.productDescriptors}" />
 </f:entry> 
</j:jelly> 

If you want a drop-down list in place of a radio list, that would be

<?jelly escape-by-default='true'?> 
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form">
 <f:dropdownDescriptorSelector title="Product" field="product"
 descriptors="${descriptor.productDescriptors}" /> 
</j:jelly> 

I hope this all helps you on the road to enlightenment

—Stephen Connolly
CloudBees
www.cloudbees.com


Stephen Connolly has over 20 years experience in software development. He is involved in a number of open source projects, including JenkinsStephen was one of the first non-Sun committers to the Jenkins project and developed the weather icons. Stephen lives in Dublin, Ireland - where the weather icons are particularly useful. Follow Stephen on Twitter and on his blog.

Add new comment