using Jenkins dynamic shared lib into pipeline - jenkins

I can't figure how to define a dynamic shared library, and use it into my pipeline:
myLib=library (identifier: 'lib#master', retriever: modernSCM(
[$class: 'GitSCMSource',
remote: 'https://mygit.orga.com/git/ORGA/Jenkins-libs.git',
credentialsId: 'aaaaaaa-8f3f-4e3c-vvvvvvv-6c77351e7872',
includes: '*',
excludes: 'test'
]))
pipeline {
agent {
node(){
label("linux&&!master")
}
}
tools {
jdk "JDK1.8.0_45"
maven "MVN339"
}
stages{
stage("test lib"){
steps {
script {
myLib.a.b.c.Utils.sayHelloTo("Guillaume")
log.info("test lib")
}
}
}
}
At runtime it fails with :
java.lang.ClassNotFoundException: Utils
at java.net.URLClassLoader$1.run(URLClassLoader.java:366)
at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
at java.lang.ClassLoader.loadClass(ClassLoader.java:425)
Any ideas of what is wrong? I can't find any info on dynamic shared lib tutorial on the net.
Edit : adding the library tree :
src
--a
--b
--c
Utils.groovy
Utils.groovy content:
package a.b.c
class Utils {
def sayHelloTo(String name) {
script.echo("Hello there $name")
}
}
Thanks guys,
Guillaume

I can't explain the CNFE you are getting, but there is definitely a couple of issues with this code.
The sayHelloTo method is not declared as static and yet, it is being called as one.
The script.echo doesn't seem valid, as I can't see script being a valid name here.
If you want to use this as a static function, then change the function like this
< def sayHelloTo(String name) {
> static sayHelloTo(def steps, String name) {
The only change to the call is to pass this (which should refer to pipeline steps):
< myLib.a.b.c.Utils.sayHelloTo("Guillaume")
> myLib.a.b.c.Utils.sayHelloTo(this, "Guillaume")
If you instead want to keep it as an instance method, then you still need to accept steps:
< def sayHelloTo(String name) {
> def sayHelloTo(def steps, String name) {
However, the call would change like this:
< myLib.a.b.c.Utils.sayHelloTo("Guillaume")
> myLib.a.b.c.Utils.new().sayHelloTo(this, "Guillaume")
In all the cases, the echo call would change as:
< script.echo("Hello there $name")
> steps.echo("Hello there $name")

Related

Jenkins samples groovy code vs plain groovy (closure errors)

In a jenkins shared library I can do something like this:
Jenkinsfile
#Library(value="my-shared-lib", changelog=false) _
jobGenerator {
notifier = [notifyEveryUnstableBuild: true]
}
sharedLibary/vars/jobGenerator.groovy
def call(body) {
println 'hi!'
}
To better understand the flow of whats goes on I have created two groovy files locally (with no reference to jenkins at all):
samples/launcher.groovy
jobGenerator {
s = 's'
}
samples/jobGenerator.groovy
def call(body) {
println 'inside jobGenerator '
}
But when I run that with:
groovy "/home/user/samples/launcher.groovy"
I get:
Caught: groovy.lang.MissingMethodException: No signature of method: launcher.jobGenerator() is applicable for argument types: (launcher$_run_closure1) values: [launcher$_run_closure1#61019f59]
groovy.lang.MissingMethodException: No signature of method: launcher.jobGenerator() is applicable for argument types: (launcher$_run_closure1) values: [launcher$_run_closure1#61019f59]
at launcher.run(launcher.groovy:2)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
So how much of the above code is jenkins/shared library specific? And is it even possible to write something like the above in plain groovy?
Or put in another way. How do I convert the above jenkins code to plain groovy?
IMHO following is close to what jenkins is doing
launcher.groovy
// load library scripts/functions
def binding = this.getBinding()
def gshell = new GroovyShell(this.getClass().getClassLoader(),binding)
new File("./my-lib").traverse(nameFilter: ~/.*\.groovy$/){f-> binding[f.name[0..-8]] = gshell.parse(f) }
// main
bar{
foo(name:"world")
}
./my-lib/foo.groovy
def call (Map m){
return "hello $m.name"
}
./my-lib/bar.groovy
def call (Closure c){
println ( "BAR: "+c() )
}
#> groovy launcher.groovy
BAR: hello world

Jenkins/Groovy: why are functions forbidden by scripts permitted by imported libraries?

Question: why are some functions disallowed if called in a Jenkinsfile, but allowed if called in a shared library that is imported by that same Jenkinsfile?
This question is not specific to directory-creation, but I will use it as an example, since that is the context in which I discovered this behavior:
The following Jenkins pipeline succeeds in creating a directory:
#Library('my-shared-libs') _
pipeline {
agent any
stages {
stage( "1" ) {
steps {
script {
utils.MkDir("/home/user/workspace/prj/foo")
}
}
}
}
}
// vars/utils.groovy
import java.io.File
def MkDir(the_dir) {
def f = new File(the_dir)
if ( ! f.mkdirs() ) { echo "Failed creating ${the_dir}" }
else { echo "Succeeded creating ${the_dir}" }
}
But the following pipeline:
pipeline {
agent any
stages {
stage( "1" ) {
steps {
script {
def the_dir = "/home/user/workspace/prj/bar"
def f = new File(the_dir)
if ( ! f.mkdirs() ) { echo "Failed creating ${the_dir}" }
else { echo "Succeeded creating ${the_dir}" }
}
}
}
}
}
...fails with this error:
org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: Scripts not permitted to use new java.io.File java.lang.String
Why is the directory-creation unsuccessful when called from the Jenkinsfile, but successful when called from the shared-library that is imported from that same Jenkinsfile?
The broader question this raises: what is the underlying "distinction" between a Jenkinsfile and shared libraries that it uses? There is some kind of "delineation" or "distinction" between Jenkinsfile declarative-syntax scripts and Groovy, and shared libraries, that isn't quite gelling in my mind. I'd be grateful if someone could help me understand.
Following #injecteer's suggestion, I tried the following modification to the second Jenkinsfile:
def the_dir = "/home/user/workspace/prj/bar"
def u = new URL( "file://${the_dir}" ).toURI()
def f = new File(u)
if ( ! f.mkdirs() ) { echo "Failed creating ${the_dir}" }
else { echo "Succeeded creating ${the_dir}" }
...which resulted in this error:
Scripts not permitted to use method java.net.URL toURI. Administrators can decide whether to approve or reject this signature.
It's not an option for me to do (or have done) this administrative approval, so this suggestion can't be an option for me, unfortunately.

How to pass parameters in a stage call in Jenkinsfile

Actually my Jenkinsfile looks like this:
#Library('my-libs') _
myPipeline{
my_build_stage(project: 'projectvalue', tag: '1.0' )
my_deploy_stage()
}
I am trying to pass these two variables (project and tag) to my build_stage.groovy, but it is not working.
What is the correct syntax to be able to use $params.project or $params.tag in my_build_stage.groovy?
Please see the below code which will pass parameters.
In your Jenkinsfile write below code:
// Global variable is used to get data from groovy file(shared library file)
def mylibrary
def PROJECT_VALUE= "projectvalue"
def TAG = 1
pipeline{
agent{}
stages{
stage('Build') {
steps {
script {
// Load Shared library Groovy file mylibs.Give your path of mylibs file which will contain all your function definitions
mylibrary= load 'C:\\Jenkins\\mylibs'
// Call function my_build stage and pass parameters
mylibrary.my_build_stage(PROJECT_VALUE, TAG )
}
}
}
stage('Deploy') {
steps {
script {
// Call function my_deploy_stage
mylibrary.my_deploy_stage()
}
}
}
}
}
Create a file named : mylibs(groovy file)
#!groovy
// Write or add Functions(definations of stages) which will be called from your jenkins file
def my_build_stage(PROJECT_VALUE,TAG_VALUE)
{
echo "${PROJECT_VALUE} : ${TAG_VALUE}"
}
def my_deploy_stage()
{
echo "In deploy stage"
}
return this

Groovy - readYaml() expecting java.util.LinkedHashMap instead of a file

As a part of our Jenkins solutions, we use Groovy in our pipelines.
In one of our groovy file I want to update a docker-stack.yaml.
To do so I'm using readYaml():
stage("Write docker-stack.yaml") {
def dockerStackYamlToWrite = readFile 'docker-stack.yaml'
def dockerStackYaml = readYaml file: "docker-stack.yaml"
def imageOrigin = dockerStackYaml.services[domain].image
def versionSource = imageOrigin.substring(imageOrigin.lastIndexOf(":") + 1, imageOrigin.length())
def imageWithNewVersion = imageOrigin.replace(versionSource, imageTag)
dockerStackYamlToWrite = dockerStackYamlToWrite.replace(imageOrigin, imageWithNewVersion)
sh "rm docker-stack.yaml"
writeFile file: "docker-stack.yaml", text: dockerStackYamlToWrite
sh "git add docker-stack.yaml"
sh "git commit -m 'promote dockerStack to ${envname}'"
sh "git push origin ${envname}"
}
I am using test to validate my code:
import org.junit.Before
import org.junit.Test
class TestUpdateVersionInDockerStack extends JenkinsfileBaseTest {
#Before
void setUp() throws Exception {
helper.registerAllowedMethod("build", [Map.class], null)
helper.registerAllowedMethod("steps", [Object.class], null)
super.setUp()
}
#Test void success() throws Exception {
def script = loadScript("src/test/jenkins/updateVersionInDockerStack/success.jenkins")
script.execute()
}
}
Here is the success.jenkins:
def execute() {
node() {
stage("Build") {
def version = buildVersion()
updateVersionInDockerStack([
DOMAIN : "security-package",
IMAGE_TAG : version,
GITHUB_ORGA : "Bla",
TARGET_ENV : "int"
])
}
}
}
return this
When I run my test I get this message:
groovy.lang.MissingMethodException: No signature of method: updateVersionInDockerStack.readYaml() is applicable for argument types: (java.util.LinkedHashMap) values: [[file:docker-stack.yaml]]
At this point I'm lost. For what I understand from the documentation readYaml() can I a file as an argument.
Can you help to understand why it is expecting a LinkedHashMap? Do you have to convert my value in a LinkedHashMap?
Thank you
Your pipeline unit test fails, because there is no readYaml method registered in pipeline's allowed methods. In your TestUpdateVersionInDockerStack test class simply add to the setUp method following line:
helper.registerAllowedMethod("readYaml", [Map.class], null)
This will instruct Jenkins pipeline unit environment that the method readYaml that accepts a single argument of type Map is allowed to use in the pipeline and invocation of this method will be registered in the unit test result stack. You can add a method printCallStack() call to your test method to see the stack of all executed steps during the test:
#Test void success() throws Exception {
def script = loadScript("src/test/jenkins/updateVersionInDockerStack/success.jenkins")
script.execute()
printCallStack()
}

Jenkins pipeline script fails with "General error during class generation: Method code too large!"

When running a large Jenkins pipeline script, it can give the error:
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed: General error during class generation: Method code too large!
java.lang.RuntimeException: Method code too large!
What is the reason for this error and how can it be fixed?
This is due to a limit between Java and Groovy, requiring that method bytecode be no larger than 64kb. It is not due to the Jenkins Pipeline DSL.
To solve this, instead of using a single monolithic pipeline script, break it up into methods and call the methods.
For example, instead of having:
stage foo
parallel([
... giant list of maps ...
])
Instead do:
stage foo
def build_foo() {
parallel([
...giant list of maps...
])}
build_foo()
If you are using declarative pipeline with shared library, you may need to refactor and externalize your global variables in the new methods. Here is a full example:
Jenkinsfile:
#Library("my-shared-library") _
myPipeline()
myPipeline.groovy:
def call() {
String SOME_GLOBAL_VARIABLE
String SOME_GLOBAL_FILENAME
pipeline {
stages() {
stage('stage 1') {
steps {
script {
SOME_GLOBAL_VARIABLE = 'hello'
SOME_GLOBAL_FILENAME = 'hello.txt'
...
}
}
}
stage('stage 2') {
steps {
script {
doSomething(fileContent: SOME_GLOBAL_VARIABLE, filename: SOME_GLOBAL_FILENAME)
sh "cat $SOME_GLOBAL_FILENAME"
}
}
}
}
}
}
def doSomething(Map params) {
String fileContent = params.fileContent
String filename = params.filename
sh "echo $fileContent > $filename"
}

Resources