groovy, script:this and jenkinsfile - jenkins

Based on this:
println in "call" method of "vars/foo.groovy" works, but not in method in class
I am trying to get my head around printing to the console from classes created in a Jenkins pipeline using jenkins shared libraries. I have the following:
MyPipeline.groovy
node("test") {
stage("Test") {
def a = new A(script:this)
echo "Calling A.a()"
a.a()
}
}
A.groovy
class A {
Script script;
public void a() {
script.echo("Hello from A")
def b = new B(script)
echo "Calling B.b()"
b.b()
}
}
B.groovy
class B {
Script script;
public void b() {
script.echo("Hello from B")
}
}
When I run that I get:
"Hello from A"
but then I get the error from B:
groovy.lang.GroovyRuntimeException: Could not find matching constructor for: samples.B(samples.MyPipeline)
How do I make it possible to print to console/build log in my classes also when delegating to other classes - like B in the above case?
As suggested by below answer I have now tried to update A.groovy to:
class A {
Script script;
public void a() {
script.echo("Hello from A")
def b = new B()
b.script = script
//def b = new B(script)
echo "Calling B.b()"
b.b()
}
}
But that just gives a new error:
hudson.remoting.ProxyException: groovy.lang.MissingMethodException: No signature of method: samples.A.echo() is applicable for argument types: (java.lang.String) values: [Calling B.b()]
Possible solutions: each(groovy.lang.Closure), getAt(java.lang.String), wait(), a(), every(), grep()

As per groovy's Initializing beans with named parameters and the default constructor
Just call empty constructor and set the parameter script
def b = new B()
b.script = script
With a bean like:
class Server {
String name
Cluster cluster }
Instead of setting each setter in subsequent statements as follows:
def server = new Server()
server.name = "Obelix"
server.cluster = aCluster
Also replace the following echo
echo "Calling B.b()"
To using script.echo method:
script.echo("Calling B.b()")

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 can I mock this closure with parameters that wraps other methods in a groovy script?

I just asked about how to mock jenkins step/global vars for testing a custom step and got help here
This is how I'm doing that now:
// vars/myCustomStep.groovy
def call(Map config) {
def paramOne = config.paramOne
container('my-container') {
echo paramOne
sh 'printenv'
}
}
// test/vars/myCustomStep.groovy
class myCustomStepTest {
Binding binding
def shell
def myCustomStep
#Before
public void setUp() throws Exception {
// Instantiate shell object and attach methods for mocking jenkins steps
binding = new Binding()
binding.echo = { String message -> println "$message" }
binding.sh = { String command -> println "sh step: $message" }
// How do I mock this??? this returns "No signature of method: myCustomStep.container() is applicable for argument types..."
binding.container = { Closure closure -> closure }
shell = new GroovyShell(binding)
// Instantiate groovy script
myCustomStep = shell.parse( new File( 'vars/myCustomStep.groovy' ) )
}
#Test
public void "sdfsdfsdf"() throws Exception {
myCustomStep paramOne: "sdlfsdfdsf"
}
}
My current challenge is figuring out how to mock things like script{} and container{} closures. I can't modify the structure of myCustomStep.groovy because this is jenkins specific. The behavior I want is for the test to essentially just ignore the container('xxx'){} method and only execute what is inside it.

How can I mock a method an external script uses?

This is specific to Jenkins but hoping there is a generic groovy feature that can help me here.
I have a groovy script (myCustomStep.grooy) I want to unit test. It MUST be written like it is below (it cannot be a class). It will include methods that are available during Jenkins run time but not locally and I want to mock them out.
Here is one of these scripts and a corresponding test. How do I mock out echo without modifying myCustomStep.groovy?
# vars/myCustomStep.grooy
def call(Map config) {
def paramOne = config.paramOne
echo paramOne
}
class MyCustomStepTest {
// I tried to define it here but I get "No signature of method: myCustomStep.echo()"
def echo(message) {
println "$message"
}
#Test
public void "sdfsdfsdf"() throws Exception {
def aaa = new GroovyShell().parse( new File( 'vars/myCustomStep.groovy' ) )
aaa deployment: "sdlfsdfdsf"
}
}
I can't have myCustomStep.grooy accept echo as an argument. Is there a way to monkey patch echo into the myCustomStep namespace?
EDIT: I found a simple solution but now I want to know how I can attach methods to myCustomStep for all tests instead of having to redefine for every test. I tried to do this in a #Before method (using junit) but the myCustomStep obj wasn't available to the tests.
class MyCustomStepTest {
def myCustomStep = new GroovyShell().parse( new File( 'vars/myCustomStep.groovy' ) )
#Test
public void "sdfsdfsdf"() throws Exception {
// how can I attach this once for use by all my tests?
myCustomStep.echo = { String message -> println "$message" }
myCustomStep deployment: "sdlfsdfdsf"
}
}
EDIT:
I was just confused about where to instantiate the object. Looks like I just need to create the object outside of the #before method and then update it inside of it.
#Before
public void setUp() throws Exception {
myCustomStep.echo = { String message -> println "$message" }
}
def myCustomStep = new GroovyShell().parse( new File( 'vars/myCustomStep.groovy' ) )
You could put echo in the binding using something like this:
Binding b = new Binding()
b.echo = { println "Hello There" }
def shell = new GroovyShell(b)
def aaa = shell.parse( new File( 'ars/myCustomStep.groovy' ) )
aaa deployment: "sdlfsdfdsf"

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.

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

Resources