Dec 27th, 2019 - written by Kimserey with .
When we have multiple pipelines in Jenkins, it becomes necessary to share code between them. For example, we might want to have a stage that we want to setup for a different environment therefore the only change needed would be its parameters. Today we will see how we can provide reusable functionalities in Jenkins pipeline across a single or multiple pipelines.
For this post I will be using the local setup Jenkins + Git repository I have explained in my previous blog post therefore the repository URLs are all local.
In this example we will be providing a reusable component which provides a log functionality. This is how it will look like when using it in a pipeline:
1
2
3
4
log {
type = "warning"
message = "test warning closure!"
}
What we want to achieve is a DSL implementation with a single log {}
element.
This functionality will be available across all pipelines therefore we will be able to use it everywhere we need it.
This is actually how Jenkins pipeline is built itself with node {}
, stage {}
, etc..
We start by setting up a git repository with the following structure:
1
2
3
/jenkins-shared
- /vars
- logs.groovy
We then create the content of the logs.groovy
file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def log_info(message) {
echo "INFO: ${message}"
}
def log_warning(message) {
echo "WARNING: ${message}"
}
def log_error(message) {
echo "ERROR: ${message}"
}
def call(body) {
def config = [:]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = config
body()
switch (config.type) {
case 'info':
log_info config.message
break
case 'warning':
log_warning config.message
break
case 'error':
log_error config.message
break
default:
error "Unhandled type."
}
}
The function call
is a special function in groovy that can be called without going .call ()
. We use it in order to be able to call logs {}
without the need of going logs.call {}
. The body
parameter is expected to be a closure which will set values in the closure to the config. This allow the following notation to set the values on the map config.
1
2
3
4
{
type = "warning"
message = "test warning closure!"
}
Line by line, we create an empty map which will hold the configuration, then we set the strategy of the closure to resolve the delegate first and set the delegate to be the configuration. This allows the closure to sets values on the config
instead of setting the current class. Lastly we execute the closure body()
.
1
2
3
4
def config = [:]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = config
body()
To understand better what Closure.DELEGATE_FIRST
mean, take a look at the following:
1
2
3
4
5
6
7
8
9
10
11
def config = [:]
def body = {
type = "warning"
message = "test warning closure!"
}
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = config
body()
println "config:" + config.message
println "this:" + this.message
If we execute this on an online groovy compiler, we will see that when we set Closure.DELEGATE_FIRST
, the message
is present on the config, while when we remove it, the message
is present on the current class this
instead.
We now have a class that can be used on its own with the notation we wanted:
1
2
3
4
log {
type = "warning"
message = "test warning closure!"
}
There are limitations to be aware of, the first one being that if you are using parameters for your pipeline, the params
will not be available within the closure therefore the following will not work:
1
2
3
4
log {
type = "warning"
message = params.MESSAGE // <= this will throw null exception
}
Another limitation is that it’s not possible to use the same name as the property itself as it will result in null. For example the following will not work:
1
2
3
4
5
6
def message = "My Message"
log {
type = "warning"
message = message // <= this will be null
}
Now let’s see how we can make it available in Jenkins pipeline.
To make our functionality available in Jenkins pipelines, we need to setup Jenkins to recognize our repository as a shared library via ``Manage Jenkins`:
Then in the Global Pipeline libraries
, we add our repository. The name will be the name used to import the library.
In this example, my repository is local therefore the URI is a file://
URI. Once this is setup, we will now be able to use our log
functionality in our pipeline.
We can now use the library by importing it:
1
@Library('my-shared-library') _
We then can use it in our pipeline:
1
2
3
4
5
6
7
8
@Library('my-shared-library') _
stage ("Shared Library Test") {
log {
type = "warning"
message = "This is a log message!"
}
}
We are now able to import my-shared-library
and use log
in any pipeline we created.
Today we saw how we could create a reusable functionality to be used between multiple Jenkins pipeline. We started by looking at how we could create a DSL by leveraging groovy call
function and closure
. Then we moved on to setup shared libraries on Jenkins portal and finally we saw how we could import the library and use it. Hope you liked this post, see you on the next one!