Mocking jenkins pipeline steps - jenkins

I have a class that i use in my jenkinsfile, simplified version of it here:
class TestBuild {
def build(jenkins) {
jenkins.script {
jenkins.sh(returnStdout: true, script: "echo build")
}
}
}
And i supply this as a jenkins parameter when using it in the jenkinsfile. What would be the best way to mock jenkins object here that has script and sh ?
Thanks for your help

I had similar problems the other week, I came up with this:
import org.jenkinsci.plugins.workflow.cps.CpsScript
def mockCpsScript() {
return [
'sh': { arg ->
def script
def returnStdout
// depending on sh is called arg is either a map or a string vector with arguments
if (arg.length == 1 && arg[0] instanceof Map) {
script = arg[0]['script']
returnStdout = arg[0]['returnStdout']
} else {
script = arg[0]
}
println "Calling sh with script: ${script}"
},
'script' : { arg ->
arg[0]()
},
] as CpsScript
}
and used together with your script (extended with non-named sh call):
class TestBuild {
def build(jenkins) {
jenkins.script {
jenkins.sh(returnStdout: true, script: "echo build")
jenkins.sh("echo no named arguments")
}
}
}
def obj = new TestBuild()
obj.build(mockCpsScript())
it outputs:
[Pipeline] echo
Calling sh with script: echo build
[Pipeline] echo
Calling sh with script: echo no named arguments
Now this it self isn't very useful, but it easy to add logic which defines behaviour of the mock methods, for example, this version controls the contents returned by readFile depending of what directory and file is being read:
import org.jenkinsci.plugins.workflow.cps.CpsScript
def mockCpsScript(Map<String, String> readFileMap) {
def currentDir = null
return [
'dir' : { arg ->
def dir = arg[0]
def subClosure = arg[1]
if (currentDir != null) {
throw new IllegalStateException("Dir '${currentDir}' is already open, trying to open '${dir}'")
}
currentDir = dir
try {
subClosure()
} finally {
currentDir = null
}
},
'echo': { arg ->
println(arg[0])
},
'readFile' : { arg ->
def file = arg[0]
if (currentDir != null) {
file = currentDir + '/' + file
}
def contents = readFileMap[file]
if (contents == null) {
throw new IllegalStateException("There is no mapped file '${file}'!")
}
return contents
},
'script' : { arg ->
arg[0]()
},
] as CpsScript
}
class TestBuild {
def build(jenkins) {
jenkins.script {
jenkins.dir ('a') {
jenkins.echo(jenkins.readFile('some.file'))
}
jenkins.echo(jenkins.readFile('another.file'))
}
}
}
def obj = new TestBuild()
obj.build(mockCpsScript(['a/some.file' : 'Contents of first file', 'another.file' : 'Some other contents']))
This outputs:
[Pipeline] echo
Contents of first file
[Pipeline] echo
Some other contents
If you need to use currentBuild or similar properties, then you can need to assign those after the closure coercion:
import org.jenkinsci.plugins.workflow.cps.CpsScript
def mockCpsScript() {
def jenkins = [
// same as above
] as CpsScript
jenkins.currentBuild = [
// Add attributes you need here. E.g. result:
result:null,
]
return jenkins
}

Related

Set global variable in jenkins pipeline with Prepare an environment for the run

I'm trying to set environment variables in a jenkins pipeline script.
I want to use the section "Prepare an environment for the run" in configure. So, in this section, I put the following groovy script:
instance = Jenkins.getInstance()
def globalNodeProperties = instance.getGlobalNodeProperties()
def envVarsNodePropertyList = globalNodeProperties.getAll(hudson.slaves.EnvironmentVariablesNodeProperty.class)
def newEnvVarsNodeProperty = null
def envVars = null
if ( envVarsNodePropertyList == null || envVarsNodePropertyList.size() == 0 ) {
newEnvVarsNodeProperty = new hudson.slaves.EnvironmentVariablesNodeProperty();
globalNodeProperties.add(newEnvVarsNodeProperty)
envVars = newEnvVarsNodeProperty.getEnvVars()
} else {
envVars = envVarsNodePropertyList.get(0).getEnvVars()
}
envVars.put("DEFCONFIG", "Name_of_Defconfig")
instance.save()
And, the pipeline script is shown below :
def get_name() {
return env.DEFCONFIG
}
pipeline {
agent {
node {
label 'BSP'
}
}
environment {
defconfig_name = get_name()
}
stages {
stage('test var') {
steps {
sh '''
echo ${defconfig_name}
echo "Hello word"
'''
}
}
}
}
But, instead of reading the value of DEFCONFIG I find "null" in the console:
[Pipeline] sh
+ echo null
null
Probably I am missing something but I cannot find what.
Any idea please?

Jenkins Declarative Pipeline Include File

I am trying to a separate file holding variable for a Jenkins pipeline, this is because it will be used by multiple pipelines. But I can't seem to find the proper way to include it? Or if there's any way to include it?
MapA:
def MapA = [
ItemA: [
Environment: 'envA',
Name: 'ItemA',
Version: '1.0.0.2',
],
ItemB: [
Environment: 'envB',
Name: 'ItemB',
Version: '2.0.0.1',
]
]
return this;
MainScript:
def NodeLabel = 'windows'
def CustomWorkSpace = "C:/Workspace"
// Tried loading it here (Location 1)
load 'MapA'
pipeline {
agent {
node {
// Restrict Project Execution
label NodeLabel
// Use Custom Workspace
customWorkspace CustomWorkSpace
// Tried loading it here (Location 2)
load 'MapA'
}
}
stages {
// Solution
stage('Solution') {
steps {
script {
// Using it here
MapA.each { Solution ->
stage("Stage A") {
...
}
stage("Stage B") {
...
}
// Extract Commit Solution
stage("Stage C") {
...
echo "${Solution.value.Environment}"
echo "${Solution.value.Name}"
echo "${Solution.value.Version}"
}
}
}
}
}
}
}
On Location 1 outside the pipeline and node section: it gave the below error
org.jenkinsci.plugins.workflow.steps.MissingContextVariableException: Required context class hudson.FilePath is missing
Perhaps you forgot to surround the code with a step that provides this, such as: node
On Location 2 inside the node section: it gave the below error
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
WorkflowScript: 7: Expected to find ‘someKey "someValue"’ # line 7, column 14.
load 'MapA'
node {
^
You can achieve your scenario in 2 ways:
#1
If you want you can hardcode the variable in the same Jenkins file and make use of it on your pipeline like below Example :
Jenkinsfile content
def MapA = [
ItemA: [
Environment: 'envA',
Name: 'ItemA',
Version: '1.0.0.2',
],
ItemB: [
Environment: 'envB',
Name: 'ItemB',
Version: '2.0.0.1',
]
]
pipeline {
agent any;
stages {
stage('debug') {
steps {
script {
MapA.each { k, v ->
stage(k) {
v.each { k1,v1 ->
// do your actual task by accessing the map value like below
echo "${k} , ${k1} value is : ${v1}"
}
}
}
}
}
}
}
}
#2
If you would like to keep the variable in a separate groovy file in a gitrepo, it will be like below
Git Repo file and folder structure
.
├── Jenkinsfile
├── README.md
└── var.groovy
var.groovy
def mapA() {
return [
ItemA: [
Environment: 'envA',
Name: 'ItemA',
Version: '1.0.0.2',
],
ItemB: [
Environment: 'envB',
Name: 'ItemB',
Version: '2.0.0.1',
]
]
}
def helloWorld(){
println "Hello World!"
}
return this;
Jenkinsfile
pipeline {
agent any
stages {
stage("iterate") {
steps {
sh """
ls -al
"""
script {
def x = load "${env.WORKSPACE}/var.groovy"
x.helloWorld()
x.mapA().each { k, v ->
stage(k) {
v.each { k1,v1 ->
echo "for ${k} value of ${k1} is ${v1}"
}
} //stage
} //each
} //script
} //steps
} // stage
}
}

How to pass and invoke a method utility to Jenkins template?

I have this template:
def call(body) {
def pipelineParams= [:]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = pipelineParams
body()
pipeline {
agent any
....
stages {
stage('My stages') {
steps {
script {
pipelineParams.stagesParams.each { k, v ->
stage("$k") {
$v
}
}
}
}
}
}
post { ... }
}
}
Then I use the template in a pipeline:
#Library('pipeline-library') _
pipelineTemplateBasic {
stagesParams = [
'First stage': sh "do something...",
'Second stage': myCustomCommand("foo","bar")
]
}
In the stagesParams I pass the instances of my command (sh and myCustomCommand) and they land in the template as $v. How can I then execute them? Some sort of InvokeMethod($v)?
At the moment I am getting this error:
org.jenkinsci.plugins.workflow.steps.MissingContextVariableException: Required context class hudson.FilePath is missing
Perhaps you forgot to surround the code with a step that provides this, such as: node
The problem of using node is that it doesn't work in situations like parallel:
parallelStages = [:]
v.each { k2, v2 ->
parallelStages["$k2"] = {
// node {
stage("$k2") {
notifySlackStartStage()
$v2
checkLog()
}
// }
}
}
If you want to execute sh step provided with a map, you need to store map values as closures, e.g.
#Library('pipeline-library') _
pipelineTemplateBasic {
stagesParams = [
'First stage': {
sh "do something..."
}
'Second stage': {
myCustomCommand("foo","bar")
}
]
}
Then in the script part of your pipeline stage you will need to execute the closure, but also set the delegate and delegation strategy to the workflow script, e.g.
script {
pipelineParams.stagesParams.each { k, v ->
stage("$k") {
v.resolveStrategy = Closure.DELEGATE_FIRST
v.delegate = this
v.call()
}
}
}

Jenkins Pipeline Conditional Environmental Variables

I have a set of static environmental variables in the environmental directive section of a declarative pipeline. These values are available to every stage in the pipeline.
I want the values to change based on an arbitrary condition.
Is there a way to do this?
pipeline {
agent any
environment {
if ${params.condition} {
var1 = '123'
var2 = abc
} else {
var1 = '456'
var2 = def
}
}
stages {
stage('One') {
steps {
script {
...
echo env.var1
echo env.var2
...
}
}
}
}
stag('Two'){
steps {
script {
...
echo env.var1
echo env.var2
...
}
}
}
Looking for the same thing I found a nice answer in other question:
Basically is to use the ternary conditional operator
pipeline {
agent any
environment {
var1 = "${params.condition == true ? "123" : "456"}"
var2 = "${params.condition == true ? abc : def}"
}
}
Note: keep in mind that in the way you wrote your question (and I did my answer) the numbers are Strings and the letters are variables.
I would suggest you to create a stage "Environment" and declare your variable according to the condition you want, something like below:-
pipeline {
agent any
environment {
// Declare variables which will remain same throughout the build
}
stages {
stage('Environment') {
agent { node { label 'master' } }
steps {
script {
//Write condition for the variables which need to change
if ${params.condition} {
env.var1 = '123'
env.var2 = abc
} else {
env.var1 = '456'
env.var2 = def
}
sh "printenv"
}
}
}
stage('One') {
steps {
script {
...
echo env.var1
echo env.var2
...
}
}
}
stage('Two'){
steps {
script {
...
echo env.var1
echo env.var2
...
}
}
}
}
}
Suppose we want to use optional params for downstream job if it is called from upsteam job, and default params if downsteam job is called by itself.
But we don't want to have "holder" params with default value in downstream for some reason.
This could be done via groovy function:
upstream Jenkinsfile - param CREDENTIALS_ID is passed downsteam
pipeline {
stage {
steps {
build job: "my_downsteam_job_name",
parameters [string(name: 'CREDENTIALS_ID', value: 'other_credentials_id')]
}
}
}
downstream Jenkinsfile - if param CREDENTIALS_ID not passed from upsteam, function returns default value
def getCredentialsId() {
if(params.CREDENTIALS_ID) {
return params.CREDENTIALS_ID;
} else {
return "default_credentials_id";
}
}
pipeline {
environment{
TEST_PASSWORD = credentials("${getCredentialsId()}")
}
}
you can get another level of flexibility, using maps:
stage("set_env_vars") {
steps {
script {
def MY_MAP1 = [A: "123", B: "456", C: "789"]
def MY_MAP2 = [A: "abc", B: "def", C: "ghi"]
env.var1 = MY_MAP1."${env.switching_var}"
env.var2 = MY_MAP2."${env.switching_var}"
}
}
}
This way, more choices are possible.

Jenkins pipeline: load properties from file

Below pipeline codes works well:
pipeline {
agent {
label "test_agent"
}
stages {
stage("test") {
steps {
script {
sh "echo 'number=${BUILD_NUMBER}' >log"
if (fileExists('log')) {
load 'log'
retVal = "${number}"
}
echo "${retVal}"
}
}
}
}
}
However, when I tried to put the logic of read file to a lib(named getNumber.groovy) and call it in pipeline, like this:
getNumber.groovy
def call() {
def retVal
if (fileExists('log')) {
load 'log'
retVal = "${number}"
}
return retVal
}
This is how the pipeline (test.groovy) call this lib:
#Library('lib') _
pipeline {
agent {
label "test_agent"
}
stages {
stage("test") {
steps {
script {
sh "echo 'number=${BUILD_NUMBER}' >log"
def retVal = getNumber()
echo "${retVal}"
}
}
}
}
}
It always fail with below error:
[Pipeline] End of Pipeline
groovy.lang.MissingPropertyException: No such property: number for class: getNumber
at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.unwrap(ScriptBytecodeAdapter.java:53)
at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.getProperty(ScriptBytecodeAdapter.java:458)
at com.cloudbees.groovy.cps.sandbox.DefaultInvoker.getProperty(DefaultInvoker.java:34)
at com.cloudbees.groovy.cps.impl.PropertyAccessBlock.rawGet(PropertyAccessBlock.java:20)
Any suggestion? How to fix it if I want to encapsulate the logic in a lib?
[Edit]
If I change this segment
load 'log'
retVal = "${number}"
to this:
def matcher = readFile('log') =~ '^number=(.+)'
retVal=matcher ? matcher[0][1] : null
it works. But I just curious why the previous one can't work.

Resources