How to extract sections of Jenkins pipeline script into classes? - jenkins

I want to refactor my Jenkins pipeline script into classes for readability and reuse.
The problem is i get exceptions when doing so.
Let's look at a simple example:
When i run
echo currentBuild.toString()
everything is fine
But when i extract it into a class as so:
class MyClass implements Serializable {
def runBuild() {
echo currentBuild.toString()
}
}
new MyClass().runBuild()
i get an exception:
Started by user admin
Replayed #196
[Pipeline] End of Pipeline
groovy.lang.MissingPropertyException: No such property: currentBuild for class: MyClass
What is the proper way of extracting pipeline code in to classes?

You are on the right way, but the problem is that you didn't pass the script object to the instance of your class and was trying to call method which is not defined in the class that you have created.
Here is one way to solve this:
// Jenkins file or pipeline scripts editor in your job
new MyClass(this).runBuild()
// Class declaration
class MyClass implements Serializable {
def script
MyClass(def script) {
this.script=script
}
def runBuild() {
script.echo script.currentBuild.toString()
}
}

your code missing declare class field script
class MyClass implements Serializable {
def script
MyClass(def script) {
this.script=script
}
def runBuild() {
script.echo script.currentBuild.toString()
}
}
this code should be ok #bram

Related

Executing shell commands from inside Pipeline Shared Library

I'm writing a shared library that will get used in Pipelines.
class Deployer implements Serializable {
def steps
Deployer(steps) {
this.steps = steps
}
def deploy(env) {
// convert environment from steps to list
def process = "ls -l".execute(envlist, null)
process.consumeProcessOutput(output, error)
process.waitFor()
println output
println error
}
}
In the Jenkinsfile, I import the library, call the class and execute the deploy function inside a script section:
stage('mystep') {
steps {
script {
def deployer = com.mypackage.HelmDeployer("test")
deployer.deploy()
}
}
}
However, no output or errors are printed on the Console log.
Is it possible to execute stuff inside a shared library class? If so, how, and what am I doing wrong?
Yes, it is possible but not really an obvious solution. Every call that is usually done in the Jenkinsfile but was moved to the shared-library needs to reference the steps object you passed.
You can also reference the Jenkins environment by calling steps.env.
I will give you a short example:
class Deployer implements Serializable {
def steps
Deployer(steps) {
this.steps = steps
}
def callMe() {
// Always call the steps object
steps.echo("Test")
steps.echo("${steps.env.BRANCH_NAME}")
steps.sh("ls -al")
// Your command could look something like this:
// def process = steps.sh(script: "ls -l", returnStdout: true).execute(steps.env, null)
...
}
}
You also have to import the object of the shared library and create an instance of it. Define the following outside of your Pipeline.
import com.mypackage.Deployer // path is relative to your src/ folder of the shared library
def deployer = new Deployer(this) // 'this' references to the step object of the Jenkins
Then you can call it in your pipeline as the following:
... script { deployer.test() } ...

Loading a shared library implicitly

I would like to integrate a Global library into my build flow. I have written a basic function
srv/core/jenkins/Checks.groovy:
package core.jenkins
class Checks implements Serializable {
def script
Checks(script) {
this.script = script
}
def fileExists(){
script.echo "File exists in the repo."
}
}
And it is exposed as a global var
vars/fileExisits.groovy:
def call() {
new core.jenkins.Checks(this).fileExists()
}
While configuring the Global Shared Library settings in Jenkins, I have the following settings:
Now in my jenkinsfile, Im doing something like this:
pipeline {
agent { label 'master' }
stages {
stage('Check for md files'){
steps {
sh 'echo hello'
script {
checks.fileExists()
}
}
}
}
}
This always gives the error
groovy.lang.MissingPropertyException: No such property: checks for class: groovy.lang.Binding
at groovy.lang.Binding.getVariable(Binding.java:63)
at
For it to work, I have to add the lines to the top of my Jenkinsfile
import core.jenkins.Checks
def checks = new Checks(this)
Is there a way for me to invoke the function fileExists from a library without having to add the above 2 lines always ?
Just replace:
checks.fileExists()
with:
fileExists()
All Groovy scripts that implements def call() methods and are stored in the vars/ folder can be triggered by their script file name. Alternatively, if you would like to keep checks.fileExists() syntax, then you need to create vars/checks.groovy script file and implement def fileExists() method inside of it.

How can I access global variables (currentBuild, env, ...) from within a Shared Library?

I'd like to access a global variable like currentBuild, env, etc. from within a shared Groovy library.
Example 1 (works):
// vars/customStep.groovy
def call() {
echo env.myParameter
}
Example 2 (doesn't work):
// vars/customStep.groovy
class customStep implements Serializable {
def call() {
echo env.myParameter
}
}
Example 3 (doesn't work):
// src/com/acme/Lib.groovy
package com.acme
class Lib {
def someMethod() {
echo env.myParameter
}
}
I'd like to be able to access the variables in either case. How can I do this?
If you use the env variable from inside a class definition, Groovy will try to access a class variable env, rather than a global variable. I think you need to create a constructor and pass the env variable into it. For example:
package com.acme
class Lib {
def env
}
and in your pipeline use:
def library = Lib(env: env)
I take groovy constructor syntax from here

Commands fail when moving to them custom class in jenkinsfile

I'm setting up a new build. Running a simple shell command works perfectly, like below:
stage("Demo") {
sh "echo 'Hi There'"
}
I have been trying to "package" my shell scripts into their own classes just to neaten things up a bit. The problem is that when trying to execute the same exact shell script from within a class, jenkins fails the builds with:
org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException:
unclassified method java.lang.Class sh java.lang.String
This is a simple example that fails for me after moving the above method into its own class:
stage('Demo stage') {
Tools.PrintMe("Hi There")
}
public class Tools {
public static void PrintMe(String message) {
sh "echo " + message
}
}
There is also no option provided in the script manager to Whitelist this rejected method.
Is there a way to get around this? Or is there a limitation that I'm not aware of?
#Crait to make a call of predefined steps in your own class you need to path script object to you class.
So, try this:
stage('Demo stage') {
Tools.PrintMe(this, "Hi There")
}
public class Tools {
public static void PrintMe(def script, String message) {
script.sh "echo " + message
}
}
As #sshepel pointed out above, code executing in a plain script is not in the same context as code inside a class. I resolved it in a similar way to above by creating a static reference to the script object and then executing against that in my classes.
//Set the static reference in the script
Script.environment = this
public class Script {
public static environment
}
public class Tools {
public static void PrintMe(String message) {
Script.environment.sh "echo " + message
}
}
I did it this way to avoid polluting method signatures with passing the script object around. The downside is that all my classes will have a dependency on having "Script.environment = this" set.

jenkinsfile use traits and other groovy synax

I would like to use a slightly more complex pipeline build via jenkinsfiles, with some reusable steps as I have a lot or similar projects. I'm using jenkins 2.0 with the pipeline plugins. I know that you can load groovy scripts which contain can contain some generic pieces of code but I was wondering if these scripts can use some of the Object oriented features of groovy like traits. For example say I had a trait called Step:
package com.foo.something.ci
trait Step {
void execute(){ echo 'Null execution'}
}
And a class that then implemented the trait in another file:
class Lint implements Step {
def execute() {
stage('lint')
node {
echo 'Do Stuff'
}
}
}
And then another class that contained the 'main' function:
class foo {
def f = new Lint()
f.execute()
}
How would I load and use all these classes in a Jenkinsfile, especially since I may have multiple classes each defining a step? Is this even possible?
Have a look at Shared Libaries. These enable the use of native groovy code in Jenkins.
Your Jenkinsfile would include your shared libary, and the use the classes you defined. Be aware, that you have to pass the steps variable of Jenkins, if you want to use stage or the other variables defined in the Jenkins Pipeline plugin.
Excerpt from the documentation:
This is the class, which would define your stages
package org.foo
class Utilities implements Serializable {
def steps
Utilities(steps) {this.steps = steps}
def mvn(args) {
steps.sh "${steps.tool 'Maven'}/bin/mvn -o ${args}"
}
}
You would use it like this:
#Library('utils') import org.foo.Utilities
def utils = new Utilities(steps)
node {
utils.mvn 'clean package'
}

Resources