Introductory

9 minute read

Introduction

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

  1. Create plugin workspace.

  2. Define plugin spec and generate plugin.

  3. Implement additional logic.

  4. Build plugin and test.

Prerequisites

These are assumptions for this tutorial.

  • An active CloudBees CD instance.

  • A machine with Docker installed that is registered as a pingable resource in CloudBees CD.

  • Internet connection.

  • The pdk tool is installed and setup.

Install and setup Jenkins

Pull the Docker Jenkins image that already has CloudBees CD 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.

  1. Change to your working directory:

    cd ~/work
  2. Call pdk as follows:

    `pdk generate workspace`
  3. 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: ec-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: ec-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 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.

Summary

The following diagram is provided as a reference to enunciate the flow of control using flowpdf-perl.