Jenkins Shared Libraries Jenkins

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.

Groovy Component

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!"
}

Limitations

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.

Shared Library

To make our functionality available in Jenkins pipelines, we need to setup Jenkins to recognize our repository as a shared library via ``Manage Jenkins`:

Manage Jenkins

Then in the Global Pipeline libraries, we add our repository. The name will be the name used to import the library.

Pipeline Libraries

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.

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

Conclusion

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!

Designed, built and maintained by Kimserey Lam.