How "vars" in Jenkins Shared Libraries work? - jenkins

I'm experiencing some behaviors in Jenkins Shared Libraries, and it'll be great if someone can explain this to me:
First issue
Let's say i have a file in the vars directory:
// MultiMethod.groovy
def Foo() { ... }
def Bar() { ... }
Now if i want to use those functions from the pipeline, what i did was:
// Jenkinsfile
#Library('LibName') _
pipeline {
...
steps {
script {
// Method (1): this will work
def var = new MultiMethod()
var.Foo()
var.Bar()
// Method (2) this will not work
MultiMethod.Foo()
}
}
}
(The (1) and (2) methods are methods of calling the methods in the groovy script. Don't be confused by these 2 uses of the word "Method" please.)
So it works only if I instantiate this MultiMethod with the new operator.
But, if I name the file multiMethod (camelCased) instead of MultiMethod, i can use method (2) to call the methods in the script. Can someone explain this behavior?
That seems to be working fine.
Second issue
Based on the example above. If I have the groovy file named MultiMethod, (We saw earlier that i can use its methods if I instantiate with with new), I can't seem to instantiate an object of MultiMethod when loading the library dynamically, like this:
// Jenkinsfile
pipeline {
...
steps {
script {
// Method (1): this will not work
library 'LibName'
def var = new MultiMethod()
var.Foo()
var.Bar()
}
}
}
If i try to do so, i get this:
Running in Durability level: MAX_SURVIVABILITY
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
WorkflowScript: 11: unable to resolve class multiMethod
# line 11, column 32.
def mult = new multiMethod()
^
1 error
at org.codehaus.groovy.control.ErrorCollector.failIfErrors(ErrorCollector.java:310)
at org.codehaus.groovy.control.CompilationUnit.applyToSourceUnits(CompilationUnit.java:958)
at org.codehaus.groovy.control.CompilationUnit.doPhaseOperation(CompilationUni
...
EDIT
I notice that if I do this:
// Jenkinsfile
pipeline {
...
steps {
script {
library 'LibName'
MultiMethod.Foo()
MultiMethod.Bar()
}
}
}
It does work !!
Last Question
Another question if you may. I noticed that people use to write
return this
In the end of their scripts in the vars directory. Can someone explain what is it good for? I'd be happy if someone could explain this in the context of how does the mechanism of this works, like why are those scripts turns into Global Variables?
Thanks

First Question Answer
It's because Jenkins has defined that standard for the shared library. To clear your doubt, there is really a good explanation in Jenkins official documentation and it will work if you do it by following the standards. like below example:
make sure you are following this folder structure
shared-library
├── src
│   └── org
│   └── any
│   └── MultiMethod.groovy
└── vars
└── multiMethod.groovy
multiMethod.groovy
def foo() {
echo "Hello foo from vars/multiMethod.groovy"
}
def bar() {
echo "Hello bar from vars/multiMethod.groovy"
}
Once you have this and you are configuring your shared library like this way, then you can make use of multiMethod.groovy on your Jenkins file like below:
Jenkinsfile
#Library('jenkins-shared-library') _
pipeline {
agent any;
stages {
stage('log') {
steps {
script {
multiMethod.foo()
multiMethod.bar()
}
}
}
}
}
why does it work this way? - it explains here
But to make use of src/org/any/MultiMethod.groovy available in the src folder, you have to instantiate the class and call the method. Below is my example
MultiMethod.groovy
package org.any
class MultiMethod {
def steps;
MultiMethod(steps) {
this.steps = steps
}
def foo() {
steps.echo "Hello foo from src/org/any/MultiMethod.groovy"
}
def bar() {
steps.echo "Hello bar from src/org/any/MultiMethod.groovy"
}
}
Jenkinsfile
#Library('jenkins-shared-library') _
import org.any.MultiMethod
pipeline {
agent any;
stages {
stage('log') {
steps {
script {
def x= new MultiMethod(this);
x.foo()
x.bar()
}
}
}
}
}
Second Question Answer
Your second question is duplicate to this post. I have tried to explain and given an example. Please take a look.
Last Question Answer
It's not necessary to return this from the Jenkins global variable defined in vars if you do
vars/returnThisTest.groovy
def helloWorld() {
echo "Hello EveryOne"
}
or
def helloWorld() {
echo "Hello EveryOne"
}
return this;
both are the same and from Jenkinsfile you can just call like returnThisTest.helloWorld(), but return this can be more useful when a scenario will be like this - a good example from Jenkins documentation

Related

Create variable in shared library for Jenkinsfile

I'm new to shared libraries in Jenkins, and fairly new to Groovy as well.
I have several multibranch pipelines for different projects. I have setup email notifications for each job using an environmental variable containing a list of email addresses, which works just fine. However, several jobs share the same email addresses (depending on the project it's for) and I'd like to create a shared library for a master email list, so I don't have to update the list in each job individually if say I want to add or remove someone. I'm having trouble defining a variable in a library that can be used later in the Jenkinsfile. This is a simplified version of what I've been trying:
shared library (basically a copy paste of the environmental variables I was originally using in the individual Jenkinsfiles/jobs, which works):
Jenkinsfile-shared-libraries\vars\masterEmailList
def call () {
environment {
project1EmailList = "user1#xyz.com, user2#xyz.com, user3#xyz.com"
project2EmailList = "user2#xyz.com, user4#xyz.com, user5#xyz.com"
}
}
Jenkinsfile
#Library('Jenkinsfile-shared-libraries') _
pipeline {
agent any
stages {
stage ('email list for project 1') {
steps {
masterEmailList()
echo env.project1EmailList
}
}
}
}
The echo returns "null" rather than the email list of the project like I would expect.
Any guidance would be much appreciated!
Cheers.
The "Defining global variables" section of https://www.jenkins.io/doc/book/pipeline/shared-libraries/#defining-global-variables helped solve this one.
shared library:
Jenkinsfile-shared-libraries\vars\masterEmailList
def project1EmailList() {
"user1#xyz.com, user2#xyz.com, user3#xyz.com"
}
def project2EmailList() {
"user2#xyz.com, user4#xyz.com, user5#xyz.com"
}
Jenkinsfile:
#Library('Jenkinsfile-shared-libraries') _
pipeline {
agent any
stages {
stage ('email list for project 1') {
steps {
script {
echo masterEmailList.project1EmailList
}
}
}
}
}

Executing shell commands from inside Pipeline Shared Library

I'm writing a shared library that will get used in Pipelines.
class Deployer implements Serializable {
def steps
Deployer(steps) {
this.steps = steps
}
def deploy(env) {
// convert environment from steps to list
def process = "ls -l".execute(envlist, null)
process.consumeProcessOutput(output, error)
process.waitFor()
println output
println error
}
}
In the Jenkinsfile, I import the library, call the class and execute the deploy function inside a script section:
stage('mystep') {
steps {
script {
def deployer = com.mypackage.HelmDeployer("test")
deployer.deploy()
}
}
}
However, no output or errors are printed on the Console log.
Is it possible to execute stuff inside a shared library class? If so, how, and what am I doing wrong?
Yes, it is possible but not really an obvious solution. Every call that is usually done in the Jenkinsfile but was moved to the shared-library needs to reference the steps object you passed.
You can also reference the Jenkins environment by calling steps.env.
I will give you a short example:
class Deployer implements Serializable {
def steps
Deployer(steps) {
this.steps = steps
}
def callMe() {
// Always call the steps object
steps.echo("Test")
steps.echo("${steps.env.BRANCH_NAME}")
steps.sh("ls -al")
// Your command could look something like this:
// def process = steps.sh(script: "ls -l", returnStdout: true).execute(steps.env, null)
...
}
}
You also have to import the object of the shared library and create an instance of it. Define the following outside of your Pipeline.
import com.mypackage.Deployer // path is relative to your src/ folder of the shared library
def deployer = new Deployer(this) // 'this' references to the step object of the Jenkins
Then you can call it in your pipeline as the following:
... script { deployer.test() } ...

Using another class from Jenkins Shared Pipeline

I am currently using a Jenkins library without issues from my jobs.
Right now I am trying to do some refactor, there is a chunk of code to determine with AWS account to use in almost every tool we currently have in the library.
I created the following file "get account.groovy"
class GetAccount {
def getAccount(accountName) {
def awsAccount = "abcd"
return awsAccount;
}
}
Then I am trying to do this from within one of the other groovy scripts:
def getaccount = load 'getaccount.groovy'
def awsAccount = getaccount.getAccount(account)
But that does not work since it is looking for that file in the current work directory not in the library directory
I am unable to figure out what the best way to call another class from within a library that is already being used.
Jenkins load DSL is meant to load an externalize groovy file that is available in the job workspace and it will not work if you try to load a groovy script available in Jenkins shared library, because the shared library never checkout in the job Workspace.
If you follow the standard shared library structure like below, it could be done like :
shared-library
├── src
│   └── org
│   └── any
│   └── GetAccount.groovy
└── vars
└── aws.groovy
GetAccount.groovy
package org.any
class GetAccount {
def getAccount(accountName) {
def awsAccount = "abcd"
return awsAccount;
}
}
aws.groovy
import org.any;
def call() {
def x = new GetAccount()
// make use of val and proceed with your further logic
def val = x.getAccount('xyz')
}
In your Jenkinsfile (declarative or scripted ) you can use both the shared library groovy class like :
make use of aws.groovy
scripted pipeline
node {
stage('deploy') {
aws()
}
}
declarative pipeline
pipeline {
agent any;
stages {
stage('deploy') {
steps {
aws()
}
}
}
}
make use of GetAccount.groovy
scripted pipeline
import org.any
node {
stage('deploy') {
def x = new GetAccount()
// make use of val and proceed with your further logic
def val = x.getAccount('xyz')
}
}
declarative pipeline
import org.any
pipeline {
agent any;
stages {
stage('deploy') {
steps {
script {
def x = new GetAccount()
// make use of val and proceed with your further logic
def val = x.getAccount('xyz')
}
}
}
}
}

Use multi-method global variables in Jenkins Shared Libraries

Consider this groovy file in a repo that is loaded as shared library in Jenkins:
/ vars
|
--- Utility.groovy
// Utility.groovy
def funcA() { ... }
def funcB() { ... }
And in the Jenkinsfile:
// Jenkinsfile
#Library('LibName') _
pipeline {
...
steps {
script {
def util = new Utility()
util.funcA()
}
}
}
This works fine. But if i try to load the library dynamically:
// Jenkinsfile
pipeline {
...
steps {
script {
library 'LibName'
def util = new Utility()
}
}
}
That doesn't work...
Can someone explain this with respect to this quote from the documentation:
The documentation of Shared Libraries in Jenkins says:
Internally, scripts in the vars directory are instantiated on-demand as singletons. This allows multiple methods to be defined in a single .groovy file for convenience.
Loading a Jenkins Shared Library dynamically has some limitation and challenges because of:
Using classes from the src/ directory is also possible, but trickier. Whereas the #Library annotation prepares the “classpath” of the script prior to compilation, by the time a library step is encountered the script has already been compiled. Therefore you cannot import or otherwise “statically” refer to types from the library. which is explained here
And it seems this question is kind of similar to this one.

Trigger an action within Jenkins declarative pipeline right after a stage ends or just before a stage begins?

I have the following Jenkinsfile:
pipeline {
agent any
environment { }
stages {
stage('stageA') {
steps {
... Do something with arg1, arg2 or arg3
}
}
stage('stageB') {
steps {
... Do something with arg1, arg2 or arg3
}
}
...
}
}
Is there anywhere I can specify a universal "pre-stage" or "post-stage" set of actions to perform? A use-case would be sending logging information at the end of a stage to a log manager, but it would be preferable to not copy and paste those invocations at the end of each and every stage.
As far as I know there is no generic post- or pre-stage hook in Jenkins pipelines. You can define post steps in a post section but you need one per stage.
However, if you don't want to repeat yourself, you have some options.
Use a shared lib
The place to put repeating code to it a shared library. That way allows you to declare your own steps using Groovy.
You need another repository to define a shared lib, but apart from that it is a pretty strait forward way and you can reuse the code in all of your Jenkins' pipelines.
Use a function
If you declare a function outside of the pipeline, you can call it from any stage. This is not really documented and might be prevented in the future. As far as I understand it messes with the coordination between master and agents. However, it works:
pipeline {
agent any
stages {
stage ("First") {
steps {
writeFile file: "resultFirst.txt", text: "all good"
}
post {
always {
cleanup "first"
}
}
}
stage ("Second") {
steps {
writeFile file: "resultSecond.txt", text: "all good as well"
}
post {
always {
cleanup "second"
}
}
}
post {
always {
cleanup "global" // this is only triggered after all stages, not after every
}
}
}
}
void cleanup(String stage) {
echo "cleanup ${stage}"
archiveArtifacts artifacts: "result*"
}

Resources