Things to consider when using Jenkins Groovy

Having learned Groovy and pipeline scripting at the same time from ground up - without having any decent background in any of the underlying "modern" principles or languages - has been an interesting experience. This section is dedicated to listing and pointing out those pitfalls and learning experiences that we went through, to help people saving the same struggle and speed up their getting productive. And the same time it is supposed as a "reference" to explain why some things were done things way they are done in the showcased examples.

Any hints and suggestions to improve or circumnavigate the pitfalls in smarter ways are highly appreciated.

Using steps in classes

Using pipeline steps, i.e. almost every execution of a plugin, within classes that are not the main script, require the script steps to be passed to the class and executing the corresponding methods of this steps class. Failure to do so will result in

groovy.lang.MissingPropertyException

for the respective method. This starts with a simple prinln. Almost all classes in use in our examples perform one or the other steps method. Therefore, a common scheme is to declare the class and its constructor as follows:

class IspwHelper implements Serializable
{
    def steps
...
    IspwHelper(steps ...) 
    {

        this.steps              = steps
...
    }

Instantiation of these classes will happen like this

    ispwHelper  = new   IspwHelper(steps ...)

or

    ispwHelper  = new   IspwHelper(this.steps ...)

For more and detailed information refer to the Jenkins documentation

Non serializable classes

As seen above and as discussed in the Jenkins documentation classes must implement the Serializable interface. This is necessary so that pipeline jobs remain 'restartable', i.e. when the Jenkins node fails during execution of a job, the job is able to resume work from the place were it got interrupted. For this to be possible, Jenkins needs to be able to store the state of all instantiated objects.

This bears some implications when it comes to the use of third party classes that are not serializable. Trying to re-use objects of such classes will likely result in

java.io.NotSerializableException

Example are the JsonSlurper class or the responseBody class. The latter is being returned by a native httpRequest, the former is used to digest the JSON responseBody from an httpRequest. The simplest way to use these classes without running into java.io.NotSerializableException we found, is to de-reference the objects as soon as possible in the code. That way there is no need to store their state across method boundaries:

    def ArrayList getAssigmentList(String cesToken, String level)
    {
        def returnList  = []

        def taskIds     = getSetTaskIdList(cesToken, level)

        def response = steps.httpRequest(
            url:                        "${ispwUrl}/ispw/${ispwRuntime}/releases/${ispwRelease}/tasks",
            consoleLogResponseBody:     false, 
            customHeaders:              [[
                                        maskValue:  true,
                                        name:       'authorization',
                                        value:      "${cesToken}"
                                        ]]
            )

        def jsonSlurper = new JsonSlurper()
        def resp        = jsonSlurper.parseText(response.getContent())
        response        = null
        jsonSlurper     = null
        ...

In the example response recieves the result of the httpRequest, jsonSlurper get instantiated and resp receives the content of response as list. Once the two objects are not needed they get de-referenced by

        response        = null
        jsonSlurper     = null

Using methods in class constructors

Simply put, class constructors in Groovy cannot use methods, be it own internal methods, or instantiating other classes and using their methods. Anything other than 'simple' variable initialization will result in

hudson.remoting.ProxyException: com.cloudbees.groovy.cps.impl.CpsCallableInvocation

Therefore, many of the classes in use here, have an initialize method that performs any additional work necessary after the constructor executed before any of the other methods can be used.

Plugins setting variables

There are certain plugins that within their execution set variables that are exposed to the rest of the script. Two of these plugins being used throughout the examples are Config File Provider configFileProvider and Credentials Binding withCredentials.

The first one allows accessing a file that has been defined using the Config File Provider plugin like the mailList.config file. You pass the fileID and retrieve a variable that contains the (temporary) path to the file. In the following snippet variable mailListFilePath will contain that path.

    configFileProvider(
        [
            configFile(
                fileId: 'MailList',
                variable: 'mailListFilePath'
            )
        ]
    )
    {
        File mailConfigFile = new File(mailListFilePath)

        if(!mailConfigFile.exists())
        {
            steps.error "File - ${mailListFilePath} - not found! \n Aborting Pipeline"
        }

        mailListlines = mailConfigFile.readLines()
    }

The second one allows retrieving the information stored in a Jenkins credentials token, in cases when certain plugins require the content in clear text. This is the case for example when using native httpRequest to interact with REST APIs. Some of the example code makes use of the httpRequest and the ISPW API requires the CES credentials to be passed. Other than the ISPW operations plugin the httpRequest cannot use the Jenkins secret text token storing the CES token. Using the withCredentials, one can use the Jenkins in credentialsID token to retrieve the 'clear text' CES token during runtime (stored in variable cesToken in the example below), without having to expose the token in the code:

    withCredentials(
        [string(credentialsId: "${CES_Token}", variable: 'cesToken')]
    ) 
    {
        response1 = steps.httpRequest(
            url:                    "${ISPW_URL}/ispw/${ISPW_Runtime}/sets/${ISPW_Container}/tasks",
            httpMode:               'GET',
            consoleLogResponseBody: false,
            customHeaders:          [[maskValue: true, name: 'authorization', value: "${cesToken}"]]
        )
    }

Unfortunately, while this works perfectly fine in the main script, trying to use such plugins in classes 'external' to the script class will likely result in exceptions like the following:

groovy.lang.MissingPropertyException: No such property: mailConfigPath for class: com.compuware.devops.util.PipelineConfig

This seems to be Groovy specific and the only work around so far seems to be to execute these plugins in the main script. That is why the mailList.config down not get read in the PipelineConfig class as one would expect, but in the main script.