Things to consider when using Jenkins Groovy
Having learned Groovy and pipeline scripting at the same time from the 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 avoid the same struggles and speed up their productivity. At the same time it is a reference to help you understand why some things were done in a certain way 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 execute the corresponding methods of the steps
class. Failure to do so will result in a
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 (opens new window)
Non serializable classes
As seen above and as discussed in the Jenkins documentation (opens new window) 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 a
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 we have found, to use these classes without running into `java.io.NotSerializableException , is to de-reference the objects in the code as soon as possible. That way there is no need to store their state across method boundaries, like this:
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
receives the result of the httpRequest
, jsonSlurper
get instantiated and resp
receives the content of response
as a 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 the following:
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
(opens new window) and Credentials Binding withCredentials
(opens new window).
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. 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 those 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 that stores 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 (opens new window) and the only work around so far seems to be to execute these plugins in the main script. That is why the mailList.config
may not get read in the PipelineConfig
class as one would expect, but rather in the main script.