How to create an extendable base job in Jenkins Job DSL? - jenkins

I'm trying to create a base job, to reduce the duplication between our jobs. I did the following, but it's not working:
def baseJob(Map m, Closure c = {}) {
type = m.type ?: 'dev'
pipelineJob("prefix-${m.name}") {
parameters {
stringParam('ONE', 'one', 'Description one')
}
c()
}
}
baseJob(type: 'release', name: 'test') {
parameters { // <-- Fails here
stringParam('TWO', 'two', 'Description two')
}
}
I get the following error:
ERROR: (script, line 12) No signature of method: script.parameters() is applicable for argument types: (script$_run_closure1$_closure4) values: [script$_run_closure1$_closure4#18b249b3]
The following works as expected:
def baseJob(Map m, Closure c = {}) {
type = m.type ?: 'dev'
pipelineJob("prefix-${m.name}") {
parameters {
stringParam('ONE', 'one', 'Description one')
}
parameters { // <-- This is fine
stringParam('TWO', 'two', 'Description two')
}
c()
}
}
baseJob(type: 'release', name: 'test')
So the problem isn't that I call parameters multiple times. The problem seems to be that I call parameters from inside a closure.
I'd like to believe that there is a way to execute the closure, so that parameters is called correctly. However, I suspect that I will have to learn a lot more about Groovy and Jenkins Job DSL, before I am able to figure it out. So I'm hoping that there is someone that knows how to do it.
If you have an alternative solution to accomplishing an extendable base job, that's a valid answer as well.

Answer : The method parameters is not implemented inside your script. It was actually implemented inside the pipeline Closure delegate class.
This code may help you to understand what's happening there....
class Test {
void printMe()
{
println "I am in test"
}
}
void test(Closure cl)
{
Test t = new Test()
cl.delegate = t
cl()
}
def callMe = { printMe()}
test {
printMe() // This will run
callMe() // This will fail
}
In Your case :
pipeline (String arg, Closure pipeLineClosure)
pipeLineClousure had been implemented inside a class X where the parameters method can be Found. like shown below code,
class X
{
...
parameters (Closure cl)
}
So the possible implementation could be :
class Custom{
String val1
String val2
String val3
}
def baseJob(Map m, List<Custom> l) {
type = m.type ?: 'dev'
pipelineJob("prefix-${m.name}") {
l.each{v->
parameters {
stringParam(v.val1, v.val2, v.val3)
}
}
}
}
List l = []
l.add new Custom(val1: 'ONE', val2: 'one', val3: 'description')
// can be add more values
baseJob(type: 'release', name: 'test', l)
HOPE IT HELPS

You just need to set the delegate of the closure that you're calling to the delegate of the closure that you are in:
def baseJob(Map m, Closure c = {}) {
type = m.type ?: 'dev'
pipelineJob("prefix-${m.name}") {
parameters {
stringParam('ONE', 'one', 'Description one')
}
c.delegate = delegate // <-- Just add this line
c()
}
}
baseJob(type: 'release', name: 'test') {
parameters {
stringParam('TWO', 'two', 'Description two')
}
}
delegate contains the delegate of the currently executing closure.

Related

Jenkins/Groovy: How to call withCredentials() from a class constructor?

How do I call withCredentials() from a Groovy class constructor?
Why does this:
#Library('my-sandbox-libs#dev') sandbox_lib
pipeline {
agent any
stages {
stage( "1" ) {
steps {
script {
try {
def my_obj = new org.obj.Obj()
}
catch(Exception e) {
echo "Jenkinsfile: ${e.toString()}"
throw e
}
}
}
}
}
}
// src/org/obj/Obj.groovy
package org.obj
public class Obj {
def secret_
Obj() {
withCredentials([string(credentialsId: 'test_secret_text', variable: 'val')]) {
this.secret_ = val
}
}
}
...generate this error:
Jenkinsfile: groovy.lang.MissingMethodException: No signature of method: org.obj.Obj.string() is applicable for argument types: (java.util.LinkedHashMap) values: [[credentialsId:test_secret_text, variable:val]]
Possible solutions: toString(), toString(), print(java.io.PrintWriter), print(java.lang.Object), find(), split(groovy.lang.Closure)
?
Update:
Tried the following, in line with #daggett's answer:
// src/org/obj/Obj.groovy
package org.obj
public class Obj {
def secret_
Obj(pipeline) {
pipeline.withCredentials([string(credentialsId: 'test_secret_text',
variable: 'val')]) {
this.secret_ = val
}
}
}
#Library('my-sandbox-libs#dev') sandbox_lib
pipeline {
agent any
stages {
stage( "1" ) {
steps {
script { def my_obj = new org.obj.Obj(this) }
}
}
}
}
...which generated the error Posting build status of FAILED to bitbucket.company.comhudson.remoting.ProxyException: groovy.lang.MissingMethodException: No signature of method: org.obj.Obj.string() is applicable for argument types: (java.util.LinkedHashMap) values: [[credentialsId:test_secret_text, variable:val]] Possible solutions: toString(), toString(), print(java.io.PrintWriter), print(java.lang.Object), find(), split(groovy.lang.Closure)
However, the following -- just an experiment -- runs fine:
// src/org/obj/Obj.groovy
package org.obj
public class Obj {
def secret_
Obj(pipeline) {
pipeline.echo "hello world"
}
}
Note: I also tried public class Obj implements Serializable {...}, which did not change the noted error.
I'm still having a hard time building a mental model of the Jenkins/Groovy language, but to me this appears as though there's something "special" about withCredentials() preventing it from being called from a class constructor.
In case it's relevant, I have Jenkins version 2.190.3, which has Groovy version 2.4.12.
this should help: https://www.jenkins.io/doc/book/pipeline/shared-libraries/#accessing-steps
the easiest way - to pass pipeline as argument into constructor
public class Obj {
def secret_
Obj(pipeline) {
pipeline.withCredentials(...
}
and call it like this
...
steps {
script {
def my_obj = new org.obj.Obj(this)
I solved it by doing the following:
Create a file in /vars which will do the withCredential.
File name for example: /vars/dockerCommands.groovy
(I called it docker.groovy before, but docker is an existing step already, so I advise to change it so it will not conflict.)
.
.
.
def login(String cred, String reg) {
withCredentials([
usernamePassword(credentialsId: cred ,usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASSWORD')
]){
docker login -u ${DOCKER_USER} -p ${DOCKER_PASSWORD} ${reg}"
}
}
.
.
In the class I called for this step.
.
.
public void login(String cred, String reg) {
this.steps.dockerCommands.login(cred, reg)
}
.
.
Note, steps is a private var which is equivalent to your pipeline var.
I create it in the constructor method of the class.

How to figure out which parameter of a Jenkins Groovy script expects String and which one GString?

I've defined a class SomeClass with a method
class SomeClass implements Serializable {
void someMethod(String var1, String var2, String... vars) {
...
}
}
which I'm using in a declarative pipeline as follows:
SomeClass instance = new SomeClass(this)
pipeline {
environment {
VAR1 = "var1"
VAR2 = sh(returnStdout: true, script: "echo var2").trim()
VAR3 = "var3"
}
stages {
stage("Stage X") {
steps {
script {
instance.someMethod("${VAR1}", "${VAR2}", "${VAR3}")
}
}
}
}
}
which fails due to
hudson.remoting.ProxyException: groovy.lang.MissingMethodException: No signature of method: org.jenkinsci.plugins.docker.workflow.SomeClass.someMethod() is applicable for argument types: (org.codehaus.groovy.runtime.GStringImpl, org.codehaus.groovy.runtime.GStringImpl, org.codehaus.groovy.runtime.GStringImpl) values: [var1, var2, var3]
If I change the invocation to instance.someMethod(VAR1.toString(), VAR2.toString(), VAR3.toString()) it fails due to
hudson.remoting.ProxyException: groovy.lang.MissingMethodException: No signature of method: org.jenkinsci.plugins.docker.workflow.SomeClass.someMethod() is applicable for argument types: (java.lang.String, java.lang.String, java.lang.String) values: [var1, var2, var3]
Afaik at least one of them should work. I think Groovy should be capable of making both work, but that's a second step. How can I invoke the method with the given arguments?
You can pass script object to the method. You can find information here and here.
Code below print variable:
SomeClass instance = new SomeClass()
pipeline {
agent any
environment {
VAR1 = "var1"
VAR2 = sh(returnStdout: true, script: "echo var2").trim()
VAR3 = "var3"
}
stages {
stage("Stage X") {
steps {
script {
instance.someMethod(this, "${VAR1}", "${VAR2}", "${VAR3}")
//instance.someMethod("${VAR1}", "${VAR2}", "${VAR3}")
}
}
}
}
}
class SomeClass implements Serializable {
void someMethod(def script, String var1, String var2, String... vars) {
script.echo "someMethod: ${var1}"
}
}
The reason was a type in instance when invoking instance.someMethod. Sorry for not doing a clean copy and paste from the reproducer. I'm surprised by the exception, though - very misleading to say the method doesn't exist if the instance it's called on doesn't even exist.

Jenkinsfile: How to call a groovy function with named arguments

I have a simple Declarative Pipeline with function inside. How to correctly use named arguments for a function?
def getInputParams(param1='a', param2='b') {
echo "param1 is ${param1}, param2 is ${param2}"
}
pipeline {
...
...
stages {
stage('Test') {
steps {
getInputParams(param1='x', param2='y')
}
}
}
}
I cannot understand why named params become null in function?
[Pipeline] echo
param1 is null, param2 is null
...
Well, I'm able to call function like getInputParams('x', 'y'), but it's not human readable (arguments amount may increase in future)
Groovy is executed inside the Jenkinsfile so you have to follow its syntax for named arguments.
foo(name: 'Michael', age: 24)
def foo(Map args) { "${args.name}: ${args.age}" }
Quote from Groovy's named arguments:
Like constructors, normal methods can also be called with named
arguments. They need to receive the parameters as a map. In the method
body, the values can be accessed as in normal maps (map.key).
def getInputParams(Map map) {
echo "param1 is ${map.param1}, param2 is ${map.param2}"
}
pipeline {
...
stages {
stage('Test') {
steps {
getInputParams(param1: 'x', param2: 'y')
}
}
}
}
If you are using groovy, use this.
def getInputParams(def param1, def param2) {
println("param1 is "+ param1 + ", param2 is " + param2)
}
pipeline {
...
...
stages {
stage('Test) {
steps {
getInputParams(x, y)
}
}
}
}

Passing closures from a class in Groovy/Jenkins

I'm trying to create a JobGenerator class that will pass a build step down to the calling instance. I'm running into an issue where if I get this error when I try to run this:
hudson.remoting.ProxyException: groovy.lang.MissingMethodException: No signature of method: org.jenkinsci.plugins.workflow.cps.CpsClosure2.build() is applicable for argument types: (java.util.LinkedHashMap) values: [[job:FooJob]]
class BuildGenerator implements Serializable {
static def generateJob() {
return [
"TestJob",
{ ->
build(
job: 'FooJob'
)
},
]
}
}
node(){
def tasks = [:]
def label
def task
stage("Build") {
def generator = new BuildGenerator()
tasks["Testing"] = generator.generateJob()[1]
parallel tasks
}
}
If I remove the generateJob function outside of the class then it works fine. What am I doing wrong with closures here? I'm new to groovy/jenkins world.
Well... This is the way Groovy/Jenkins pipeline work. build is available inside node as the rest of steps and functions. If you wish to access these you have to pass the CPS instance to the method, like this (or use constructor to pass the instance only once):
class BuildGenerator implements Serializable {
static def generateJob(script) {
return [
"TestJob",
{ ->
script.build(
job: 'FooJob'
)
},
]
}
}
node(){
def tasks = [:]
def label
def task
stage("Build") {
def generator = new BuildGenerator()
tasks["Testing"] = generator.generateJob(this)[1]
parallel tasks
}
}

Grails Mocking: Supply any argument to demand closure

I have a service that I'm trying to test. Inside this service is another UserAPIService that I want to mock. To mock it I'm doing the following:
given:
def userAPIServiceMock = mockFor(UserAPIService)
userAPIServiceMock.demand.createUser { def apiToken, firstname, lastname, email -> return true
}
service.userAPIService = userAPIServiceMock.createMock()
In the demand closure, I don't really care what arguments are passed to the createUser method. Is there a way I can say "regardless of the arguments passed to the createUser method, return true."
In other words, how can I change
userAPIServiceMock.demand.createUser { def apiToken, firstname, lastname, email -> return true
to
userAPIServiceMock.demand.createUser { [any arguments] -> return true
This is possible with Spock.
The syntax is as follows:
mock.method(*_) >> { args -> true }
Not sure how your Grails service needs to be mocked, but here's a full, general example in Spock:
interface Service {
boolean hej( String s, boolean b, char c )
}
class ExampleSpec extends Specification {
def "mock method with any number of args"() {
when:
def mock = Mock( Service )
mock.hej(*_) >> { args -> true }
then:
mock.hej( 'hi', true, 'a' as char ) == true
}
}
args is a List containing the actual arguments, which you can inspect in the closure and return the appropriate value.

Resources