Extending Jenkins job DSL code in multiple classes - jenkins

I've read a lot of tutorials regarding the Jenkins job DSL plugin but I cannot figure out how to create a job factory class that in turn uses another class that generates the jobs.
I already have a lot of jobs Groovy scripts that directly call the job factory and that works and my jobs are generated correctly. What I need is another class that in turn generates a lot of jobs using the previous job factory.
I've spent hours trying to debug this issue. I'm pretty sure it has something to do with closures and instantiating the factory objects.
package company.factory
import javaposse.jobdsl.dsl.DslFactory
import javaposse.jobdsl.dsl.Job
/**
* Base DSL templates for all Jenkins jobs.
*
*/
class JobFactory {
private DslFactory dslFactory
JobFactory(DslFactory dslFactory) {
this.dslFactory = dslFactory
}
Job generateBaseJob() {
dslFactory.job('TEST-1') {
logRotator(365, -1, 1, -1)
}
}
def generate() {
generateBaseJob()
}
}
Calling it a Groovy script works:
import company.factory.JobFactory
JobFactory jobfactory = new JobFactory(this)
jobfactory.generate()
Now, when I try to extend that class with another class, it fails.
package company.flow
import company.factory.JobFactory
class DeploymentFlow {
JobFactory jobfactory = new JobFactory(this)
def generateAllJobs() {
jobfactory.generate()
}
}
Calling it a Groovy script fails:
import company.flow.DeploymentFlow
new DeploymentFlow().generateAllJobs()
Error:
javaposse.jobdsl.dsl.DslScriptException: (JobFactory.groovy, line 36) No signature of method: company.flow.DeploymentFlow.job() is applicable for argument types: (java.lang.String, company.factory.JobFactory$_generateBaseJob_closure1) values: [TEST-1, company.factory.JobFactory$_generateBaseJob_closure1#60ed3159]
Possible solutions: any(), wait(), getA(), find(), grep(), dump()
at javaposse.jobdsl.dsl.AbstractDslScriptLoader.runScriptEngine(AbstractDslScriptLoader.groovy:114)
at javaposse.jobdsl.dsl.AbstractDslScriptLoader.runScripts_closure1(AbstractDslScriptLoader.groovy:61)
at groovy.lang.Closure.call(Closure.java:414)
at groovy.lang.Closure.call(Closure.java:430)
at javaposse.jobdsl.dsl.AbstractDslScriptLoader.runScripts(AbstractDslScriptLoader.groovy:46)
at javaposse.jobdsl.dsl.AbstractDslScriptLoader.runScript(AbstractDslScriptLoader.groovy:87)
at JobScriptsSpec.test script #file.name(JobScriptsSpec.groovy:57)
Caused by:
groovy.lang.MissingMethodException: No signature of method: company.flow.DeploymentFlow.job() is applicable for argument types: (java.lang.String, company.factory.JobFactory$_generateBaseJob_closure1) values: [TEST-1, company.factory.JobFactory$_generateBaseJob_closure1#60ed3159]
Possible solutions: any(), wait(), getA(), find(), grep(), dump()
at company.factory.JobFactory.generateBaseJob(JobFactory.groovy:36)
at company.flow.DeploymentFlow.generateAllJobs(DeploymentFlow.groovy:31)
at script.run(script:24)
at javaposse.jobdsl.dsl.AbstractDslScriptLoader.runScript(AbstractDslScriptLoader.groovy:138)
at javaposse.jobdsl.dsl.AbstractDslScriptLoader.runScriptEngine(AbstractDslScriptLoader.groovy:108)
... 6 more
I need to extend my job factory class into another class as I want to generate a lot of jobs and I want to avoid duplicated code. Any help would be greatly appreciated.

I managed to figure it out with the help from the nice people on Google Jenkins DSL plugin group.
The working code is below.
Factory class:
package company.factory
import javaposse.jobdsl.dsl.DslFactory
import javaposse.jobdsl.dsl.Job
class JobFactory {
private DslFactory dslFactory
JobFactory(DslFactory dslFactory) {
this.dslFactory = dslFactory
}
Job generateBaseJob() {
dslFactory.job('TEST-1')
}
}
Deployment class:
package company.flow
import company.factory.JobFactory
import javaposse.jobdsl.dsl.DslFactory
class DeploymentFlow {
JobFactory JobFactory
DeploymentFlow(DslFactory dslFactory) {
JobFactory = new JobFactory(dslFactory)
}
void generateAllJobs() {
JobFactory.generateBaseJob()
}
}
Calling my deployment class in my seed job:
import company.flow.DeploymentFlow
DeploymentFlow deploymentFlow = new DeploymentFlow(this)
deploymentFlow.generateAllJobs()

Related

Grails Quartz Plugin - No signature of triggerNow()

I'm trying to trigger a job manually from a controller in my Grails4 application.
I'm seeing this error:
groovy.lang.MissingMethodException: No signature of method: static com.myapp.MyJob.triggerNow() is applicable for argument types: (LinkedHashMap) values: [[foo:It works]]
I've seen older posts with the same issue, but they were using older versions of the plugin and I have tried all of the suggestions (package of controller and job the same, inject the quartzScheduler in the controller).
Here's my controller:
package com.myapp
class MyController {
def quartzScheduler
def myAction() {
MyJob.triggerNow([foo:"It works"])
}
}
Here's my job:
package com.myapp
class MyJob {
static triggers = {}
def execute(context) {
println context.mergedJobDataMap.get('foo')
}
}
My build.gradle file has this:
compile 'org.grails.plugins:quartz:2.0.13'
compile 'org.quartz-scheduler:quartz:2.2.1'

Can not use Groovy collections with closures in Jenkinsfile script

I want to use groovy closures with collections in Jenkinsfile:
// other parts removed for brevity
steps {
script {
def testList = ["item1", "item2", "item3"]
testList.stream().map{it+".jpeg"}.each{println it}
}
}
// other parts removed for brevity
However, it gives error:
hudson.remoting.ProxyException: groovy.lang.MissingMethodException: No signature of method: java.util.stream.ReferencePipeline$Head.map() is applicable for argument types: (org.jenkinsci.plugins.workflow.cps.CpsClosure2) values: [org.jenkinsci.plugins.workflow.cps.CpsClosure2#248ba046]
Possible solutions: map(java.util.function.Function), max(java.util.Comparator), min(java.util.Comparator), wait(), dump(), grep()
at org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SandboxInterceptor.onMethodCall(SandboxInterceptor.java:153)
at org.kohsuke.groovy.sandbox.impl.Checker$1.call(Checker.java:161)
at org.kohsuke.groovy.sandbox.impl.Checker.checkedCall(Checker.java:165)
at com.cloudbees.groovy.cps.sandbox.SandboxInvoker.methodCall(SandboxInvoker.java:17)
at WorkflowScript.run(WorkflowScript:49)
at ___cps.transform___(Native Method)
at com.cloudbees.groovy.cps.impl.ContinuationGroup.methodCall(ContinuationGroup.java:86)
...
I am fighting the same problem--constructs in Groovy which I use all the time, like all the handy collection methods, are breaking.
It turns out that in a Jenkinsfile, the code is passed through a transformer called Groovy CPS which enables Jenkins to stop and restart execution of a stage. In the process, stuff gets changed from its original class, and this causes the kind of error you're seeing here, because you're mixing non-CPS code (the Java lib methods) with CPS code (anything inside the Jenkinsfile).
Here's the link to the documentation of this behavior...but I didn't find it very helpful, so I am reverting to doing stuff like I would in Java (pre-lambda) with lots of for loops :(
While it’s not as handy as locally defined closures, you can, in some cases, define methods in the global scope and then rely on them via the Groovy method pointer operator.
void doSomething(final String key, final String value) {
println "${key} → ${value}"
}
pipeline {
stages {
stage('Process data') {
steps {
script {
final Map myData = [
"foo": "bar",
"plop": "yo",
]
myData.each(this.&doSomething)
}
}
}
}
}

Loading a shared library implicitly

I would like to integrate a Global library into my build flow. I have written a basic function
srv/core/jenkins/Checks.groovy:
package core.jenkins
class Checks implements Serializable {
def script
Checks(script) {
this.script = script
}
def fileExists(){
script.echo "File exists in the repo."
}
}
And it is exposed as a global var
vars/fileExisits.groovy:
def call() {
new core.jenkins.Checks(this).fileExists()
}
While configuring the Global Shared Library settings in Jenkins, I have the following settings:
Now in my jenkinsfile, Im doing something like this:
pipeline {
agent { label 'master' }
stages {
stage('Check for md files'){
steps {
sh 'echo hello'
script {
checks.fileExists()
}
}
}
}
}
This always gives the error
groovy.lang.MissingPropertyException: No such property: checks for class: groovy.lang.Binding
at groovy.lang.Binding.getVariable(Binding.java:63)
at
For it to work, I have to add the lines to the top of my Jenkinsfile
import core.jenkins.Checks
def checks = new Checks(this)
Is there a way for me to invoke the function fileExists from a library without having to add the above 2 lines always ?
Just replace:
checks.fileExists()
with:
fileExists()
All Groovy scripts that implements def call() methods and are stored in the vars/ folder can be triggered by their script file name. Alternatively, if you would like to keep checks.fileExists() syntax, then you need to create vars/checks.groovy script file and implement def fileExists() method inside of it.

How to extract sections of Jenkins pipeline script into classes?

I want to refactor my Jenkins pipeline script into classes for readability and reuse.
The problem is i get exceptions when doing so.
Let's look at a simple example:
When i run
echo currentBuild.toString()
everything is fine
But when i extract it into a class as so:
class MyClass implements Serializable {
def runBuild() {
echo currentBuild.toString()
}
}
new MyClass().runBuild()
i get an exception:
Started by user admin
Replayed #196
[Pipeline] End of Pipeline
groovy.lang.MissingPropertyException: No such property: currentBuild for class: MyClass
What is the proper way of extracting pipeline code in to classes?
You are on the right way, but the problem is that you didn't pass the script object to the instance of your class and was trying to call method which is not defined in the class that you have created.
Here is one way to solve this:
// Jenkins file or pipeline scripts editor in your job
new MyClass(this).runBuild()
// Class declaration
class MyClass implements Serializable {
def script
MyClass(def script) {
this.script=script
}
def runBuild() {
script.echo script.currentBuild.toString()
}
}
your code missing declare class field script
class MyClass implements Serializable {
def script
MyClass(def script) {
this.script=script
}
def runBuild() {
script.echo script.currentBuild.toString()
}
}
this code should be ok #bram

jenkinsfile use traits and other groovy synax

I would like to use a slightly more complex pipeline build via jenkinsfiles, with some reusable steps as I have a lot or similar projects. I'm using jenkins 2.0 with the pipeline plugins. I know that you can load groovy scripts which contain can contain some generic pieces of code but I was wondering if these scripts can use some of the Object oriented features of groovy like traits. For example say I had a trait called Step:
package com.foo.something.ci
trait Step {
void execute(){ echo 'Null execution'}
}
And a class that then implemented the trait in another file:
class Lint implements Step {
def execute() {
stage('lint')
node {
echo 'Do Stuff'
}
}
}
And then another class that contained the 'main' function:
class foo {
def f = new Lint()
f.execute()
}
How would I load and use all these classes in a Jenkinsfile, especially since I may have multiple classes each defining a step? Is this even possible?
Have a look at Shared Libaries. These enable the use of native groovy code in Jenkins.
Your Jenkinsfile would include your shared libary, and the use the classes you defined. Be aware, that you have to pass the steps variable of Jenkins, if you want to use stage or the other variables defined in the Jenkins Pipeline plugin.
Excerpt from the documentation:
This is the class, which would define your stages
package org.foo
class Utilities implements Serializable {
def steps
Utilities(steps) {this.steps = steps}
def mvn(args) {
steps.sh "${steps.tool 'Maven'}/bin/mvn -o ${args}"
}
}
You would use it like this:
#Library('utils') import org.foo.Utilities
def utils = new Utilities(steps)
node {
utils.mvn 'clean package'
}

Resources