Scripted Jenkinsfile executes scheduling on wrong nodes - parallel-processing

The system/setup
Here is my setup:
1 machine running Jenkins which also provides the built-in node (former master). It has 4 workers assigned to it
1 FreeBSD machine as remote SSH node with the name FreeBSD 13.0 x86_64. It has 1 worker
1 Ubuntu machine as remote SSH node with the name Ubuntu 20.04 x86_64. It has 1 worker
I would like to have a sequence of Checkout, Build, Test and Upload steps executed serially on each of those FreeBSD 13.0 x86_64 and Ubuntu 20.04 x86_64 nodes. However, I would like each of those nodes to work independently on their list of serial tasks. Something like this:
For this, I have created the following scripted Jenkinsfile
String[] nodesNames = [
'FreeBSD 13.0 x86_64',
'Ubuntu 20.04 x86_64'
]
Map builders = [:]
for (nodeName in nodesNames) {
builders[nodeName] = {
node(nodeName) {
stage('Checkout') {
sh 'sleep 5'
}
stage('Build') {
sh 'sleep 10'
}
stage('Test') {
sh 'sleep 15'
}
stage('Upload') {
sh 'sleep 20'
}
}
}
}
parallel builders
The problem
Jenkins will need an executor to execute the Jenkinsfile itself, which is scheduling the real work. Again, not the work, but the scheduling of the work. And it picks one up from the nodes like this:
Running on Ubuntu 20.04 x86_64 in /home/jenkins/workspace/test
That is the problem: it picks up the wrong node. That node has one executor, and it should never run the scheduling. Scheduling should be done on the built-in node. This will later result in the following message:
Still waiting to schedule task
Waiting for next available executor on ‘Ubuntu 20.04 x86_64’
The outcome
Because one of the 1-worker-node is doing both scheduling and actual work, we will end up with a seemengly "parallel" execution but in fact, is as serial as it can be. Here is a picture taken in the middle of the whole process. Notice how the FreeBSD machine is alternatively doing work then scheduling. As it happens, is giving work to itself. When the work for itself is finished, it will start giving work to Ubuntu.
The solution?
How can one tell Jenkins to execute the Jenkins file itself (the scheduling part) on the built-in node (the former master) and not use a precious worker from the actual remote nodes?
The non-maintainable solution
(Update after initial question and as a response for #MaratC)
We can use declarative syntax. However, it has a major/crippling flaw: imagine one needs to add another machine. It will basically repeat a lot of code. After the 4th-5th machine, it becomes unmaintainable.
pipeline {
agent { node('built-in') }
stages {
stage('Build all') {
parallel {
stage('FreeBSD') {
agent { node('FreeBSD 13.0 x86_64') }
stages {
stage('Checkout') {
steps { sh 'sleep 5' }
}
stage('Build') {
steps { sh 'sleep 10' }
}
stage('Test') {
steps { sh 'sleep 15' }
}
stage('Upload') {
steps { sh 'sleep 20' }
}
}
}
stage('Ubuntu') {
agent { node('Ubuntu 20.04 x86_64') }
stages {
stage('Checkout') {
steps { sh 'sleep 5' }
}
stage('Build') {
steps { sh 'sleep 10' }
}
stage('Test') {
steps { sh 'sleep 15' }
}
stage('Upload') {
steps { sh 'sleep 20' }
}
}
}
}
}
}
}

First of all, you can combine declarative and scripted syntax (note I didn't check the following code):
pipeline {
agent { node('built-in') }
stages {
stage('Build all') {
script {
def myBuilders = getParallelBuilders()
parallel myBuilders
}
}
}
}
def getParallelBuilders() {
String[] nodesNames = [
'FreeBSD 13.0 x86_64',
'Ubuntu 20.04 x86_64'
]
Map builders = [:].asSynchronized() // don't ask
for (nodeName in nodesNames) {
def final_name = nodeName // don't ask
builders[final_name] = {
node(final_name) {
stage('Checkout') {
sh 'sleep 5'
}
stage('Build') {
sh 'sleep 10'
}
stage('Test') {
sh 'sleep 15'
}
stage('Upload') {
sh 'sleep 20'
}
}
}
return builders
}
But I think that your problem may be solved faster by disallowing the planning code to run on Ubuntu and FreeBSD nodes, by configuring these nodes to only run what is planned to run on the labels (and not just everything). This is achieved by selecting "Only build jobs with label expressions matching this node" in the node configuration screen.

The solution to original scripted Jenkinsfile question
String[] nodesNames = [
'FreeBSD 13.0 x86_64',
'Ubuntu 20.04 x86_64'
]
def tasks = [:]
def createTask(tasks, title, nodeName) {
tasks[title] = {
stage (title) {
node(nodeName) {
stage('Checkout') {
sh 'hostname; sleep 1'
}
stage('Build') {
sh 'hostname; sleep 2'
}
stage('Test') {
sh 'hostname; sleep 3'
}
stage('Upload') {
sh 'hostname; sleep 4'
}
}
}
}
}
for (nodeName in nodesNames) {
createTask(tasks, nodeName, nodeName)
}
node('built-in') {
parallel tasks
}
It builds everything as expected:
Solution proposed by #MaratC:
(Thank you for the suggestion)
It was slight/minor changed to make it work, and it looks like this:
pipeline {
agent { node('built-in') }
stages {
stage('Build all') {
steps {
script {
def myBuilders = getParallelBuilders()
parallel myBuilders
}
}
}
}
}
def getParallelBuilders() {
String[] nodesNames = [
'FreeBSD 13.0 x86_64',
'Ubuntu 20.04 x86_64'
]
//org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: //
// Scripts not permitted to use staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods asSynchronized java.util.Map
Map builders = [:]//.asSynchronized() // don't ask:
for (nodeName in nodesNames) {
def final_name = nodeName // don't ask
builders[final_name] = {
node(final_name) {
stage('Checkout') {
sh 'hostname; sleep 1'
}
stage('Build') {
sh 'hostname; sleep 2'
}
stage('Test') {
sh 'hostname; sleep 3'
}
stage('Upload') {
sh 'hostname; sleep 4'
}
}
}
}
return builders
}
And the behavior:
Notice that the Checkout step is executed, and the only step is listed in the graph. However, notice the blue ongoing task below. Something is off.

Related

Parallel pipeline with 2 inline stages

Playing with Jenkins pipeline from https://www.jenkins.io/doc/pipeline/examples/#parallel-multiple-nodes
Simple two parallel steps (OK)
I made a first test pipeline this way:
pipeline {
stages {
stage('Build') {
steps {
script {
def labels = ['precise', 'trusty'] // labels for Jenkins node types we will build on
def builders = [:]
for (x in labels) {
def label = x // Need to bind the label variable before the closure - can't do 'for (label in labels)'
// Create a map to pass in to the 'parallel' step so we can fire all the builds at once
builders[label] = {
node('JenkinsNode') {
sh script: 'echo build', label: 'Build on $env.NODE_NAME'
}
}
}
parallel builders
}
}
}
}
}
It resulted in the following expected diagram in Blue Ocean view:
Simple two parallel steps with two sub steps each (KO)
Attempt#1
Then I tried to split each parallel step in two inline stages (to simulate build and tests for example)
pipeline {
stages {
stage('Build') {
steps {
script {
def labels = ['precise', 'trusty'] // labels for Jenkins node types we will build on
def builders = [:]
for (x in labels) {
def label = x // Need to bind the label variable before the closure - can't do 'for (label in labels)'
// Create a map to pass in to the 'parallel' step so we can fire all the builds at once
builders[label] = {
node('JenkinsNode') {
stage("build") {
sh script: 'echo build', label: 'Build on $env.NODE_NAME'
}
stage("test") {
sh script: 'echo run unit tests', label: 'Run unit tests on $env.NODE_NAME'
}
}
}
}
parallel builders
}
}
}
}
}
The Jenkins logs show both build and test stages are run for each parallel step, but the Blue Ocean view only states build stage:
I would expect something like:
I'm not very clear about the boundaries between declarative and scripted pipelines, but I suspect a misunderstanding around this.
Attempt#2
Following a suggestion in comments, I slightly changed the code to have sub-stages unique names (build1, test1, build2, test2) and it does not change the diagram. I still have build steps only.
Here are the Jenkins logs in this case:
Question: Is the pipeline invalid (leading to only "build" sub-steps instead of build + test sub-steps) or is it a limitation of Blue Ocean (1.25.3)?
When combining declarative and scripted syntax things become a bit tricky.
In this specific combination case, to make it work like you expect you must create an encapsulating stage for each parallel execution code that has the same name as the parallel branch.
This will cause the blue ocean to display the inner stages as requested.
pipeline {
agent any
stages {
stage('Build') {
steps {
script {
def labels = ['precise', 'trusty']
def builders = [:]
for (x in labels) {
def label = x
builders[label] = {
stage(label) { // Encapsulating stage with same name as parallel branch
node('JenkinsNode') {
stage("build") {
sh script: 'echo build', label: 'Build on $env.NODE_NAME'
}
stage("test") {
sh script: 'echo run unit tests', label: 'Run unit tests on $env.NODE_NAME'
}
}
}
}
}
parallel builders
}
}
}
}
}
Or in a more Groovy way:
pipeline {
agent any
stages {
stage('Build') {
steps {
script {
def labels = ['precise', 'trusty']
def builders = labels.collectEntries {
["${it}" : {
stage(it) { // Encapsulating stage with same name as parallel branch
node('JenkinsNode') {
stage("build") {
sh script: 'echo build', label: 'Build on $env.NODE_NAME'
}
stage("test") {
sh script: 'echo run unit tests', label: 'Run unit tests on $env.NODE_NAME'
}
}
}
}]
}
parallel builders
}
}
}
}
}
The result:

Jenkins pipelines with parallel and different containers

So I am already running Jenkins pipelines with parallel base on the example from: Is it possible to create parallel Jenkins Declarative Pipeline stages in a loop?
I want to run each job in different isolated container, the agent name should be the same to all of them. Tried a few options all of them ended up withe errors, I think I need to use both declarative and scripted but not sure how.
Things I tired:
def generateTerraformStage(env) {
return {
agent { label 'local_terraform' }
stage("stage: Terraform ${TERRAFORM_ACTION} ${env}") {
echo "${env}"
sleep 30
}
}
}
stage('parallel stages') {
agent { label 'local_terraform' }
steps {
script {
parallel parallelStagesMapEnvironment
}
}
}
One of the errors I got during testing:
"java.lang.NoSuchMethodError: No such DSL method 'agent' found among steps" and "java.lang.IllegalArgumentException: Expected named arguments but got org.jenkinsci.plugins.workflow.cps.CpsClosure2#560f3533"
Dynamic parallel stages could be created only by using Scripted Pipelines. The API built-it Declarative Pipeline is not available (like agent, options, when etc.).
I don't see any information that you really need dynamic stages (e.g. based on the value returned by a 3rd-party service), so I prepared two solutions:
dynamic parallel stages - stages are generated based on something
static parallel stages - you know all stages (the when block could be used to disable these which are not needed - e.g. passed in parameters)
pipeline {
// ...
stages {
stage('dynamic parallel stages') {
steps {
script {
// params.ENVS == ['envA', 'envB', 'envC']
def values = params.ENVS.split(',')
def stages = [:]
for (def value in values) {
stages[value] = generateTerraformStage(value)
}
parallel stages
}
}
}
stage('static parallel stages') {
parallel {
stage('envA') {
agent { label 'local_terraform' }
when {
expression { return params.ENVS.split(',').contains('envA') }
}
steps {
terraformStageLogic 'envA'
}
}
stage('envB') {
agent { label 'local_terraform' }
when {
expression { return params.ENVS.split(',').contains('envB') }
}
steps {
terraformStageLogic 'envB'
}
}
stage('envC') {
agent { label 'local_terraform' }
when {
expression { return params.ENVS.split(',').contains('envC') }
}
steps {
terraformStageLogic 'envC'
}
}
// ...
}
}
}
}
Closure<Void> generateTerraformStage(env) {
return {
node('local_terraform') {
stage("stage: Terraform ${TERRAFORM_ACTION} ${env}") {
echo "${env}"
sleep 30
}
}
}
}
void terraformStageLogic(env) {
echo "${env}"
sleep 30
}
When you don't use the workspace in the stage responsible for generating or executing other stages (dynamic parallel stages and static parallel stages) then you don't need to allocate any node to it (waste of resources).

Jenkins pipeline multiline script command as variable

how can I save a command in a variable and executed anywhere in the stage
tried differnt way, but still success
here is my example
pipeline {
agent any
environment {
myscript = sh '''
echo "hello"
echo "hello"
echo "hello"
'''
}
stages {
stage("RUN") {
steps {
sh "${myscript}"
}
}
}
}
you can do it like this. Not with a groovy variable but can be more dynamic with groovy function/method
def reusableScript(message) {
sh """
echo Hello World
echo Hi ${message}
"""
}
pipeline {
agent any;
stages {
stage('01') {
steps {
script {
reusableScript("From ${env.STAGE_NAME}")
}
}
}
stage('02') {
steps {
script {
reusableScript("From ${env.STAGE_NAME}")
}
}
}
}
}

Fail Jenkins pipeline stage if some file contains specific strings

I need to fail one Jenkins pipeline stage when one file contains 'errors'
I do not know how to return an error from bash to Jenkins
stage('check if file continas error and exit if true') {
steps {
sh "grep 'error' filetocheck.txt"
}
}
}
reference Is it possible to capture the stdout from the sh DSL command in the pipeline
This worked for me,
def runShell(String command){
def responseCode = sh returnStatus: true, script: "${command} &> tmp.txt"
def output = readFile(file: "tmp.txt")
return (output != "")
}
pipeline {
agent any
stages {
stage('check shellcheck') {
steps {
script {
if (runShell('grep \'error\' file_to_parse.txt')) {
sh "exit 1"
}
}
}
}
}
}
you can try using String.count(charSequence) where String could be a file or string.
def file = 'path/to/file.txt'
if ( file.count('error') > 0 )
return stageResultMap.didB2Succeed = false

How to return environment values in jenkins declarative pipeline to a file?

I have a Jenkinsfile as below and
i am passing environment variables per stage,
want to create a docker.env file using these environment variables,
so i can use this docker.env file as part of my docker run 'docker run -d --env-file=docker.env java' per stage
looking for a <command_to_copy_all_values_under_'environment'_to_file_'docker.env'> ???
Jenkinsfile:
pipeline {
agent any
stages {
stage('Staging') {
environment {
KEY1_1=VALUE1_1
KEY1_2=VALUE1_2
KEY2_1=VALUE2_1
KEY2_2=VALUE2_2
}
steps {
timestamps() {
deleteDir()
sh '''
<command_to_copy_all_values_under_above_'environment'_to_file_'docker.env'>
docker run -d --env-file=docker.env java
'''
}
}
}
stage('Production') {
when {
branch 'release'
}
environment {
KEY1_1=VALUE1_1
KEY1_2=VALUE1_2
KEY2_1=VALUE2_1
KEY2_2=VALUE2_2
}
steps {
timestamps() {
timeout(time: 30, unit: 'MINUTES') {
input 'Ready to deploy to PRODUCTION, click PROCEED or ABORT ?'
}
deleteDir()
sh '''
<command_to_copy_all_values_under_above_'environment'_to_file_'docker.env'>
docker run -d --env-file=docker.env java
'''
}
}
}
}
tools {
maven 'apache_maven_352'
}
environment {
KEY1=VALUE1
KEY2=VALUE2
KEY3=VALUE3
KEY4=VALUE4
}
}
Here's solution with AWK:
awk -v B1="block-2" -v B2="block-Y" '/^[^ \t]/ { main=$1; next } main==B1 && $1==B2 { fnd=1; next } $1=="}" { fnd=0 } fnd && $0~/=/ { gsub(/^[ \t]+/,""); print $0 }' YOURFILE
Output:
key2_1=value2_1
key2_2=value2_2

Resources