Closure as argument in Jenkins shared library function - jenkins

let's say I have a Configuration class in a Jenkins shared library written like this
class Configuration {
String param1, param2
Closure closure1
}
There's also a helper class like this
class Helper {
String helperMethod(String arg1, Closure closure1) {
// some invocation to closure 1
}
}
Within the var folder there's a dynamic pipeline in a pipeline.groovy file like this:
def call(Configuration config) {
node {
stage {
def helper = new Helper()
helper.helperMethod('foo') { config.closure1 it }
}
}
}
Finally I'm trying to use the shared library in another repo like this,
#Library('my-library')
import com.mylibrary.configuration.Configuration
def baz = { it.toUpperCase() }
def config = new Configuration(
param1: 'foo',
param2: 'bar',
closure1: baz
)
pipeline(config)
The problem is that baz get transformed to org.jenkinsci.plugins.workflow.cps.CpsClosure2 and helperMethod throws a MissingMethodException because of the type mismatch between CpsClosure2 and the expected groovy.lang.Closure
I've tried:
Using the #NonCPS annotation in baz
Strong typing the closure through a functional interface and trying to pass it to the config like closure1: baz as MyStronglyTypedClosure
Removing the closure typing in the helperMethod definition
Using helper.helperMethod('foo', config.closure1) instead of helper.helperMethod('foo') { config.closure1 it }
to no avail :(
Is there any workaround to receive the closure in the configuration so it can be used correctly in the helper class? Thanks in advance

MissingMethodException is usually thrown when you are invoking functions incorrectly or inexistent function, it rarely has to do with CPS closures, which indeed as dagget mentioned, extends standard java closures. I strongly recommend you to use testing frameworks like Spock (with Jenkins extension) to be able to detect errors beforehand.

Related

Jenkins | Use method from an imported java class in a groovy script

I want to be able to use a method from a Jenkins plugin via its java class
Just to point out I'm not a developer or a groovy/java expert - happy to learn!
The java class that my method is part of is com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator
From this I would like to use the method getRepoOwner()
What I've done is set my import and defined a new call to the class:
import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator
def bbSCMNav = new BitbucketSCMNavigator()
When I run this I get the error below:
org.codehaus.groovy.runtime.metaclass.MethodSelectionException: Could not find which method <init>() to invoke from this list:
public com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator#<init>(java.lang.String)
public com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator#<init>(java.lang.String, java.lang.String, java.lang.String)
I've searched for the error above Could not find which method <init>() to invoke from this list
And I came across this ticket Could not find which method <init>() to invoke from this list on newInstance in groovy closure
Can't say that I entirerly understand the reply if it's helpful to me or not as I say I'm not a developer and groovy and java are relatively new to me but happy to understand if anyone can point me in the right direction with this
The goal of this exercise is to use the method during the run-time of a build to get the output of getRepoOwner() and use that in a variable to construct a URI
This question also seems similar to mine - Calling internal methods of Jenkins plugin (thinBackup)
But I'm not using maven or a pom.xml here
Cheers
Quick Answer
This error Could not find which method < init >() is related to a missing constructor.
Almost all internal jenkins class are ready to use in groovy.
In your case, BitbucketSCMNavigator does not have a default constructor. It have a constructor with one String argument. Check this line
Explanation
I could replicate your error with another internal class org.jenkinsci.plugins.workflow.cps.CpsGroovyShellFactory:
node {
stage('internal') {
org.jenkinsci.plugins.workflow.cps.CpsGroovyShellFactory obj =
new org.jenkinsci.plugins.workflow.cps.CpsGroovyShellFactory();
}
}
hudson.remoting.ProxyException: org.codehaus.groovy.runtime.metaclass.MethodSelectionException: Could not find which method <init>() to invoke from this list:
private org.jenkinsci.plugins.workflow.cps.CpsGroovyShellFactory#<init>(org.jenkinsci.plugins.workflow.cps.CpsFlowExecution, boolean, java.lang.ClassLoader, java.util.List)
But, reviewing this class CpsFlowExecution I could see that CpsGroovyShellFactory does not have a default constructor. It have a constructor with one argument : CpsGroovyShellFactory(this)
So, If I instance the constructor with one argument, no errors appear.
node {
stage('internal') {
org.jenkinsci.plugins.workflow.cps.CpsGroovyShellFactory obj =
new org.jenkinsci.plugins.workflow.cps.CpsGroovyShellFactory(null);
}
}

Unapprovable RejectedAccessException when using Tuple in Jenkinsfile

I tried to use Tuple in a Jenkinsfile.
The line I wrote is def tupleTest = new Tuple('test', 'test2').
However, Jenkins did not accept this line and keep writing the following error to the console output:
No such constructor found: new groovy.lang.Tuple java.lang.String java.lang.String. Administrators can decide whether to approve or reject this signature.
...
org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: No such constructor found: new groovy.lang.Tuple java.lang.Integer java.lang.String
...
When I visited the "Script Approval" configuration I could not see any scripts that pend approval.
Following this link, I tried to install and enable the "Permissive Security" plugin, but it did not help either - The error was the same.
I even tried to manually add the problematic signature to the scriptApproval.xml file. After I added it, I was able to see it in the list of approved signatures, but the error still remained.
Is there something I am doing wrong?
I had the same issue trying to use tuple on jenkins so I found out that I can simply use a list literal instead:
def tuple = ["test1", "test2"]
which is equivalent to
def (a, b) = ["test1", "test2"]
So now, instead of returning a tuple, I am returning a list in my method
def myMethod(...) {
...
return ["test 1", "test 2"]
}
...
def (a, b) = myMethod(...)
This is more or less a problem caused by groovy.lang.Tuple constructor + Jenkins sandbox Groovy mode. If you take a look at the constructor of this class you will see something like this:
package groovy.lang;
import java.util.AbstractList;
import java.util.List;
public class Tuple extends AbstractList {
private final Object[] contents;
private int hashCode;
public Tuple(Object[] contents) {
if (contents == null) throw new NullPointerException();
this.contents = contents;
}
//....
}
Groovy sandbox mode (enabled by default for all Jenkins pipelines) ensures that every invocation passes script approval check. It's not foolproof, and when it sees new Tuple('a','b') it thinks that the user is looking for a constructor that matches exactly two parameters of type String. And because such constructor does not exists, it throws this exception. However, there are two simple workarounds to this problem.
Use groovy.lang.Tuple2 instead
If your tuple is a pair, then use groovy.lang.Tuple2 instead. The good news about this class is that it provides a constructor that supports two generic types, so it will work in your case.
Use exact Object[] constructor
Alternatively, you can use the exact constructor, e.g
def tuple = new Tuple(["test","test2"] as Object[])
Both options require script approval before you can use them (however, in this case both constructors appear in the in-process script approval page).

Using an enum loaded from another Groovy file (Jenkins Pipeline issue)

I have the following Groovy script as part of a Jenkins pipeline
permissions.groovy
enum PermissionType {
ANONYMOUS,
AUTHENTICATED
}
def get_job_permissions(PermissionType permission) {
...
}
return this
I load this file into another Groovy file as part of my Jenkins pipeline and call get_job_permissions passing through one of the enums as a parameter.
pipeline.groovy
def job_permissions = load 'permissions.groovy'
job_permissions.get_job_permissions(job_permissions.PermissionType.AUTHENTICATED)
Jenkins fails on this with the following error (I've verified that in this case 'Script3' is the call to get_job_permissions with the enum parameter).
groovy.lang.MissingPropertyException: No such property: PermissionType for class: Script3
I know the script load and call is correct, as I can change the signature of get_job_permissions to the following, pass through a random string in pipeline.groovy and the call goes through correctly.
def get_job_permissions(def permission) {
...
}
If I don't change the signature, and still pass through a random string, Jenkins fails the build as it can't find the method it thinks I'm calling (which is true, it's not there, it's expecting a PermissionType type).
I've tried a number of different things to expose PermissionType to the calling script
Adding #Field (not legal Groovy)
Changing the enum definition to public def PermissionType (not legal Groovy)
Removing and adding public to the enum definition
Changing the case (though I believe enums need to start with an upper case character?)
None of these solutions allow me to reference the enum type from the calling script, and I understand this is because I'm trying to access a type by referencing it through an instance of the script.
But if I can't do it this way, what is the best way to do it?
Thanks
I managed to get something to work - I certainly know it's probably not the right, or even good, way to do it, but it unblocked me and gave me what I needed.
Rather than define the enum in a script as you normally would
enum PermissionType {
ANONYMOUS,
AUTHENTICATED
}
I created a class containing the enum with member variables initialised to the values within the enum.
permissions.groovy
public class PermissionTypes {
public enum Values {
ANONYMOUS,
AUTHENTICATED
}
public final PermissionTypes.Values ANONYMOUS = PermissionTypes.Values.ANONYMOUS
public final PermissionTypes.Values AUTHENTICATED = PermissionTypes.Values.AUTHENTICATED
}
#Field final PermissionTypes Permissions = new PermissionTypes()
I can then expose an instance of that class in the script, load it as normal and I finally get access to the enum values.
pipeline.groovy
def job_permissions = load 'permissions.groovy'
job_permissions.get_job_permissions(job_permissions.Permissions.AUTHENTICATED)
I think we can all agree this is slightly bonkers, but it gave me what I needed.
Only issues I have with this (which I can live with for now)
You can only load the file ones in a script otherwise you get a duplicate class exception
You can't use the type in an external method, only the values - OK for me since any methods taking in the type are local to the class definition
Would still love to know the right way to do this :)
I ran into this problem recently and found a different solution that looks less hacky.
enum PermissionType {
ANONYMOUS,
AUTHENTICATED
}
def get_job_permissions(PermissionType permission) {
...
}
// Do this before you return out to make the enum available as well
this.PermissionType = PermissionType
return this
I prefer to use:
MyEnumClass.groovy:
package cl.mypackage.utils
class MyEnumClass {
static enum MyEnum {
FOO, BAR, QWERTY
}
}
How to use:
import cl.mypackage.utils.MyEnumClass
def in_some_place() {
def fooEnum = MyEnumClass.MyEnum.FOO
}
Regards

How do I create a "getter" for my modules (How do you instantiate a Module Object using a Navigator?)

I am trying to make it so that all of my page and module references can autocomplete in intellij.
Due to some sort of bug I am unable to do this like one normally would. (see here for more details: How to have geb static content recognized form test script )
In order to work around the above mentioned bug. I opted to create "getters" for all of my static content.
for example:
The Page:
class MyPage extends Page{
static content = {
tab {$(By.xpath("somexpath")}
}
Navigator tab(){
return tab
}
}
The Script:
//imagine we are in the middle of a feature method here
def test = at MyPage
test.tab().click()
So all of the above code works as I expect it to, and I want to redo my pages like this so that I can have autocomplete from the script side. Problems occur when I try to use this same technique for modules.
For example:
class MyPage extends Page{
static content = {
mod {module(new MyModule())}
}
MyModule mod(){
return mod
}
}
If I try and access mod from the script like so
//imagine we are in the middle of a feature method here
def test = at MyPage
test.mod().someModContentMaybe().click()
I get the following error:
org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object 'MyPage' -> mod: 'MyModule' with class 'geb.content.TemplateDerivedPageContent' to class 'MyModule'
If I try to do the following in the page object:
class MyPage extends Page{
static content = {
mod {module(new MyModule())}
}
MyModule mod(){
return new MyModule()
}
}
I get the following error when attempting to access the module from the script:
geb.error.ModuleInstanceNotInitializedException: Instance of module class MyModule has not been initialized. Please pass it to Navigable.module() or Navigator.module() before using it.
I guess it wants me to take an instantiated Navigator Object and and to call module(MyModule) but I am not sure how this works or how one would decide which Navigator Object to call module from.
All in all, I just want to be able to autocomplete module Names and static content from my scripts.
The Book of Geb's section about modules answers your question. You should not manually call the module's constructor, but instead instead use the syntax described right at the beginning of the chapter. This solution gets rid of the exception and also solves the code completion problem for me:
static content = {
mod { module MyModule }
}
Now that the exception is gone here is how to add the getter you asked for:
def myModule() { mod }
You're getting a GroovyCastException when returning content that contains a module from a method whose return type is a class which extends geb.Module because navigators and modules returned from content definitions get wrapped in geb.content.TemplateDerivedPageContent.
You can unwrap them using the as keyword as explained in the manual section about unwrapping modules returned from the content DSL. So, for one of your examples it would look like this:
MyModule mod(){
mod as MyModule
}
I think the problem is you content block. Modules are defined via Navigators' module method:
static content = {
mod { $("div.module").module(MyModule)
}
So no constructor calling required.

Using a shared library class from a custom step with Jenkins pipeline shared libraries

I am setting up a shared library for Jenkins pipelines and am trying to figure out how to import a class in the shared library into a custom step that I am writing.
Here's what the directory structure looks like:
src
--jenny
----util
------Versioning.groovy
vars
--calculateVersion.groovy
The Versioning.groovy file defines some static helper methods that do some stuff.
package jenny.util
class Versioner implements Serializable {
static bool checkForValidVersion(version) {
return true
}
}
I would like to call this method from the calculateVersion.groovy something like this:
def call(version) {
return jenny.util.Versioner.checkForValidVersion(version)
}
So that my declarative pipeline can call:
def valid = calculateVersion "1.0.0"
But I receive this error No such property: jenny for class: calculateReleaseVersions
Is it possible to reference the classes in the shared library from files in the vars to define custom steps and how is this done?
Yes it is possible. At least for us:
Just like in plain java (or groovy) we put an import statement into the groovy script in vars. In your case that would be something like:
import jenny.util.Versioner
def call(version) {
return Versioner.checkForValidVersion(version)
}
Another thing I just found: It looks like the file name of the class Versioner doesn't match the class name: Versioning.groovy. Could that be the issue?
If that doesn't work you propably want to upgrade your pipeline plugin version(s).

Resources