This tutorial provides instructions for the 4 step process to create a
functional plugin called MyJenkins
that retrieves the last build
number from a Jenkins job and stores it in an output parameter.
The four-step plugin creation process
-
Create plugin workspace.
-
Define plugin spec and generate plugin.
-
Implement additional logic.
-
Build plugin and test.
Prerequisites
These are assumptions for this tutorial.
-
An active CloudBees CD/RO instance.
-
A machine with Docker installed that is registered as a pingable resource in CloudBees CD/RO.
-
Internet connection.
-
The
pdk
tool is installed and setup.
Install and setup Jenkins
Pull the Docker Jenkins image that already has CloudBees CD/RO agent installed from the Docker hub.
docker pull electricflow/jenkins
Preface sudo with Docker commands in case you have not
setup Docker to run as a non-root user.
|
Start the Jenkins Docker container as follows:
docker run -p "8080:8080" -d electricflow/jenkins:latest
Do the following to confirm that your container is running:
ubuntu@ubuntu:~$ sudo docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 0d84b3b113a9 electricflow/jenkins:latest "/bin/bash -c /work/…" 2 weeks ago Up 2 weeks 8043/tcp, 0.0.0.0:8080->8080/tcp sleepy_khayyam
The above snippet is provided for reference only. Your output would be similar, but not exactly the same as above.
Once you see that Docker container is running, point your browser to
your Docker machine address using port 8080. If you’re using localhost
,
just visit http://localhost:8080. You should see a Jenkins instance
login screen. The credentials for this Jenkins instance are
admin
/changeme
.
If you can login to the above instance, you are done with your Jenkins environment setup.
Step 1: Create plugin workspace
After making sure pdk
is available in your PATH
, you can create a
plugin workspace.
-
Change to your working directory:
cd ~/work
-
Call
pdk
as follows:`pdk generate workspace`
-
In the interactive prompt type
MyJenkins
as plugin name. For the rest of the options, which are all optional, choose defaults if available or provide your own values.
The generated workspace has the following 2 files:
Step 2: Define plugin spec and generate plugin
The pluginspec.yaml
has two sections: one for configuration and one
for procedures.
Update config section of YAML
To interact with Jenkins we need the following config values:
-
Jenkins endpoint
-
Username for authorization
-
Password for authorization
Fill in the configuration
section as follows.
configuration: # Shell to be used for checking connection shell: cb-perl parameters: - name: config type: entry label: Configuration Name required: true documentation: The name for the created configuration - name: desc type: entry label: Description required: false documentation: Description for the configuration - name: endpoint label: Endpoint type: entry required: true documentation: A Jenkins URL with port. - name: basic_credential type: credential label: Credential userNameLabel: Username required: true passwordLabel: Password - name: debugLevel # This parameter name "debugLevel" is supported by ECPDF Logger out of the box label: Debug Level type: select required: 0 value: 0 options: - name: Info value: 0 - name: Debug value: 1 - name: Trace value: 2
This tutorial assumes that the Jenkins environment is
protected using the Prevent Cross Site Request Forgery exploits
(CSRF) security. When the plugin makes a POST request, a CSRF
protection token should be sent as part of the HTTP request header.
Rename the auto-generated specification called credential
to
basic_credential
.
Update procedure section
Now implement a procedure called Get Last Build Number
with
interface specification as follows.
procedures: - name: Get Last Build Number description: This procedure gets last build number from provided Jenkins job. hasConfig: true # configuration field will be generated automatically parameters: - name: jenkinsJobName documentation: A name of the Jenkins job to get the latest build number. type: entry required: true label: Jenkins Job Name outputParameters: lastBuildNumber: A last build number for job. # Steps are not defined so we assume that this is one-step procedure with a single step named Deploy shell: cb-perl
Generate plugin
Execute the following command from the root level directory of the plugin workspace.
`pdk generate plugin`
After execution, it should look as follows:
With this step plugin generation is complete.
Review the auto-generated code
This section is provided to give a perspective on how the boiler plate generated code looks like and what it means.
The dsl/properties
folder has the following structure:
-
perl/core
-
perl/lib
Do not modify any file under the core folder. This folder has plugin-related internal code, which should not be edited by plugin developer. |
The only folder that could be modified is the perl/lib
folder.
Notice that dsl/properties/perl/lib/FlowPlugin/MyJenkins.pm
has been
auto-generated and contains the following code:
package FlowPlugin::MyJenkins; use strict; use warnings; use base qw/FlowPDF/; use FlowPDF::Log; # Feel free to use new libraries here, e.g. use File::Temp; # Service function that is being used to set some metadata for a plugin. sub pluginInfo { return { pluginName => '@PLUGIN_KEY@', pluginVersion => '@PLUGIN_VERSION@', configFields => ['config'], configLocations => ['ec_plugin_cfgs'], defaultConfigValues => {} }; } # Auto-generated method for the procedure Get Last Build Number/Get Last Build Number # Add your code into this method and it will be called when step runs sub getLastBuildNumber { my ($pluginObject, $runtimeParameters, $stepResult) = @_; my $context = $pluginObject->newContext(); logInfo("Current context is: ", $context->getRunContext()); my $params = $context->getStepParameters(); logInfo("Step parameters are: ", $params); my $configValues = $context->getConfigValues(); logInfo("Config values are: ", $configValues); $stepResult->setJobStepOutcome('warning'); $stepResult->setJobSummary("This is a job summary."); } ## === step ends === # Please do not remove the marker above, it is used to place new procedures into this file. 1;
Each plugin procedure has one or more steps. The auto-generated code for the step Get Last Build Number
is located in dsl/procedures/GetLastBuildNumber/steps/GetLastBuildNumber.pl
and looks like the following:
$[/myProject/perl/core/scripts/preamble.pl] use FlowPlugin::MyJenkins; # Auto generated code of plugin step # Go to dsl/properties/perl/lib/EC/Plugin/MyJenkins.pm and change the function "getLastBuildNumber" FlowPlugin::MyJenkins->runStep('Get Last Build Number', 'Get Last Build Number', 'getLastBuildNumber');
Step 3: Implement Additional logic
Add default config values
Edit the pluginInfo
function to set a default values for parameters. Notice that authScheme
is set to the value basic
.
sub pluginInfo { return { pluginName => '@PLUGIN_KEY@', pluginVersion => '@PLUGIN_VERSION@', configFields => ['config'], configLocations => ['ec_plugin_cfgs'], defaultConfigValues => { authScheme => 'basic' }, }; }
Add necessary library imports
Since Jenkins returns a JSON response code, add appropriate libraries that can be used to parse, write them to flow logs as necessary or throw exceptions.
package FlowPlugin::MyJenkins; use strict; use warnings; use base qw/FlowPDF/; use FlowPDF::Log; use FlowPDF::Helpers qw/bailOut/; use Data::Dumper; use JSON; use Try::Tiny;
Retrieve the Jenkins CSRF Token
Note that the CSRF Token is also called as the Jenkins crumb.
Implement a function that takes the endpoint returned by plugin configuration and appends an additional path to it.
sub getBaseUrl { my ($self, $url) = @_; if (!$url) { bailOut("URL is mandatory parameter"); } # retrieving runtime parameters my $runtimeParameters = $self->getContext()->getRuntimeParameters(); # endpoint is a field from configuration my $endpoint = $runtimeParameters->{endpoint}; # removing trailing slash. $endpoint =~ s|\/+$||gs; # appending url my $retval = $endpoint . '/' . $url; return $retval; }
For example if the endpoint is https://example.com/crumbUrl
, it would be https://example.com/crumbIssuer/api/json
in the snippet below.
my $crumbUrl = $self->getBaseUrl('crumbIssuer/api/json');
Implement crumb retrieval as follows.
sub addCrumbToRequest { my ($self, $request) = @_; # Creating base URL using previously implemented function my $crumbUrl = $self->getBaseUrl('crumbIssuer/api/json'); # Creating REST client object my $rest = $self->getContext()->newRESTClient(); # Creating crumb request object my $crumbRequest = $rest->newRequest(GET => $crumbUrl); # actual request. my $response = $rest->doRequest($crumbRequest); if ($response->code() > 399) { return $request; } my $decodedResponse; try { $decodedResponse = decode_json($response->decoded_content()); }; if ($decodedResponse->{crumb} && $decodedResponse->{crumbRequestField}) { $request->header($decodedResponse->{crumbRequestField} => $decodedResponse->{crumb}); } return $request; }
Note that in the above snippet the REST interaction with Jenkins is
handled by pdk
.
Retrieve Jenkins job last build number
Introduce a function that retrieves Jenkins job build number by first getting the CSRF token and then passing it to Jenkins by appending it to the Jenkins endpoint set up in the plugin configuration.
sub retrieveLastBuildNumberFromJob { my ($self, $jobName) = @_; if (!$jobName) { bailOut "Missing jobName for retrieveLastBuildNumberFromJob"; } # Retrieving base URL for Jenkins job. my $baseUrl = $self->getBaseUrl( sprintf('job/%s/api/json', $jobName) ); # getting rest client my $rest = $self->getContext()->newRESTClient(); # creating request object my $request = $rest->newRequest(GET => $baseUrl); # augmenting this request with crumbs $request = $self->addCrumbToRequest($request); # performing request: my $response = $rest->doRequest($request); # decoding request content: my $json = decode_json($response->decoded_content()); # finding and returning build number. if ($json->{lastBuild} && $json->{lastBuild}->{number}) { return $json->{lastBuild}->{number}; } return undef; }
Modify step code
The auto-generated function in step looks as follows:
sub getLastBuildNumber { my ($pluginObject, $runtimeParameters, $stepResult) = @_; my $context = $pluginObject->newContext(); logInfo("Current context is: ", $context->getRunContext()); my $params = $context->getStepParameters(); logInfo("Step parameters are: ", $params); my $configValues = $context->getConfigValues(); logInfo("Config values are: ", $configValues); $stepResult->setJobStepOutcome('warning'); $stepResult->setJobSummary("This is a job summary."); }
Modify the logic as follows using the function you built:
sub getLastBuildNumber { my ($pluginObject, $runtimeParameters, $stepResult) = @_; my $buildNumber = $pluginObject->retrieveLastBuildNumberFromJob($runtimeParameters->{jenkinsJobName}); unless ($buildNumber) { bailOut("No buildNumber for Jenkins job."); } }
Set Output Parameters
An output parameter is set as part of the $stepResult
object.
$stepResult->setOutputParameter(lastBuildNumber => $buildNumber);
Add the above code to the step function as follows:
sub getLastBuildNumber { my ($pluginObject, $runtimeParameters, $stepResult) = @_; my $buildNumber = $pluginObject->retrieveLastBuildNumberFromJob($runtimeParameters->{jenkinsJobName}); unless ($buildNumber) { bailOut("No buildNumber for Jenkins job."); } $stepResult->setOutputParameter(lastBuildNumber => $buildNumber); }
Set pipeline, job, and jobstep summary
Set all the three summaries - pipeline summary, job summary, and jobstep summary.
sub getLastBuildNumber { my ($pluginObject, $runtimeParameters, $stepResult) = @_; my $buildNumber = $pluginObject->retrieveLastBuildNumberFromJob($runtimeParameters->{jenkinsJobName}); unless ($buildNumber) { bailOut("No buildNumber for Jenkins job."); } logInfo("Last Build Number for $runtimeParameters->{jenkinsJobName} is $buildNumber"); $stepResult->setOutputParameter(lastBuildNumber => $buildNumber); $stepResult->setPipelineSummary("Build Number for $runtimeParameters->{jenkinsJobName}", $buildNumber); $stepResult->setJobSummary("Build Number for $runtimeParameters->{jenkinsJobName}: $buildNumber"); $stepResult->setJobStepSummary("Build Number for $runtimeParameters->{jenkinsJobName}: $buildNumber"); }
Review MyJenkins.pm
This is a summary of how MyJenkins.pm
after you make the changes.
package FlowPlugin::MyJenkins; use strict; use warnings; use base qw/FlowPDF/; use FlowPDF::Log; use FlowPDF::Helpers qw/bailOut/; use Data::Dumper; use JSON; use Try::Tiny; # Feel free to use new libraries here, e.g. use File::Temp; # Service function that is being used to set some metadata for a plugin. sub pluginInfo { return { pluginName => '@PLUGIN_KEY@', pluginVersion => '@PLUGIN_VERSION@', configFields => ['config'], configLocations => ['ec_plugin_cfgs'], defaultConfigValues => { authScheme => 'basic' }, }; } # Auto-generated method for the procedure Get Last Build Number/Get Last Build Number # Add your code into this method and it will be called when step runs sub getLastBuildNumber { my ($pluginObject, $runtimeParameters, $stepResult) = @_; my $buildNumber = $pluginObject->retrieveLastBuildNumberFromJob($runtimeParameters->{jenkinsJobName}); unless ($buildNumber) { bailOut("No buildNumber for Jenkins job."); } $stepResult->setOutputParameter(lastBuildNumber => $buildNumber); $stepResult->setPipelineSummary("Build Number for $runtimeParameters->{jenkinsJobName}", $buildNumber); $stepResult->setJobSummary("Build Number for $runtimeParameters->{jenkinsJobName}: $buildNumber"); $stepResult->setJobStepSummary("Build Number for $runtimeParameters->{jenkinsJobName}: $buildNumber"); } ## === step ends === # Please do not remove the marker above, it is used to place new procedures into this file. sub retrieveLastBuildNumberFromJob { my ($self, $jobName) = @_; if (!$jobName) { bailOut "Missing jobName for retrieveLastBuildNumberFromJob"; } # Retrieving base URL for Jenkins job. my $baseUrl = $self->getBaseUrl( sprintf('job/%s/api/json', $jobName) ); # getting rest client my $rest = $self->getContext()->newRESTClient(); # creating request object my $request = $rest->newRequest(GET => $baseUrl); # augmenting this request with crumbs $request = $self->addCrumbToRequest($request); # performing request: my $response = $rest->doRequest($request); # decoding request content: my $json = decode_json($response->decoded_content()); # finding and returning build number. if ($json->{lastBuild} && $json->{lastBuild}->{number}) { return $json->{lastBuild}->{number}; } return undef; } sub getBaseUrl { my ($self, $url) = @_; if (!$url) { bailOut("URL is mandatory parameter"); } # retrieving runtime parameters my $runtimeParameters = $self->getContext()->getRuntimeParameters(); # endpoint is a field from configuration my $endpoint = $runtimeParameters->{endpoint}; # removing trailing slash. $endpoint =~ s|\/+$||gs; # appending url my $retval = $endpoint . '/' . $url; return $retval; } sub addCrumbToRequest { my ($self, $request) = @_; # Creating base URL using previously implemented function my $crumbUrl = $self->getBaseUrl('crumbIssuer/api/json'); # Creating REST client object my $rest = $self->getContext()->newRESTClient(); # Creating crumb request object my $crumbRequest = $rest->newRequest(GET => $crumbUrl); # actual request. my $response = $rest->doRequest($crumbRequest); if ($response->code() > 399) { return $request; } my $decodedResponse; try { $decodedResponse = decode_json($response->decoded_content()); }; if ($decodedResponse->{crumb} && $decodedResponse->{crumbRequestField}) { $request->header($decodedResponse->{crumbRequestField} => $decodedResponse->{crumb}); } return $request; } 1;
Step 4: Build, install, and test
Build the plugin from the root directory of the MyJenkins
plugin
workspace. :
pdk build
It should look something like this:
Congratulations! You have now created a functional plugin.
Install and promote the plugin
Go to your CloudBees CD/RO instance, login and navigate to the Plugin Manager page which lists all plugins.
Select the Install from File/URL tab, then Choose File, select your file, and then Upload:
You are redirected to the Plugin Manager page. Find your plugin MyJenkins
and select Promote. After promotion the page is automatically refreshed.
Create a Pipeline and Configure the plugin
After your plugin is installed, navigate to the Pipelines
page and select New in the top right corner of the page.
Click Create New, enter MyJenkinsTestPipeline
, choose your project,
click Ok.
Now you’re in the pipeline editor. Click Add + on the task explorer, enter Get Last Build Number
, click Select Task Type, scroll down the plugin list and click on your MyPlugin
procedure below the plugin name:
Now, select your procedure name and click Define. You see:
Click on input parameters to confirm that this procedure sees two parameters as per the YAML
spec. Enter Jenkins Job Name as HelloWorld
, click on triangle and then New Configuration.
Enter values relevant for your configuration now. In this example, Jenkins instance is located at 10.200.1.171:8080
. Username
and password
are admin
/changeme
:
Save configuration and select it on the Input Parameters form.
This concludes the plugin as well as the pipeline setup.
Running the pipeline
In the flow pipeline page select Run on the pipeline you have created. In the popup dialog, click New Run and in the modal dialog that shows up, click Run again. You see the following:
Select Summary:
Select 1. Get Last Build Number:
Select the Parameters tab:
Click back on the Steps link to open the log link:
We now have a minimal working plugin for Jenkins.