I'm using Knapsack Pro to split up unit tests and run them in parallel, and I've recently noticed that sometimes, it assigns the same node index to multiple nodes.
The build script looks like this (trimmed for brevity):
def num_nodes = 10;
def nodes = [:]
env.total_nodes = num_nodes
for (i in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) {
int index = i
nodes["ci_node_${i}"] = {
node('ec2-slave') {
...
env.new_index = index
stage('Build & run rspec') {
dir("$WORKSPACE") {
sh '''#!/bin/bash
cd $WORKSPACE
export NODE_ENV=production
export KNAPSACK_PRO_CI_NODE_TOTAL=${total_nodes}
export KNAPSACK_PRO_CI_NODE_BUILD_ID=${BUILD_TAG}
export KNAPSACK_PRO_CI_NODE_INDEX=${new_index}
export KNAPSACK_PRO_COMMIT_HASH=${commithash}
export KNAPSACK_PRO_BRANCH=${BRANCH_NAME}
export KNAPSACK_PRO_TEST_FILE_EXCLUDE_PATTERN="spec/smokes/*_spec.rb"
env
...
'''
}
}
}
}
}
try {
parallel nodes // run CI nodes in parallel
} catch (err) {
echo "Build Failed - ${err.getMessage()}"
} finally {
...
}
But when I grep the logs for all the values of KNAPSACK_PRO_CI_NODE_INDEX that get printed by env, I sometimes (but not always) see something like this:
KNAPSACK_PRO_CI_NODE_INDEX=0
KNAPSACK_PRO_CI_NODE_INDEX=2
KNAPSACK_PRO_CI_NODE_INDEX=1
KNAPSACK_PRO_CI_NODE_INDEX=5
KNAPSACK_PRO_CI_NODE_INDEX=3
KNAPSACK_PRO_CI_NODE_INDEX=8
KNAPSACK_PRO_CI_NODE_INDEX=6
KNAPSACK_PRO_CI_NODE_INDEX=6
KNAPSACK_PRO_CI_NODE_INDEX=7
KNAPSACK_PRO_CI_NODE_INDEX=9
Node 4 never gets assigned, and node 6 gets assigned twice. How is this happening?
note: That horrible hard-coded for-loop is an attempt to work around this, which may be relevant:
http://blog.freeside.co/2013/03/29/groovy-gotcha-for-loops-and-closure-scope
Passing loop variable to sh in Jenkinsfile doesn't scope correctly
Parallel jenkins job using a for loop
env.new_index = index
env is a global object that is part of the main script. env.new_index is a global property.
Example:
node {
env.VAR1 = "value"
}
println(env.VAR1)
will print
[Pipeline] Start of Pipeline
[Pipeline] node
[Pipeline] {
[Pipeline] }
[Pipeline] // node
[Pipeline] echo
value
[Pipeline] End of Pipeline
Finished: SUCCESS
If you want to define env vars per node then it's better to use withEnv
withEnv(["new_index=$index"]) {
stage('Build & run rspec') {
dir("$WORKSPACE") {
sh '''#!/bin/bash
cd $WORKSPACE
export NODE_ENV=production
export KNAPSACK_PRO_CI_NODE_TOTAL=${total_nodes}
export KNAPSACK_PRO_CI_NODE_BUILD_ID=${BUILD_TAG}
export KNAPSACK_PRO_CI_NODE_INDEX=${new_index}
export KNAPSACK_PRO_COMMIT_HASH=${commithash}
export KNAPSACK_PRO_BRANCH=${BRANCH_NAME}
export KNAPSACK_PRO_TEST_FILE_EXCLUDE_PATTERN="spec/smokes/*_spec.rb"
env
...
'''
}
}
}
Related
(edited/updated from original post to attempt to address confusion about what the problem is)
The problem is: Values that are set in a Jenkinsfile environment section are not added to the object returned by env.getEnvironment()
The question is: How do I get a map of the complete environment, including values that were assigned in the environment section? Because env.getEnvironment() doesn't do that.
Example Jenkinsfile:
pipeline {
agent any
environment {
// this is not included in env.getEnvironment()
ONE = '1'
}
stages {
stage('Init') {
steps {
script {
// this is included in env.getEnvironment()
env['TWO'] = '2'
}
}
}
stage('Test') {
steps {
script {
// get env values as a map (for passing to groovy methods)
def envObject = env.getEnvironment()
// see what env.getEnvironment() looks like
// notice ONE is not present in the output, but TWO is
// ONE is set using ONE = '1' in the environment section above
// TWO is set using env['TWO'] = '2' in the Init stage above
println envObject.toString()
// for good measure loop through the env.getEnvironment() map
// and print any value(s) named ONE or TWO
// only TWO: 2 is output
envObject.each { k,v ->
if (k == 'ONE' || k == 'TWO') {
println "${k}: ${v}"
}
}
// now show that both ONE and TWO are indeed in the environment
// by shelling out and using the env linux command
// this outputs ONE=1 and TWO=2
sh 'env | grep -E "ONE|TWO"'
}
}
}
}
}
Output (output of envObject.toString() shortened to ... except relevant part):
[Pipeline] {
[Pipeline] withEnv
[Pipeline] {
[Pipeline] stage
[Pipeline] { (Init)
[Pipeline] script
[Pipeline] {
[Pipeline] }
[Pipeline] // script
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Test)
[Pipeline] script
[Pipeline] {
[Pipeline] echo
[..., TWO:2]
[Pipeline] echo
TWO: 2
[Pipeline] sh
+ env
+ grep -E ONE|TWO
ONE=1
TWO=2
[Pipeline] }
[Pipeline] // script
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // withEnv
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Notice ONE is missing from the env.getEnvironment() object, but TWO is present.
Also notice that both ONE and TWO are set in the actual environment and I am not asking how to access the environment or how to iterate through the values returned by env.getEnvironment(). The issue is that env.getEnvironment() does not return all the values in the environment, it excludes any values that were set inside the environment section of the Jenkinsfile.
I don't have a "why" answer for you, but you can cheat and get a map by parsing the output from env via the readProperties step.
def envMap = readProperties(text: sh(script: 'env', returnStdout: true))
println(envMap.getClass())
println("${envMap}")
I would get the env and convert it to map with the help of properties
pipeline {
agent any
environment {
// this is not included in env.getEnvironment()
ONE = '1'
}
stages {
stage('Init') {
steps {
script {
// this is included in env.getEnvironment()
env['TWO'] = '2'
}
}
}
stage('Test') {
steps {
script {
def envProp = readProperties text: sh (script: "env", returnStdout: true).trim()
Map envMapFromProp = envProp as Map
echo "ONE=${envMapFromProp.ONE}\nTWO=${envMapFromProp.TWO}"
// now show that both ONE and TWO are indeed in the environment
// by shelling out and using the env linux command
// this outputs ONE=1 and TWO=2
sh 'env | grep -E "ONE|TWO"'
}
}
}
}
}
Output of env.getEnvironment() method will not return a list or Map, Hence it's difficult to iterate with each but there are some workaround you can do to make this work.
import groovy.json.JsonSlurper
pipeline {
agent any;
environment {
ONE = 1
TWO = 2
}
stages {
stage('debug') {
steps {
script {
def jsonSlurper = new JsonSlurper()
def object = jsonSlurper.parseText(env.getEnvironment().toString())
assert object instanceof Map
object.each { k,v ->
echo "Key: ${k}, Value: ${v}"
}
}
}
}
}
}
Note - env.getEnvironment().toString() will give you a JSON String . While parsing the JOSN string if groovy jsonSlurper.parseText found any special character it will through an error
You can also explore a little bit around env Jenkins API and find an appropriate method that will either return a Map or List so that you can use each
I've inherited some Jenkins pipeline and try to improve it. Jenkins and groovy is quite fresh topic for me, so most probably I'm doing something wrong.
I'm using Jenkins ver. 2.121.3
Main aim was to add build parameter to do some extra cleaning during build. So I've added parameter CLEAN_FIRST with Boolean type and default value false to a job configuration and did something like this in pipeline:
// CLEAN_FIRST = false
// def prefix = CLEAN_FIRST ? "" : "REM"
pipeline {
agent none
stages {
stage('Some step') {
steps {
script {
node('master') {
cleanWs()
try {
def prefix = CLEAN_FIRST ? "" : "REM"
echo "CLEAN_FIRST=$CLEAN_FIRST prefix=$prefix"
bat (label: 'build third party',
script: """
$prefix call cleanSomthing.bat
call doOtherStuff.bat
"""
} finally {
echo "some stuff"
}
} // node
} // script
} // steps
} // stage
} // stages
} // pipeline
Now this doesn't work as expected. "REM" prefix is not added.
Echo prints:
CLEAN_FIRST=false prefix=
And bat invokes cleanSomthing.bat which I wish to avoid (to save on build times).
I've tried to make prefix global, but with same result.
Most probably this is caused by some evaluation order or scoping issue, but I can't put finger on it.
Can someone give me a clue why it doesn't work? How to fix it?
Answered own question. Is this problem fixed on some version of Jenkins?
replace
def prefix = CLEAN_FIRST ? "" : "REM"
with
def prefix = params.CLEAN_FIRST ? "" : "REM"
Ok I've found source of problems. It is a bit funny.
When running this pipeline (tested on Mac machine since it had empty job queue):
pipeline {
agent none
stages {
stage('Some step') {
steps {
script {
node('Mac') {
cleanWs()
try {
def logic = true
def prefix = CLEAN_FIRST ? "Ole" : "REM"
def typeLogic = logic.getClass()
def typeParam = CLEAN_FIRST.getClass()
echo "typeLogic=$typeLogic typeParam=$typeParam"
echo "CLEAN_FIRST=$CLEAN_FIRST prefix=$prefix"
sh (script: """
echo prefix=$prefix
""")
} finally {
echo "some stuff"
}
} // node
} // script
} // steps
} // stage
} // stages
} // pipeline
I've got this outcome:
Running in Durability level: MAX_SURVIVABILITY
[Pipeline] stage
[Pipeline] { (Some step)
[Pipeline] script
[Pipeline] {
[Pipeline] node
Running on master in /Users/builder/jenkins/workspace/EIbuild_MacOS
[Pipeline] {
[Pipeline] cleanWs
[WS-CLEANUP] Deleting project workspace...[WS-CLEANUP] done
[Pipeline] echo
typeLogic=class java.lang.Boolean typeParam=class java.lang.String
[Pipeline] echo
CLEAN_FIRST=false prefix=Ole
[Pipeline] sh
[EIbuild_MacOS] Running shell script
+ echo prefix=Ole
prefix=Ole
[Pipeline] echo
some stuff
[Pipeline] }
[Pipeline] // node
[Pipeline] }
[Pipeline] // script
[Pipeline] }
[Pipeline] // stage
[Pipeline] End of Pipeline
Finished: SUCCESS
So now source the problem is obvious.
Jenkins in configuration promises variable of type Boolean, but in fact provides type String with values are "true" or "false" which are always evaluated as true when used as condition since both values are not empty strings :).
The output of this python eval looks like it could be stages in a jenkins pipeline
$ python3 -c 'print("\n".join(["stage({val}) {{ do something with {val} }}".format(val=i) for i in range(3)]))'
stage(0) { do something with 0 }
stage(1) { do something with 1 }
stage(2) { do something with 2 }
Is it possible for jenkins to use output like this to create steps or stages in a pipeline so the running python script is able to update jenkins ? The point of this would be to have Blue Ocean pipeline have a stage dot that was made by an external script running separate jobs.
To elaborate on the example ... if this demo.py script which outputs the uptime in a stage
#!/bin/env python3.6
import subprocess, time
def uptime():
return (subprocess.run('uptime', stdout=subprocess.PIPE, encoding='utf8')).stdout.strip()
for i in range(3):
print("stage({val}) {{\n echo \"{output}\" \n}}".format(val=i, output=uptime()))
time.sleep(1)
where to be setup in a jenkins pipeline
node {
stage("start demo"){
sh "/tmp/demo.py"
}
}
As is this demo just outputs the text and does not create any stages in blue ocean
[Pipeline] sh
+ /tmp/demo.py
stage(0) {
echo "03:17:16 up 182 days, 12:17, 8 users, load average: 0.00, 0.03, 0.05"
}
stage(1) {
echo "03:17:17 up 182 days, 12:17, 8 users, load average: 0.00, 0.03, 0.05"
}
stage(2) {
echo "03:17:18 up 182 days, 12:17, 8 users, load average: 0.00, 0.03, 0.05"
}
Again the point of this would be to have Blue Ocean pipeline have a stage dot with a log
You can evaluate an expression and then call it.
node(''){
Closure x = evaluate("{it -> evaluate(it)}" )
x(" stage('test'){ script { echo 'hi'}}")
}
Since Jenkins converts your Groovy script into Java, compiles it and then executes the result, it would be quite hard to use an external program to generate more Groovy to execute, since that additional groovy code would need to be converted. But the generated code is a result of running, which means that the conversion is already done.
Instead, you may want to programmatically build your stages in Groovy.
some_array = ["/tmp/demo.py", "sleep 10", "uptime"]
def getBuilders()
{
def builders = [:]
some_array.eachWithIndex { it, index ->
// name the stage
def name = 'Stage #' + (index + 1)
builders[name] = {
stage (name) {
def my_label = "jenkins_label" // can choose programmatically if needed
node(my_label) {
try {
doSomething(it)
}
catch (err) { println "Failed to run ${it}"; throw err }
finally { }
}
}
}
};
return builders
}
def doSomething(something) {
sh "${something}"
}
And later in your main pipeline
stage('Do it all') {
steps {
script {
def builders = getBuilders()
parallel builders
}
}
This will run three parallel stages, where one would be running /tmp/demo.py, the second sleep 10, and the third uptime.
I have defined global variable in Jenkins pipeline
def BUILDNRO = '0'
pipeline { ...
Then i manipulate variable with shell script to enable running builds parallel by using job build number as identifier so we don't mix different docker swarms.
stage('Handle BUILD_NUMBER') {
steps {
script {
BUILDNRO = sh( script: '''#!/bin/bash
Build=`echo ${BUILD_NUMBER} | grep -o '..$'`
# Check if BUILD first character is 0
if [[ $Build:0:1 == "0" ]]; then
# replace BUILD first character from 0 to 5
Build=`echo $Build | sed s/./5/1`
fi
echo $Build
''',returnStdout: true).trim()
}
}
}
i get value out from previos stage and trying to get global variable on next stage
stage('DOCKER: Init docker swarm') {
steps {
echo "BUILDNRO is: ${BUILDNRO}" --> Value is here.
sh '''#!/bin/bash
echo Buildnro is: ${BUILDNRO} --> This is empty.
...
}
}
This will out give global variable empty. why? in previous stage there was value in it.
EDIT 1.
Modified code blocks to reflect current status.
I managed to figure it out. Here is solution how i managed to did it.
BUILDNRO is groovy variable and if wanting to used in bash variable it have to pass using withEnv. BUILD_NUMBER in first stage is bash variable hence it can be used directly script in first stage.
def BUILDNRO = '0'
pipeline {
....
stages {
stage('Handle BUILD_NUMBER') {
steps {
script {
BUILDNRO = sh( script: '''#!/bin/bash
Build=`echo ${BUILD_NUMBER} | grep -o '..$'`
''',returnStdout: true).trim()
}
}
}
stage('DOCKER: Init docker swarm') {
steps {
dir("prose_env/prose_api_dev_env") {
withEnv(["MYNRO=${BUILDNRO}"]) {
sh(returnStdout: false, script: '''#!/bin/bash
echo Buildnro is: ${MYNRO}`
'''.stripIndent())
}
}
}
}
}
}
If you are using single quotes(```) in the shell module, Jenkins treats every variable as a bash variable. The solution is using double quotes(""") but then if you made bash variable you have to escape it. Below an example with working your use case and escaped bash variable
pipeline {
agent any
stages {
stage('Handle BUILD_NUMBER') {
steps {
script {
BUILDNRO = sh(script: 'pwd', returnStdout: true).trim()
echo "BUILDNRO is: ${BUILDNRO}"
}
}
}
stage('DOCKER: Init docker swarm') {
steps {
sh """#!/bin/bash
echo Buildnro is: ${BUILDNRO}
variable=world
echo "hello \${variable}"
sh """
}
}
}
}
output of the second stage:
Buildnro is: /var/lib/jenkins/workspace/stack1
hello world
Despite following this answer and others, I am unable to successfully use a local groovy file in my Jenkinsfile (both are in the same repository).
def deployer = null
...
...
...
pipeline {
agent {
label 'cf_slave'
}
options {
skipDefaultCheckout()
disableConcurrentBuilds()
}
stages {
stage ("Checkout SCM") {
steps {
checkout scm
}
}
...
...
...
stage ("Publish CF app") {
steps {
script {
STAGE_NAME = "Publish CF app"
deployer = fileLoader.load ('deployer')
withCredentials(...) {
if (BRANCH_NAME == "develop") {
...
...
...
} else {
deployer.generateManifest()
}
}
}
}
}
...
...
}
deployer.groovy:
#!/usr/bin/env groovy
def generateManifest() {
sh "..."
echo "..."
}
In the console log (stack):
[Pipeline] stage
[Pipeline] { (Publish CF app)
[Pipeline] script
[Pipeline] {
[Pipeline] echo
before loading groovy file
[Pipeline] echo
Loading from deployer.groovy
[Pipeline] load
[Pipeline] // load
[Pipeline] }
[Pipeline] // script
[Pipeline] }
[Pipeline] // stage
Update:
It seems the problem was not with loading the file but rather with the contents of the file, where I execute the following which apparently does not play well:
sh "node $(pwd)/config/mustacher manifest.template.yml config/environments/common.json config/environments/someFile.json"
echo "..."
When only the echo is there, this is the stack.
So not the sh "node ..." nor the echo work. Even changing it just to sh "pwd" fails as well. What could it be? the syntax in the file? the way it is called in the pipeline?
If I will make the same node call in the pipeline (for example in the withCredentials if statement, it works.
Add a return this to the bottom of the deployer.groovy file, and then change you load step to use relative path and extension to groovy file like load('deployer.groovy').
The return this is documented on jenkins.io:
Takes a filename in the workspace and runs it as Groovy source text.
The loaded file can contain statements at top level or just load and run a closure. For example:
def pipeline
node('slave') {
pipeline = load 'pipeline.groovy'
pipeline.functionA()
}
pipeline.functionB()
pipeline.groovy
def pipelineMethod() {
...code
}
return this
Where pipeline.groovy defines functionA and functionB functions (among others) before ending with return this