Jenkins Shared Library Pipelines: Singletons vs On-Demand Singletons - jenkins

In the Jenkins documentation, it mentions that all var definitions are on-demand singletons, but does not explain what that means. Documentation reference:
https://jenkins.io/doc/book/pipeline/shared-libraries/#defining-global-variables
Does this mean that the singleton is determined at the moment the job is called and so its a unique instance for that particular job? And how does this compare to an explicit #Singleton call when placed on a class?
I tried setting up my shared pipeline so I could review both sides of it. When I run the code below, I can see in the output that both classes print out the information I expect. They both start as empty strings and then are set equal to the env.BUILD_TAG (list of supported env variables here).
My concern is that if I have multiple jobs running concurrently, do they share the same Singleton or is it unique to each build? So far I have not been able to prove this, so I was hoping someone has an answer to this.
Structure
src
- org
- utilities
- GlobalStateClass.groovy
vars
- globalStateVar.groovy
- main.groovy
GlobalStateClass.groovy
#!groovy
package org.utilities
#Singleton
class GlobalStateClass implements Serialization{
public String version = ""
}
globalStateVar.groovy
#!groovy
class globalStateVar implements Serialization{
public String version = ""
}
main.groovy
#!groovy
import org.utilities.GlobalStateClass
def call () {
println "Old Values: ${ GlobalStateClass.instance.version } ${ globalStateVar.version }"
String v = env.BUILD_TAG
GlobalState.instance.version = v
globalStateVar.version = v
println "New Values: ${ GlobalStateClass.instance.version } ${ globalStateVar.version }"
}
Jenkinsfile
pipeline {
agent any
stages {
stage('test 0'){
main()
}
stage('test 1'){
main()
}
stage('test 2'){
main()
}
stage('test 3'){
main()
}
}
}

Related

Accessing Jenkins pipeline WorkflowScript during execution

I would like to know if there is a way to access the Jenkins Workflow script object during its execution.
I have a shared library, and I can pass this object to any groovy class as an argument, either directly from the Jenkins file, using 'this' keyword, or from any DSL in the vars folder, also using the 'this' keyword.
But I would like to access it using a method, even if this imply using reflexivity.
Is that possible?
Here example with pipeline, where this is a script object. Some other examples here:
MyClass myClass = new MyClass()
pipeline {
agent any
environment {
VAR1 = "var1"
VAR2 = sh(returnStdout: true, script: "echo var2").trim()
VAR3 = "var3"
}
stages {
stage("Stage 1") {
steps {
script {
myClass.myPrint(this, "${VAR1}", "${VAR2}", "${VAR3}")
}
}
}
}
}
class MyClass implements Serializable {
void myPrint(def script, String var1, String var2, String... vars) {
script.echo "myPrint: ${var1}"
}
}

Strange variable scoping behavior in Jenkinsfile

When I run the below Jenkins pipeline script:
def some_var = "some value"
def pr() {
def another_var = "another " + some_var
echo "${another_var}"
}
pipeline {
agent any
stages {
stage ("Run") {
steps {
pr()
}
}
}
}
I get this error:
groovy.lang.MissingPropertyException: No such property: some_var for class: groovy.lang.Binding
If the def is removed from some_var, it works fine. Could someone explain the scoping rules that cause this behavior?
TL;DR
variables defined with def in the main script body cannot be accessed from other methods.
variables defined without def can be accessed directly by any method even from different scripts. It's a bad practice.
variables defined with def and #Field annotation can be accessed directly from methods defined in the same script.
Explanation
When groovy compiles that script it actually moves everything to a class that roughly looks something like this
class Script1 {
def pr() {
def another_var = "another " + some_var
echo "${another_var}"
}
def run() {
def some_var = "some value"
pipeline {
agent any
stages {
stage ("Run") {
steps {
pr()
}
}
}
}
}
}
You can see that some_var is clearly out of scope for pr() becuse it's a local variable in a different method.
When you define a variable without def you actually put that variable into a Binding of the script (so-called binding variables). So when groovy executes pr() method firstly it tries to find a local variable with a name some_var and if it doesn't exist it then tries to find that variable in a Binding (which exists because you defined it without def).
Binding variables are considered bad practice because if you load multiple scripts (load step) binding variables will be accessible in all those scripts because Jenkins shares the same Binding for all scripts. A much better alternative is to use #Field annotation. This way you can make a variable accessible in all methods inside one script without exposing it to other scripts.
import groovy.transform.Field
#Field
def some_var = "some value"
def pr() {
def another_var = "another " + some_var
echo "${another_var}"
}
//your pipeline
When groovy compiles this script into a class it will look something like this
class Script1 {
def some_var = "some value"
def pr() {
def another_var = "another " + some_var
echo "${another_var}"
}
def run() {
//your pipeline
}
}
Great Answer from #Vitalii Vitrenko!
I tried program to verify that. Also added few more test cases.
import groovy.transform.Field
#Field
def CLASS_VAR = "CLASS"
def METHOD_VAR = "METHOD"
GLOBAL_VAR = "GLOBAL"
def testMethod() {
echo "testMethod starts:"
def testMethodLocalVar = "Test_Method_Local_Var"
testMethodGlobalVar = "Test_Metho_Global_var"
echo "${CLASS_VAR}"
// echo "${METHOD_VAR}" //can be accessed only within pipeline run method
echo "${GLOBAL_VAR}"
echo "${testMethodLocalVar}"
echo "${testMethodGlobalVar}"
echo "testMethod ends:"
}
pipeline {
agent any
stages {
stage('parallel stage') {
parallel {
stage('parallel one') {
agent any
steps {
echo "parallel one"
testMethod()
echo "${CLASS_VAR}"
echo "${METHOD_VAR}"
echo "${GLOBAL_VAR}"
echo "${testMethodGlobalVar}"
script {
pipelineMethodOneGlobalVar = "pipelineMethodOneGlobalVar"
sh_output = sh returnStdout: true, script: 'pwd' //Declared global to access outside the script
}
echo "sh_output ${sh_output}"
}
}
stage('parallel two') {
agent any
steps {
echo "parallel two"
// pipelineGlobalVar = "new" //cannot introduce new variables here
// def pipelineMethodVar = "new" //cannot introduce new variables here
script { //new variable and reassigning needs scripted-pipeline
def pipelineMethodLocalVar = "new";
pipelineMethodLocalVar = "pipelineMethodLocalVar reassigned";
pipelineMethodGlobalVar = "new" //no def keyword
pipelineMethodGlobalVar = "pipelineMethodGlobalVar reassigned"
CLASS_VAR = "CLASS TWO"
METHOD_VAR = "METHOD TWO"
GLOBAL_VAR = "GLOBAL TWO"
}
// echo "${pipelineMethodLocalVar}" only script level scope, cannot be accessed here
echo "${pipelineMethodGlobalVar}"
echo "${pipelineMethodOneGlobalVar}"
testMethod()
}
}
}
}
stage('sequential') {
steps {
script {
echo "sequential"
}
}
}
}
}
Observations:
Six cases of variables declarations
a. Three types (with def, without def, with def and with #field) before/above pipeline
b. within scripted-pipeline (with def, without def) within pipeline
c. Local to a method (with def) outside pipeline
new variable declaration and reassigning needs scripted-pipeline within pipeline.
All the variable declared outside pipeline can be accessed between the stages
Variable with def keyword generally specific to a method, if it is declared inside script then will not be available outside of it. So need to declare global variable (without def) within script to access outside of script.

How to access variables outside stages in Jenkins file Groovy function?

My jenkins file looks like below:
import groovy.json.*
def manifestFile = "C:\\manifest.yml"
node {
stage('Build') {
}
stage('Deploy') {
checkDeployStatus()
}
}
def boolean checkDeployStatus() {
echo "${manifestFile}"
return true
}
The exception that i am getting is below:
groovy.lang.MissingPropertyException: No such property: manifestFile for class: groovy.lang.Binding
at groovy.lang.Binding.getVariable(Binding.java:63)
How do i access variables outside the node?
Groovy has a different kind of scoping at the script level. I can't ever keep it all sorted in my head. Without trying explain all the reasons for it (and probably not doing it justice), I can tell you that (as you have seen), the manifestFile variable is not in scope in that function. Just don't declare the manifestFile (i.e. don't put def in front of it). That will make it a "global" (not really, but for your purposes here) variable, then it should be accessible in the method call.
try this
import groovy.json.*
manifestFile = "C:\\manifest.yml"
node {
stage('Build') {
}
stage('Deploy') {
checkDeployStatus()
}
}
def boolean checkDeployStatus() {
echo "${manifestFile}"
return true
}

how to run a complete Jenkins scripted pipeline stage from shared libraries src file

I recently started with Jenkins shared libraries in Jenkins pipeline.
I created a "func.groov" class and located it under "src/org/prj/func.groovy" :
package org.prj
import jenkins.model.
class func implements Serializable {
def steps
func(steps) {
this.steps = steps
}
def sh(args) {
steps.sh "echo ${args}"
}
def setLBL(CurrentNodeName,newLabelName){
jenkins.model.Jenkins.instance.slaves.each{ slave ->
if (slave.getNodeName() == CurrentNodeName){
slave.setLabelString(newLabelName)
}
}
}
Jenkinsfile (scripted) looks like:
#Library('prj') import org.prj.func
def utils = new func(steps)
node(lbl)
{
stage("A"){
Build_node_lbl = env.NODE_NAME+System.currentTimeMillis()
utils.setLBL(env.NODE_NAME,Build_node_lbl)
}
}
so currently it works. my question is how to create a full stage (like "A") as a function in func.groovy shared lib which will include, for example:
GIT checkout step
sh compilation step
Artifactory deploy step
Im actually looking to create a "building blocks" (a "Build" in my example) with Jenkins pipeline and shard libraries.
1. With Class Instantiation
You can create a class like you would do in Java. Then in your Jenkinsfile you instantiate the class and call its function.
src/org/prj/MyPipeline.groovy:
package org.prj
class MyPipeline {
def steps
MyPipeline(steps) {this.steps = steps}
public def build() {
steps.node('lbl') {
steps.stage('A') {
// Do build stuff
// steps.sh(..)
}
}
}
}
Jenkinsfile:
import org.prj.MyPipeline
def pipeline = new MyPipeline(this)
pipeline.build()
2. With Static Functions
You may also work with static contexts, without instantiation. However, this would require to hand over the caller context to the pipeline:
src/org/prj/MyPipeline.groovy:
package org.prj
class MyPipeline {
public static def build(caller) {
caller.node('lbl') {
caller.stage('A') {
// Do build stuff
caller.sh(..)
}
}
}
}
Jenkinsfile:
import org.prj.MyPipeline
MyPipeline.build(this)

How to export environment variable from Jenkins Build Flow Plugin?

From the called build flow job, I've tried both:
build.environment['AOEU'] = 'aoeu' // callee would `println called.environment['AOEU']`
and:
upstream.environment['AOEU'] = 'aoeu' // callee would `println build.environment['AOEU']`
with no luck.
I fought a lot with that too and found no clean way to do it. I finally used EnvInjectPlugin in some ugly way to do this.
def buildEnv = build.getEnvVars();
varsToAdd = [:]
// add here your custom properties
buildEnv.putAll(varsToAdd)
import org.jenkinsci.plugins.envinject.EnvInjectPluginAction
def envInjectAction = build.getAction(EnvInjectPluginAction.class);
envInjectAction.overrideAll(buildEnv)
... the EnvInject plugin did the magic
I first tried to implement EnvironmentContributingAction
and add it as build.addAction(...) but did not work for me for unknown reason.
Be sure to set 'Flow run needs a workspace' in the called job.
#NoelYap: I can't comment on Hristo's answer, but it's the correct answer. The missing detail is that importing EnvInject only works if your job has the 'Flow run needs a workspace' option selected.
A simpler version based on Michael's solution
import hudson.model.*
def setEnvVariable(final String key, final String value) {
Thread.currentThread().getCurrentExecutable()
.addAction(new ParametersAction(new StringParameterValue(key, value))
);
}
setEnvVariable("THIS_ENV_VAR", "Hello World")
The variable is then available in the post-build actions.
An old question I know, but this is how i get around it. The beautiful part is this code works well in Jenkins both within the system evaluated groovy and within the Build FLow Job DSL.
Just drop the import statements when running it from the System Groovy job/console as they are already available.
As a bonus this class also helps get the environment variable regardless of the Groovy context. Still in the build flow you should favor build.environment.get it is more natural.
Note: 'Flow run needs a workspace' should be checked.
//Import the required bindings for a Build Flow DSL
import hudson.model.AbstractBuild
import hudson.model.EnvironmentContributingAction
import hudson.EnvVars
//Drop the above if running with a System Groovy Console/Job
class Job {
static def getVariable(String key) {
def config = new HashMap()
def thr = Thread.currentThread()
def build = thr?.getCurrentExecutable()
def envVarsMap = build.parent.builds[0].properties.get("envVars")
config.putAll(envVarsMap)
return config.get(key)
}
static def setVariable(String key, String value) {
def build = Thread.currentThread().getCurrentExecutable()
def action = new VariableInjectionAction(key, value)
build.addAction(action)
build.getEnvironment()
}
static def setVariable(String key, Integer value) {
setVariable(key, value.toString())
}
}
class VariableInjectionAction implements EnvironmentContributingAction {
private String key
private String value
public VariableInjectionAction(String key, String value) {
this.key = key
this.value = value
}
public void buildEnvVars(AbstractBuild build, EnvVars envVars) {
if (envVars != null && key != null && value != null) {
envVars.put(key, value);
}
}
public String getDisplayName() { return "VariableInjectionAction"; }
public String getIconFileName() { return null; }
public String getUrlName() { return null; }
}
Job.setVariable("THIS_ENV_VAR", "Hello World")
Will set the environment variable THIS_ENV_VAR to "Hello World"

Resources