Introductory Jenkins

8 minute read

Introduction

This tutorial provides instructions for the 4 step process to create a functional plugin called SampleJenkins which 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.

  1. An active CloudBees CD instance.

  2. A Machine with Docker installed which is registered as a pingable resource in CloudBees CD.

  3. Internet connection.

  4. pdk installed and set up.

Install and setup Jenkins

Pull the Docker Jenkins image that already has electricflow agent installed from Docker hub.

docker pull electricflow/jenkins

Important: Preface sudo with Docker commands if 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 with 8080 port. 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 SampleJenkins as plugin name. For the rest of the options (except the plugin language) which are all optional, choose defaults if available or provide your own values. For the plugin language you should use 'groovy'

The generated workspace would have the following 2 files:

Step 2: Define plugin spec and generate plugin

The pluginspec.yaml has 2 sections one for configuration and the other for procedures.

Update configuration section of YAML

To interact with Jenkins we need the following config values:

  1. Jenkins endpoint

  2. Username for authorization.

  3. Password for authorization.

Fill up the config section as follows.

# Plugin configuration description
configuration:
  # A set of fields will be added to process debug level in the configuration
  hasDebugLevel: true
  parameters:
  -
    name: config
    documentation: The name for the created configuration
    required: true
    type: entry
    label: Configuration Name
  -
    name: desc
    documentation: Description for the configuration
    required: false
    type: entry
    label: Description
  -
    name: endpoint
    documentation: Third-party endpoint to connect to (Jenkins server URL), e.g. 'http://jenkins:8080/'
    required: true
    type: entry
    label: Endpoint
  -
    name: basic_credential
    documentation: Jenkins credentials
    required: true
    type: credential
    label: Credential

In this tutorial we will assume that the Jenkins environment is protected using the "Prevent Cross Site Request Forgery exploits" security. This means 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 procedures section

We will implement a procedure called "GetJenkinsJob" with interface specification as follows.

procedures:
-
  name: 'GetJenkinsJob'
  description: 'Saves details for Jenkins job to properties'
  # configuration field will be generated automatically
  hasConfig: true
  shell: 'ec-groovy'
  parameters:
  -
    name: jobName
    documentation: Name of the Jenkins job.
    required: true
    type: entry
    label: Jenkins Job
  outputParameters:
    lastBuildNum: 'Number of the last job build'

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:

  • groovy/lib

  • groovy/scripts

  • perl/core

Important: Do not modify any file under core folders. Core folders has plugin-related internal code, which should not be edited by plugin developer.

The only folder that could be modified is the groovy/lib folder.

Notice that dsl/properties/groovy/lib/SampleJenkins.groovy has been auto-generated and contains the following code.

import com.cloudbees.flowpdf.*

/**
* SampleJenkins
*/
class SampleJenkins extends FlowPlugin {

    @Override
    Map<String, Object> pluginInfo() {
        return [
                pluginName     : '@PLUGIN_KEY@',
                pluginVersion  : '@PLUGIN_VERSION@',
                configFields   : ['config'],
                configLocations: ['ec_plugin_cfgs'],
                defaultConfigValues: [:]
        ]
    }

/**
    * getJenkinsJob - GetJenkinsJob/GetJenkinsJob
    * Add your code into this method and it will be called when the step runs
    * @param config (required: true)
    * @param jobName (required: true)

    */
    def getJenkinsJob(StepParameters p, StepResult sr) {

        /* Log is automatically available from the parent class */
        log.info(
          "getJenkinsJob was invoked with StepParameters",
          /* runtimeParameters contains both configuration and procedure parameters */
          p.toString()
        )

        Context context = getContext()

        // Setting job step summary to the config name
        sr.setJobStepSummary(p.getParameter('config').getValue() ?: 'null')

        sr.setReportUrl("Sample Report", 'https://cloudbees.com')
        sr.apply()
        log.info("step GetJenkinsJob has been finished")
    }

// === step ends ===

}

Each plugin procedure has one or more steps. The auto-generated code for the step “GetJenkinsJob” is located in dsl/procedures/GetJenkinsJob/steps/GetJenkinsJob.groovy and contains following:

$[/myProject/groovy/scripts/preamble.groovy.ignore]

SampleJenkins plugin = new SampleJenkins()
plugin.runStep('GetJenkinsJob', 'GetJenkinsJob', 'getJenkinsJob')

Step 3: Implement additional logic

Add default config values:

Edit pluginInfo function to set default values for parameters. Notice that authScheme is set to the value basic.

@Override
Map<String, Object> pluginInfo() {
    return [
            pluginName     : '@PLUGIN_KEY@',
            pluginVersion  : '@PLUGIN_VERSION@',
            configFields   : ['config'],
            configLocations: ['ec_plugin_cfgs'],
            defaultConfigValues: [ authScheme : 'basic' ]
    ]
}

Add necessary library imports

We will use a framework package named com.cloudbees.flowpdf.client to perform HTTP request. Add import to the head of a file:

import com.cloudbees.flowpdf.*
import com.cloudbees.flowpdf.client.*

Retrieving the Jenkins CSRF token

We are supposing, that server is protected with "Prevent Cross Site Request Forgery exploits" so an additional handling of the requests is necessary. Note that the CSRF Token is also called the Jenkins crumb. Copy and paste the following function to the end of SampleJenkins.groovy (before the last curly brace). Don’t mind the comments, it’s ignored by the compiler.

HTTPRequest addBreadcrumbsToRequest(HTTPRequest httpRequest) {
    // Checking cached values
    if (!this.jenkinsBreadcrumbField || !this.jenkinsBreadcrumbValue) {

        // Requesting new crumb
        REST client = getContext().newRESTClient()
        HTTPRequest breadCrumbsRequest = client.newRequest([method: 'GET', path: '/crumbIssuer/api/json'])
        def breadCrumbResponse = client.doRequest(breadCrumbsRequest)

        // Saving
        this.jenkinsBreadcrumbField = breadCrumbResponse['crumbRequestField'] as String
        this.jenkinsBreadcrumbValue = breadCrumbResponse['crumb'] as String
    }

    httpRequest.headers.put(this.jenkinsBreadcrumbField, this.jenkinsBreadcrumbValue)
    return httpRequest
}

Also, add this two fields to SampleJenkins.groovy. Usual way for Java is to have field declarations right under the class definition.

String jenkinsBreadcrumbField
String jenkinsBreadcrumbValue

Modify getJenkinsJob step code

The auto-generated function in SampleJenkins.groovy for the step looks as follows:

/**
    * getJenkinsJob - GetJenkinsJob/GetJenkinsJob
    * Add your code into this method and it will be called when the step runs
    * @param config (required: true)
    * @param jobName (required: true)

    */
    def getJenkinsJob(StepParameters p, StepResult sr) {

        /* Log is automatically available from the parent class */
        log.info(
          "getJenkinsJob was invoked with StepParameters",
          /* runtimeParameters contains both configuration and procedure parameters */
          p.toString()
        )

        Context context = getContext()

        // Setting job step summary to the config name
        sr.setJobStepSummary(p.getParameter('config').getValue() ?: 'null')

        sr.setReportUrl("Sample Report", 'https://cloudbees.com')
        sr.apply()
        log.info("step GetJenkinsJob has been finished")
    }

// === step ends ===

The code snippet below contains a logic to perform the request and save the properties. Replace the step code in SampleJenkins.groovy with following:

/**
* getJenkinsJob - GetJenkinsJob/GetJenkinsJob
* Add your code into this method and it will be called when the step runs
* @param config (required: true)
* @param jobName (required: true)
*/
def getJenkinsJob(StepParameters p, StepResult sr) {

    /* Log is automatically available from the parent class */
    log.info(
            "getJenkinsJob was invoked with StepParameters",
            /* runtimeParameters contains both configuration and procedure parameters */
            p.toString()
    )

    String jobName = p.getRequiredParameter('jobName').getValue()

    Context context = getContext()
    REST rest = context.newRESTClient()

    HTTPRequest request = rest.newRequest([method: 'GET', path: sprintf("/job/%s/api/json", jobName)])
    addBreadcrumbsToRequest(request)

    def jobResponse = rest.doRequest(request)
    log.debug("JSON", jobResponse.dump())

    if (jobResponse['lastBuild'] == null) {
        context.bailOut("No build was found for job '${jobName}'")
    }

    String lastBuildNumber = jobResponse['lastBuild']['number']

    sr.setOutcomeProperty('/myCall/lastBuildNumber', lastBuildNumber)

    // Showing link to a build
    String lastBuildUrl = jobResponse['lastBuild']['url']
    sr.setReportUrl("Last Build", lastBuildUrl, "Jenkins Build ${jobName}#${lastBuildNumber}")

    sr.apply()
    log.info("step GetJenkinsJob has been finished")
}

// === step ends ===

Set output parameters

An output parameter is set as part of the StepResult object. Add the following code inside of the step function in SampleJenkins.groovy before the sr.apply() call.

sr.setOutputParameter('lastBuildNum', lastBuildNumber)

Set pipeline, job and jobStep summary

Set all the 3 summaries - pipeline summary, job summary and jobStep summary. Add the following code inside of the step function in SampleJenkins.groovy before the sr.apply() call.

sr.setPipelineSummary("Last build number is", lastBuildNumber)
sr.setJobSummary("Build properties saved.")
sr.setJobStepSummary("Saved properties for build number ${lastBuildNumber}")

Review SampleJenkins.groovy

This is a summary of how SampleJenkins.groovy looks like after you make the changes.

import com.cloudbees.flowpdf.*
import com.cloudbees.flowpdf.client.*

/**
* SampleJenkins
*/
class SampleJenkins extends FlowPlugin {

    String jenkinsBreadcrumbField
    String jenkinsBreadcrumbValue

    @Override
    Map<String, Object> pluginInfo() {
        return [
                pluginName     : '@PLUGIN_KEY@',
                pluginVersion  : '@PLUGIN_VERSION@',
                configFields   : ['config'],
                configLocations: ['ec_plugin_cfgs'],
                defaultConfigValues: [ authScheme : 'basic' ]
        ]
    }

    /**
     * getJenkinsJob - GetJenkinsJob/GetJenkinsJob
     * Add your code into this method and it will be called when the step runs
     * @param config (required: true)
     * @param jobName (required: true)
     */
    def getJenkinsJob(StepParameters p, StepResult sr) {

        /* Log is automatically available from the parent class */
        log.info(
                "getJenkinsJob was invoked with StepParameters",
                /* runtimeParameters contains both configuration and procedure parameters */
                p.toString()
        )

        String jobName = p.getRequiredParameter('jobName').getValue()

        Context context = getContext()
        REST rest = context.newRESTClient()

        HTTPRequest request = rest.newRequest([method: 'GET', path: sprintf("/job/%s/api/json", jobName)])
        addBreadcrumbsToRequest(request)

        def jobResponse = rest.doRequest(request)
        log.debug("JSON", jobResponse.dump())

        if (jobResponse['lastBuild'] == null) {
            context.bailOut("No build was found for job '${jobName}'")
        }

        String lastBuildNumber = jobResponse['lastBuild']['number']

        sr.setOutcomeProperty('/myCall/lastBuildNumber', lastBuildNumber)

        // Showing link to a build
        String lastBuildUrl = jobResponse['lastBuild']['url']
        sr.setReportUrl("Last Build", lastBuildUrl, "Jenkins Build ${jobName}#${lastBuildNumber}")
        sr.setOutputParameter('lastBuildNum', lastBuildNumber)
        sr.setPipelineSummary("Last build number is", lastBuildNumber)
        sr.setJobSummary("Build properties saved.")
        sr.setJobStepSummary("Saved properties for build number ${lastBuildNumber}")
        sr.apply()
        log.info("step GetJenkinsJob has been finished")
    }

    // === step ends ===

    HTTPRequest addBreadcrumbsToRequest(HTTPRequest httpRequest) {
        // Checking cached values
        if (!this.jenkinsBreadcrumbField || !this.jenkinsBreadcrumbValue) {

            // Requesting new crumb
            REST client = getContext().newRESTClient()
            HTTPRequest breadCrumbsRequest = client.newRequest([method: 'GET', path: '/crumbIssuer/api/json'])
            def breadCrumbResponse = client.doRequest(breadCrumbsRequest)

            // Saving
            this.jenkinsBreadcrumbField = breadCrumbResponse['crumbRequestField'] as String
            this.jenkinsBreadcrumbValue = breadCrumbResponse['crumb'] as String
        }

        httpRequest.headers.put(this.jenkinsBreadcrumbField, this.jenkinsBreadcrumbValue)
        return httpRequest
    }
}

Step 4: Build, install, and test

Build the plugin from root directory of the SampleJenkins plugin workspace. :

flowpdk 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.

Click on "Install from File/URL" tab, click Choose File, select your build/SampleJenkins.zip file and click upload:

You will be redirected to the plugin manager page. Find your plugin SampleJenkins, and click "Promote" link.

After promotion the page would be automatically refreshed.

Create a pipeline and configure the plugin

After your plugin is installed, navigate to the Pipelines page and click "New" in the top right corner of the web page.

Click "Create New", enter "SampleJenkins Showcase", choose your project, click "Ok".

Now you’re in your pipeline editor. Click "Add +" on the task explorer, enter "GetJenkinsJob", click "Select Task Type", scroll down the plugin list and click on your GetJenkinsJob procedure below the plugin name:

Now click on your procedure name and "Define" link. You will see:

Click on input parameters to confirm that this procedure sees all the parameters as per the YAML spec.

Enter the job name. If you have Jenkins from electricflow/jenkins:latest image, you can use 'AlwaysOk' job.

Click on the triangle at the right of "Configuration Name" parameter input field and choose the "New Configuration" item.

Enter values relevant for your configuration now.

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 click "Run" on the pipeline you have created. In the Popup menu, click "New Run" and in the modal dialog that shows up, click "Run" again. You will see the following:

Click on the summary link:

Click on the "1. GetJenkinsJob". You can see that the special step 'flowpdk-setup' was performed to transfer the dependencies.

Click on the "Parameters" tab to see the output parameter:

Click back on the "Steps" link and open the GetJenkinsJob step 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-groovy.