Groovy Nested Closures - how to pass a hash? - jenkins

I'd like to load a closure for my Jenkins build, but pass it some variables that are generic to any type of build (Go, Java, Docker) that are going on on our system. Since I'm loading the specific closure from a separate groovy file, it doesn't see those variables. For the purposes of making a simpler example, I've commented out the load and included that closure.
I'm a little unsure about how to do this - how do I pass the config from buildProject to buildSpecificProject? Am I referring to it wrong?
#!/usr/bin/groovy
//def buildSpecificProject = load 'buildSpecificProject.groovy'
def buildSpecificProject = { body->
def config = [:]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = config
body()
println config.name
println config.builddirectory
}
def buildProject = { projbody ->
def config = [:]
projbody.resolveStrategy = Closure.DELEGATE_FIRST
projbody.delegate = config
projbody()
config.builddirectory = "/bar"
return config
}
try {
def newProjectVersion = buildSpecificProject { body ->
buildProject { projbody ->
name = 'projectname'
versionPrefix = "4.2.0"
fetchFromURL = 'git#github.com:myorg/myproject.git'
}
}
println "New Project Version = ${newProjectVersion}\n"
} catch (err) {
println err
}

I have no experience with build scripts in Jenkins really, so maybe my answer is not applicable, but from a Groovy only perspective the situation is the following:
The config variable in buildSpecificProject is a local variable, to which itself you have no access, unless you expose it or its value. Right now you do that through the setting the delegate actually.
If we say buildProject is only called from within a block given to buildSpecificProject, then the block given to buildProject is a Closure nested into the Closure given to buildSpecificProject. This Closure object will have a property owner, which will refer to the enclosing Closure instance in this case (see http://groovy-lang.org/closures.html#_owner_of_a_closure). Of this we know it has set the configuration as delegate, thus you can do projbody.owner.delegate to access the configuration set by buildSpecificProject.
But actually I would consider doing something like this:
def buildSpecificProject = { body ->
def config = [:]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = [config: config] // expose config
body()
println config.name
println config.builddirectory
return config
}
def buildProject = { config, projbody ->
projbody()
config.builddirectory = "/bar"
}
try {
def newProjectVersion = buildSpecificProject { body ->
// make config accessible to buildProject by providing it as parameter
buildProject(config) { projbody ->
config.name = 'projectname'
config.versionPrefix = "4.2.0"
config.fetchFromURL = 'git#github.com:myorg/myproject.git'
}
}
println "New Project Version = ${newProjectVersion}\n"
} catch (err) {
println err
}
As you can see it is actually enough for buildSpecificProject to set the delegate, which I set to a map with one key named config, containing the actual config. The disadvantage is of course, that you now have to do config.name. Also note the call "buildProject(config) { projbody ->", which gives the config to buildProject. And of course we can combine both ideas:
def buildSpecificProject = { body ->
def config = [:]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = config
body()
println config.name
println config.builddirectory
return config
}
def buildProject = { config, projbody ->
projbody()
config.builddirectory = "/bar"
}
try {
def newProjectVersion = buildSpecificProject {
buildProject(delegate) { projbody ->
name = 'projectname'
versionPrefix = "4.2.0"
fetchFromURL = 'git#github.com:myorg/myproject.git'
}
}
println "New Project Version = ${newProjectVersion}\n"
} catch (err) {
println err
}
But I would recommend this not so much, as I do not like to depend on the delegate in this manner. I can break too easily.

Related

Pass parameters to Jenkins shared library

I just simply want to pass the repo name cloud-nates in shared pipeline, so i've passed the parameter deployName from jenkinsfile to shared library. below is the Jenkinsfile
#Library("minePipelines#auto") _
if (env.BRANCH_NAME in ["auto", "stage"]) {
reloadDeploy {
deployName = "cloud-nates"
}
}
And below is the shared-pipeline reloadDeploy.groovy code :
def call(body) {
def config = [:]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = config
body()
properties([disableConcurrentBuilds()])
node("ops") {
timeout(unit: 'SECONDS', time: 60) {
stage("Reload Deployment") {
echo params.deployName
}
}
}
}
This prints null in console o/p. i've googled it but no luck :(
please feel free to ask any doubts.

Jenkins custom DSL Cannot invoke method image() on null object

I'm trying to define custom DSL refer https://www.jenkins.io/doc/book/pipeline/shared-libraries/#defining-custom-steps
it seems work if just define simple command in {}
but failed when use complicated command
(root)
+- vars
| +- shareL.groovy
| +- xxx.groovy
| +- monitorStep.groovy
shareL.groovy
def install(){
print "test install"
}
def checkout(){
print "test checkout"
}
monitorStep.groovy
def call(body) {
def config = [:]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = config
// This is where the magic happens - put your pipeline snippets in here, get variables from config.
script {
def status
def failed_cause=''
try{
body()
} catch(e){
status = 'fail'
failed_cause = "${e}"
throw e
}finally {
def myDataMap = [:]
myDataMap['stage'] = STAGE_NAME
myDataMap['status'] = status
myDataMap['failed_cause'] = failed_cause
influxDbPublisher selectedTarget: 'myTest', measurementName: 'myTestTable',customData: myDataMap
}
}
}
Jenkinsfile
#!groovy
#Library('myShareLibray#') _
pipeline {
stages{
stage('Checkout') {
steps {
script {
monitorStep{
shareL.checkout()
}
}
}
}
stage('Install') {
steps {
script {
monitorStep{
docker.image("node").inside(){
shareL.install()
}
}
}
}
}
}
}
first stage failed with
java.lang.NullPointerException: Cannot invoke method checkout() on null object
second stage failed with
java.lang.NullPointerException: Cannot invoke method image() on null object
The problem is that the closure cannot find the name shareL which should be accessible in the closure's delegate, which is in our case the map config.
You need to redeclare the map to expose the name shareL and additionally a second name install which must be invokable.
The solution is to rewrite the map like this:
def config = [ shareL : [install: { println "Inside the map" }] ]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = config
body()
Then when you call body() it will find shareL.install() but this will not point to the call() method in the shareL.groovy but to the property in the map.

Passing environment variable as a pipeline parameter to Jenkins shared library

I have a shared Jenkins library that has my pipeline for Jenkinsfile. The library is structured as follows:
myPipeline.groovy file
def call(body) {
def params= [:]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = params
body()
pipeline {
// My entire pipeline is here
// Demo stage
stage("Something"){
steps{
script{
projectName = params.name
}
}
}
}
}
And my Jenkinsfile is as follows:
Jenkinsfile
#Library("some-shared-lib") _
myPipeline{
name = "Some name"
}
Now, I would like to replace "Some name" string with "env.JOB_NAME" command. Normally in Jenkinsfile, I would use name = "${env.JOB_NAME}" to get the info, but because I am using my shared library instead, it failed to work. Error message is as follows:
java.lang.NullPointerException: Cannot get property 'JOB_NAME' on null object
I tried to play around with brackets and other notation but never got it to work. I think that I incorrectly pass a parameter. I would like Jenkinsfile to assign "${env.JOB_NAME}" to projectName variable, once library runs the pipeline that I am calling (via myPipeline{} command)
You can do like this in myPipeline.groovy:
def call(body) {
def params= [:]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = params
body()
pipeline {
// My entire pipeline is here
// Demo stage
stage("Something"){
steps{
script{
projectName = "${env.JOB_NAME}"
}
}
}
}
}

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()
}
}
}

Passing parameters from Jenkinsfile to a shared library

I have several components(code projects with their own Bitbucket repositories) and each of them has a Jenkinsfile as follows:
properties([parameters([string(defaultValue: "", description: "List of components", name: 'componentsToUpdate'),
string(defaultValue: "refs%2Fheads%2Fproject%2Fintegration", description: "BuildInfo CommitID", name: 'commitId'),
string(defaultValue: "", description: "Tag to release, e.g. 1.1.0-integration", name: 'releaseTag'),
string(defaultValue: "", description: "Forked buildInfo repo. Be aware right commit ID!!!", name: 'fork')]),
[$class: 'BuildDiscarderProperty', strategy: [$class: 'LogRotator', artifactDaysToKeepStr: '', artifactNumToKeepStr: '', daysToKeepStr: '7', numToKeepStr: '5']],
disableConcurrentBuilds()])
#Library('jenkins-shared-stages')
import mergePipeline
import releasePipeline
import ripplePipeline
import componentPipeline
def branchName = env.BRANCH_NAME
def rewriteDependencies = ""
def returnValue = null
def forkedRepo = params.fork
def buildInfoCommitId = params.commitId
def tagToRelease = params.releaseTag
println "buildInfoCommitId: " + buildInfoCommitId
if(params.componentsToUpdate) {
rewriteDependencies = params.componentsToUpdate
}
if (branchName == "project/integration") {
mergePipeline {
}
} else if (branchName == 'master') {
releasePipeline {
releaseTag = tagToRelease
}
} else {
returnValue = componentPipeline {
componentsToUpdate = rewriteDependencies
commitId = buildInfoCommitId
runOnForkedRepo = forkedRepo
}
rewriteDependencies = rewriteDependencies.isEmpty() ? returnValue : rewriteDependencies + "," + returnValue
println "WHAT is rewriteDependencies? " + rewriteDependencies
println "The return value: " + returnValue
ripplePipeline {
commitId = buildInfoCommitId
componentName = returnValue
runOnForkedRepo = forkedRepo
componentsToUpdate = rewriteDependencies
}
}
Need to use a 'wrapper' pipeline, say, wrapperPipeline.groovy:
import mergePipeline
import releasePipeline
import ripplePipeline
import componentPipeline
import org.slf4j.Logger
import org.slf4j.LoggerFactory
def call(body) {
final Logger logger = LoggerFactory.getLogger(wrapperPipeline)
def config = [:]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = config
body()
// Assuming we have multibranch pipeline job or defined branch name in the env
def branchName = env.BRANCH_NAME
// There is a bug in the Jenkins it will pass a string "null" as a gradle build parameter instead of NULL object if there is
// empty parameter has been passed!!!
def rewriteDependencies = ""
def returnValue = null
def forkedRepo = config.runOnForkedRepo
def buildInfoCommitId = config.commitId
def tagToRelease = config.releaseTag
def globalVars = new se.GlobalVars()
def notifyHandler = new se.NotifyHandler()
node(globalVars.getAgent('buildAgent')) {
def PIPELINE_NAME = "wrapperPipeline"
try {
logger.info("The buildInfoCommitId is {}", buildInfoCommitId)
logger.info("Branch name: {}", branchName)
println "buildInfoCommitId: "+buildInfoCommitId
println"Branch name: "+branchName
if (config.componentsToUpdate) {
rewriteDependencies = config.componentsToUpdate
}
// keep the same integration pipeline for the master branch for now
if (branchName == "project/integration") {
logger.info("Invoking mergePipeline")
println "Invoking mergePipeline"
mergePipeline {
}
} else if (branchName == 'master') {
logger.info("Invoking releasePipeline")
println "Invoking releasePipeline"
releasePipeline {
releaseTag = tagToRelease
}
} else {
logger.info("Invoking componentPipeline")
println "Invoking componentPipeline"
returnValue = componentPipeline {
componentsToUpdate = rewriteDependencies
commitId = buildInfoCommitId
runOnForkedRepo = forkedRepo
}
logger.info("Component pipeline has returned {}", returnValue)
println "Component pipeline has returned"+returnValue
// We need to provide new version of the component to the Ripple builds
rewriteDependencies = rewriteDependencies.isEmpty() ? returnValue : rewriteDependencies + "," + returnValue
logger.info("rewriteDependencies: {}", rewriteDependencies)
println "The return value: " + returnValue
ripplePipeline {
commitId = buildInfoCommitId
componentName = returnValue
runOnForkedRepo = forkedRepo
componentsToUpdate = rewriteDependencies
}
}
}
catch (err) {
def build_status = "Exception ${err.message} in build ${env.BUILD_ID}"
logger.error(build_status,err)
notifyHandler.NotifyFail(build_status, PIPELINE_NAME)
throw err
}
}
}
The modified Jenkinsfile:
properties([parameters([string(defaultValue: "", description: "List of components", name: 'componentsToUpdate'),
string(defaultValue: "refs%2Fheads%2Fproject%2Fintegration", description: "BuildInfo CommitID", name: 'commitId'),
string(defaultValue: "", description: "Tag to release, e.g. 1.1.0-integration", name: 'releaseTag'),
string(defaultValue: "", description: "Forked buildInfo repo. Be aware right commit ID!!!", name: 'fork')]),
[$class: 'BuildDiscarderProperty', strategy: [$class: 'LogRotator', artifactDaysToKeepStr: '', artifactNumToKeepStr: '', daysToKeepStr: '7', numToKeepStr: '5']],
disableConcurrentBuilds()])
#Library('jenkins-shared-stages#integration/CICD-959-wrapper-pipeline-for-the-jenkinsfile') _
import wrapperPipeline
wrapperPipeline{}
Now, I suspect that the params object(the properties from the Jenkinsfile) is not populated correctly. For example
def buildInfoCommitId = config.commitId
.
.
.
println "buildInfoCommitId: "+buildInfoCommitId
prints null.
How do I invoke the wrapperPipeline correctly?
Note: I am new to both Jenkins pipelines and Groovy :)
Because those are Jenkins Parameters, they are not in the config object.
You will access commitId as params.commitId
If you had something within the closure when you call wrapperPipeline(), then those would be in the config object. e.g.
wrapperPipeline({
param="value"
})
then config.param would result in "value"
However, as a word of advice, I recommend avoiding using a closure when calling libs stored under vars/ in the shared library. See http://groovy-lang.org/closures.html for what closures are. The crux of it is, they are fairly complicated and can introduce some issues if you end up trying to pass in dynamic variables due to when the closure is instantiated. (They have their place but for simple things, I think avoiding is better)
I'd recommend instead, implementing a helper function that will allow you use maps OR closures for calling shared libs.
add a shared library called buildConfig under your src path:
package net.my.jenkins.workflow
import com.cloudbees.groovy.cps.NonCPS
class BuildConfig implements Serializable {
static Map resolve(def body = [:]) {
Map config = [:]
config = body
if (body in Map) {
config = body
} else if (body in Closure) {
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = config
body()
} else {
throw new Exception(sprintf("Unsupported build config type:%s", [config.getClass()]))
}
return config
}
}
And then in your shared lib under vars/ start with
import net.my.jenkins.workflow.BuildConfig
def call(def body = [:]) {
// evaluate the body block, and collect configuration into the object
config = BuildConfig.resolve(body)
This then allows you to use Maps which removes the complexity, so you could for instance (not that you would since you would just use params.commitId) re-assign it.
wrapperPipeline ([
"commitId": params.commitId,
])
Which means again config.commitId now has the value of params.commitId
Let me know if you need more detail.
TL;DR - You should be using params object, because you have parameters defined.
If you did start passing in arguments via the shared lib call, I would use a map over a closure. (requires some minimal implementation)

Resources