In this article I'd like to describe some basics of Jenkins CI plugins development.
Those who ever worked with Selenium Grid knows that sometimes it may stuck due to number of reasons. To minimize problems with test execution on stucked environment, we can create a simple plugin, that could shutdown hub / nodes. Of course environment killing wouldn't be enough to make our test execution process more stable. We would also need to think about some trigger to raise our configuration back. But let's start with simple things first...
Before coding, you'll need to make some preparations. Hope you already have Maven configured. To start developing Jenkins plugins, you need to add the following to .m2/settings.xml:
Now let's create a project skeleton with the following command: mvn -cpu hpi:create.
It will ask you several questions about group and artifact ids. For this example I left default groupId and set selenium-utils artifactId. After finishing with skeleton preparation, you'll see newly created selenium-utils folder with basic Jenkins plugin example that can be packaged to .hpi format (you can further upload it directly to Jenkins via plugin manager) or even deployed directly to test Jenkins instance. You can read more about project structure and common commands in the official tutorial.
Assuming you've already played with auto-generated basic Jenkins plugin, let's start developing our own, keeping in mind that our main goal is to create a trigger for killing Selenium Grid hub / nodes.
First you need to add the following dependencies into your pom.xml:
Selenium Grid provides us REST APIs we can use to shutdown hub / nodes, e.g. to kill hub we need to send the following GET request:
Jersey client can help us to communicate with Grid. So let's create a simple client:
Actually, that's pretty much related to plugin's functionality. Now we only need to configure its layout and create appropriate handlers.
Let's start with resources folder: .jelly files are intended for creating form controls. Syntax is very similar to HMTL. Note: if you use IntelliJ IDEA, you can install Stapler plugin for .jelly syntax highlighting and IntelliSense support.
For this particular case we won't need global.jelly, so you can remove it. It could be required, if we needed to setup some global setting in the following Jenkins section:
config.jelly will introduce form components to be displayed for job's build action. Now let's think about what exact components we would need to achieve our main goal - killing hub / nodes. Generally, we should know only ip addresses and ports. Assuming we have 1 hub and N nodes we want to shutdown. For hub we would probably need only 2 textboxes. But what about nodes? If we say there could be N nodes' configurations, it's not enough to have just pure textbox components. We need something iterative. For this purpose we can use repatableProperty. So our config.jelly will look like the following:
In this part you should pay attention to field attributes, as soon we'll refer them in our java classes. Besides that, we see repeatableProperty component - something that will iteratively appear on a form, if we need more items. But this component should define some internal UI structure, e.g. the node ip / port textboxes. To define such structure we need to create another config.jelly, but it should be placed in a separate folder. So let's prepare our folders structure first.
Rename HelloWordBuilder to SeleniumBuilder and create new NodeConfiguration folder on the same level with SleniumBuilder. Now let's create config.jelly in NodeConfiguration directory with following repeatable content:
Besides common textboxes you can see 2 buttons for adding new / removing existing node configuration block. Currently we don't see any relation between these 2 jelly configs yet. It will appear only on class level.
We're almost ready with layout. As you may saw, there're some help files present besides configs. This is normaly some user-friendly description we see after clicking question mark against appropriate field. Note that these files should be named like help-fieldName, where fieldName should be exactly the same, as we created in our config.jelly. We'll skip this part.
Now it's time to create our plugin's engine, that will put everything together.
Important note: our resource config folders (SeleniumBuilder and NodeConfiguration) must be reflected via java classes. So we need to use exactly the same names for our java handlers.
Let's rename existing HelloWorldBuilder class to SeleniumBuilder. It will be our main plugin class. As you may see, it extends Builder class. It means that appropriate instance will be created when user selects our plugin while job's configuration.
All declared fields should match names defined in relative config.jelly. It'll let us to remember hub / nodes settings we put during job's configuration.
Constructor should be annotated with @DataBoundConstructor and list all declared instance variables as parameters, so that we can initialize them in its body. We would also need to create appropriate fields' getters.
When user triggers a build, perform method is invoked. At this point we сan easily access our saved configuration. Let's skip it and return later.
Our plugin should also contain an extension point. Generally it's a singleton, that defines some common plugin's configuration, such as display name, form validation rules, allows to load persisted global configuration, etc.
Some of you may notice that we missed one important part - nodes configuration. As we defined an appropriate config.jelly, we should also create its java handler. It will look almost the same, as SeleniumBuilder.
Let's create a new class NodeConfiguration in the same package, as SeleniumBuilder. It should also follow the same rules, but extend different classes.
If you remember from jelly configuration part, plugin's UI supports iterative nodes configuration block. It means that we should also create a list of NodeConfiguration and pass it to SeleniumBuilder's constructor. Note that it should still contain the same name, as we've defined in config.jelly for repeatableProperty.
Hope you've already added SeleniumClient into project. Let's finalize our plugin and add appropriate logic into perform method. To shutdown hub / nodes we just need to pass ip / port parameters we've read from plugin's UI.
That's pretty much it. Now let's test it: mvn hpi:run - it will run Jenkins instance with already deployed plugin.
If you open any job's configuration and expand Build section, you'll see our plugin in a list:
Let's click it. As you see, our plugin form contains exactly the same fields we've configured in jelly files. Validation prompts us to add some values.
Assuming you've already raised hub / nodes, let's fill in our fields and save a job.
Now if you trigger a new build and open console log, you would probably see the following:
If you want to deploy a plugin to your own Jenkins instance, you should run mvn install command first to create an .hpi file (will appear in your project's target folder), that you can then manually upload from Manage Plugins / Advanced section.
You can find full plugin's source on GitHub.
So what's next? Currently we have a mechanism for killing hub / nodes. And you would probably want to extend it by adding some new features, e.g. restart trigger, or implement even better solution. Take your time and play with a plugin. Good luck!
Those who ever worked with Selenium Grid knows that sometimes it may stuck due to number of reasons. To minimize problems with test execution on stucked environment, we can create a simple plugin, that could shutdown hub / nodes. Of course environment killing wouldn't be enough to make our test execution process more stable. We would also need to think about some trigger to raise our configuration back. But let's start with simple things first...
Before coding, you'll need to make some preparations. Hope you already have Maven configured. To start developing Jenkins plugins, you need to add the following to .m2/settings.xml:
Now let's create a project skeleton with the following command: mvn -cpu hpi:create.
It will ask you several questions about group and artifact ids. For this example I left default groupId and set selenium-utils artifactId. After finishing with skeleton preparation, you'll see newly created selenium-utils folder with basic Jenkins plugin example that can be packaged to .hpi format (you can further upload it directly to Jenkins via plugin manager) or even deployed directly to test Jenkins instance. You can read more about project structure and common commands in the official tutorial.
Assuming you've already played with auto-generated basic Jenkins plugin, let's start developing our own, keeping in mind that our main goal is to create a trigger for killing Selenium Grid hub / nodes.
First you need to add the following dependencies into your pom.xml:
Selenium Grid provides us REST APIs we can use to shutdown hub / nodes, e.g. to kill hub we need to send the following GET request:
http://hubIp:port/selenium-server/driver?cmd=shutDownSeleniumServer
Jersey client can help us to communicate with Grid. So let's create a simple client:
Actually, that's pretty much related to plugin's functionality. Now we only need to configure its layout and create appropriate handlers.
Let's start with resources folder: .jelly files are intended for creating form controls. Syntax is very similar to HMTL. Note: if you use IntelliJ IDEA, you can install Stapler plugin for .jelly syntax highlighting and IntelliSense support.
For this particular case we won't need global.jelly, so you can remove it. It could be required, if we needed to setup some global setting in the following Jenkins section:
config.jelly will introduce form components to be displayed for job's build action. Now let's think about what exact components we would need to achieve our main goal - killing hub / nodes. Generally, we should know only ip addresses and ports. Assuming we have 1 hub and N nodes we want to shutdown. For hub we would probably need only 2 textboxes. But what about nodes? If we say there could be N nodes' configurations, it's not enough to have just pure textbox components. We need something iterative. For this purpose we can use repatableProperty. So our config.jelly will look like the following:
In this part you should pay attention to field attributes, as soon we'll refer them in our java classes. Besides that, we see repeatableProperty component - something that will iteratively appear on a form, if we need more items. But this component should define some internal UI structure, e.g. the node ip / port textboxes. To define such structure we need to create another config.jelly, but it should be placed in a separate folder. So let's prepare our folders structure first.
Rename HelloWordBuilder to SeleniumBuilder and create new NodeConfiguration folder on the same level with SleniumBuilder. Now let's create config.jelly in NodeConfiguration directory with following repeatable content:
Besides common textboxes you can see 2 buttons for adding new / removing existing node configuration block. Currently we don't see any relation between these 2 jelly configs yet. It will appear only on class level.
We're almost ready with layout. As you may saw, there're some help files present besides configs. This is normaly some user-friendly description we see after clicking question mark against appropriate field. Note that these files should be named like help-fieldName, where fieldName should be exactly the same, as we created in our config.jelly. We'll skip this part.
Now it's time to create our plugin's engine, that will put everything together.
Important note: our resource config folders (SeleniumBuilder and NodeConfiguration) must be reflected via java classes. So we need to use exactly the same names for our java handlers.
Let's rename existing HelloWorldBuilder class to SeleniumBuilder. It will be our main plugin class. As you may see, it extends Builder class. It means that appropriate instance will be created when user selects our plugin while job's configuration.
All declared fields should match names defined in relative config.jelly. It'll let us to remember hub / nodes settings we put during job's configuration.
Constructor should be annotated with @DataBoundConstructor and list all declared instance variables as parameters, so that we can initialize them in its body. We would also need to create appropriate fields' getters.
When user triggers a build, perform method is invoked. At this point we сan easily access our saved configuration. Let's skip it and return later.
Our plugin should also contain an extension point. Generally it's a singleton, that defines some common plugin's configuration, such as display name, form validation rules, allows to load persisted global configuration, etc.
Some of you may notice that we missed one important part - nodes configuration. As we defined an appropriate config.jelly, we should also create its java handler. It will look almost the same, as SeleniumBuilder.
Let's create a new class NodeConfiguration in the same package, as SeleniumBuilder. It should also follow the same rules, but extend different classes.
If you remember from jelly configuration part, plugin's UI supports iterative nodes configuration block. It means that we should also create a list of NodeConfiguration and pass it to SeleniumBuilder's constructor. Note that it should still contain the same name, as we've defined in config.jelly for repeatableProperty.
Hope you've already added SeleniumClient into project. Let's finalize our plugin and add appropriate logic into perform method. To shutdown hub / nodes we just need to pass ip / port parameters we've read from plugin's UI.
That's pretty much it. Now let's test it: mvn hpi:run - it will run Jenkins instance with already deployed plugin.
If you open any job's configuration and expand Build section, you'll see our plugin in a list:
Let's click it. As you see, our plugin form contains exactly the same fields we've configured in jelly files. Validation prompts us to add some values.
Assuming you've already raised hub / nodes, let's fill in our fields and save a job.
Now if you trigger a new build and open console log, you would probably see the following:
If you want to deploy a plugin to your own Jenkins instance, you should run mvn install command first to create an .hpi file (will appear in your project's target folder), that you can then manually upload from Manage Plugins / Advanced section.
You can find full plugin's source on GitHub.
So what's next? Currently we have a mechanism for killing hub / nodes. And you would probably want to extend it by adding some new features, e.g. restart trigger, or implement even better solution. Take your time and play with a plugin. Good luck!