9

My project has many common variables for many other projects, so I use Jenkins Shared Library and created a vars/my_vars.groovy file where I defined my variables and return Map of them:

class my_vars {
    static Map varMap = [:]
    static def loadVars (Map config) {
        varMap.var1 = "val1"
        varMap.var2 = "val2"
        // Many more variables ...

        return varMap
    }
}

I load the Shared Library in my Jenkinsfile, and call the function in the environment bullet, as I want those variables to be as environment variables .

Jenkinsfile:

pipeline {

    environment {
        // initialize common vars
        common_vars = my_vars.loadVars()
    } // environment

    stages {
        stage('Some Stage') {
            // ...
        }
    }

    post {
        always { 
            script {
                // Print environment variables
                sh "env"
            } // script
        } // always
    } // post

} // pipeline

The thing is that the environment bullet gets KEY=VALUE pairs, thus my common_vars map is loaded like a String value (I can see that on sh "env").

...
vars=[var1:val1, var2:val2]
...

What is the correct way to declare those values as an environment variables? My target to get this:

...
var1=val1
var2=val2
...
Daniel Juravski
  • 181
  • 1
  • 2
  • 12

3 Answers3

9

Pipeline's environment variables store only String values. That is why when you assign a map to env.common_vars variables it stores map.toString() equivalent.

If you want to rewrite key-values from a map to the environment variables, you can iterate the variables map and assign each k-v pair to something like env."$k" = v. You can do that by calling a class method inside the environment block - that way you can be sure that the environment variables are assigned no matter which stage your pipeline gets restarted from. Consider the following example:

class MyVars {
    private Map config = [
        var1: "val1",
        var2: "val2"
    ]

    String initializeEnvironmentVariables(final Script script) {
        config.each { k,v ->
            script.env."$k" = v
        }

        return "Initialization of env variables completed!"
    }
}

pipeline {
    agent any

    environment {
        INITIALIZE_ENV_VARIABLES_FROM_MAP = "${new MyVars().initializeEnvironmentVariables(this)}"
    }

    stages {
        stage("Some stage") {
            steps {
                echo "env.var1 = ${env.var1}"
            }
        }
    }

    post {
        always {
            script {
                sh 'printenv | grep "var[0-9]\\+"'
            }
        }
    }
}

In this example, we use MyVars class to store some global config map (it can be a part of a shared library, here, for simplicity, it is a part of the Jenkinsfile). We use INITIALIZE_ENV_VARIABLES_FROM_MAP environment variable assignment to call MyVars.initializeEnvironmentVariables(this) method that can access env from the script parameter. Calling this method from inside environment block has one significant benefit - it guarantees that environment variables will be initialized even if you restart the pipeline from any stage.

And here is the output of this exemplary pipeline:

Running on Jenkins in /home/wololock/.jenkins/workspace/pipeline-env-map
[Pipeline] {
[Pipeline] withEnv
[Pipeline] {
[Pipeline] stage
[Pipeline] { (Some stage)
[Pipeline] echo
env.var1 = val1
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Declarative: Post Actions)
[Pipeline] script
[Pipeline] {
[Pipeline] sh
+ grep 'var[0-9]\+'
+ printenv
var1=val1
var2=val2
[Pipeline] }
[Pipeline] // script
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // withEnv
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS

As you can see we it sets env.var1 and env.var2 from the map encapsulated in MyVars class. Both variables can be accessed inside the pipeline step, script block or even inside the shell environment variables.

Szymon Stepniak
  • 40,216
  • 10
  • 104
  • 131
  • 1
    This is a really clever and clean manipulation of the `env` map. – Matthew Schuchard Nov 04 '19 at 20:36
  • That elegant solutions works **only** when the `MyVars` class is in the same `Jenkinsfile` where is the pipeline deceleration. As I said I use shared library named `MySharedLibrary`, what is the solution for that case? Many thanks! – Daniel Juravski Nov 06 '19 at 07:15
  • @DanielJuravski, that was just simplification of that example. The `MyVars` class can be a part of the shared library, e.g. `src/com/example/MyVars.groovy`. The only difference compared to that example is that you need to import it (`import com.example.MyVars`) and it will work the same. – Szymon Stepniak Nov 06 '19 at 08:14
  • @Szymon Stepniak, as I new to the shared library stuff I will ask, can the MyVars.groovy be in vars/ dir? Or class impl. must be at src/ dir and imported from src/? In other words, what can be a solution for making what I want in 1 line? (except import line and new class line) Thanks! – Daniel Juravski Nov 06 '19 at 14:12
  • The `vars/` folder is designed to store scripts that can be called e.g. as steps. The `src/` folder is designed to store classes used by your shared library. I've never tried storing classes in `vars/`, I store pipeline custom steps/scripts only. Those scripts and steps use classes from the `src/` folder. I guess you can put a class in the `vars/` folder. It's up to you. – Szymon Stepniak Nov 06 '19 at 14:17
  • Any chance to extend this great solution to support the credentials helper function? I would like to inject credentials and pass them to the environment block but somehow fail... – Martin Feb 14 '20 at 10:43
3

As far as I know there is no easy way to do this in declarative pipeline (e.g. in the environment directive. Instead, what you can do is to setup the environment outside of the declarative definition, like this:

my_vars.loadVars().each { key, value ->
    env[key] = value
}
// Followed by your pipelines definition:
pipeline {
    stages {
        stage('Some Stage') {
            // ...
        }
    }
    // ...
} // pipeline

As an full example:

class my_vars {
    static Map varMap = [:]
    static def loadVars (Map config) {
        varMap.var1 = "val1"
        varMap.var2 = "val2"
        // Many more variables ...

        return varMap
    }
}

my_vars.loadVars().each { key, value ->
    env[key] = value
}

pipeline {
    agent any
    stages {
        stage("Some stage") {
            steps {
                echo "env.var1 = ${env.var1}"
            }
        }
    }
}

Which outputs the following when built:

Started by user xxx
Running in Durability level: MAX_SURVIVABILITY
[Pipeline] Start of Pipeline
[Pipeline] node
Running on yyy in /zzz
[Pipeline] {
[Pipeline] stage
[Pipeline] { (Some stage)
[Pipeline] echo
env.var1 = val1
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS

Edit; If your class (my_vars) is located in a shared library (MySharedLibrary):

library 'MySharedLibrary' // Will load vars/my_vars.groovy
my_vars.loadVars().each { key, value ->
    env[key] = value
}

pipeline {
    agent any
    stages {
        stage("Some stage") {
            steps {
                echo "env.var1 = ${env.var1}"
            }
        }
    }
}
Jon S
  • 15,846
  • 4
  • 44
  • 45
  • That elegant solutions works only when the `MyVars` class is in the same `Jenkinsfile` where is the pipeline deceleration. As I said I use shared library named `MySharedLibrary`, what is the solution for that case? Many thanks! – Daniel Juravski Nov 06 '19 at 08:32
  • @DanielJuravski I've added an example. Basically, load the library and then you can access it as before. – Jon S Nov 06 '19 at 09:42
  • that is really approaching to what I need but, I didn't mention that I need to initialize the the `loadVars()` with some other values. Those other values are initialized in the `environment {}` bullet. Therefore, where should I set the `my_vars.loadVars()` fun? (I do not want to make it on a unique stage). (I thought about initialize those some others values by calling them at the same groovy class as this one, but I ran into a problem of calling var class out of other var class. Do you have a solution for this?) Thanks! – Daniel Juravski Nov 18 '19 at 15:22
0

You don't have to return a map of your environment variables from your shared library. You can simply set them in a shared library method, the method will run in the same container as your pipeline.

In you shared library vars/ directory:

def setVars() {
    env.var1 = "var1"
    env.var2 = "var2"
    env.var3 = "var3"
}

In your pipeline:

pipeline {
    agent any
    stages {
        stage("Setup") {
            steps {
                script {
                    imported_shared_lib.setVars()
                }
            }
        }
    }
}

Others mentioned the need to preserve the environment variables even if you restart the pipeline from a certain stage. In my experiments, the variables are preserved using this method, even if the setVars() method is not called in the environment{} block.

Kuranes
  • 62
  • 1
  • 7