How does Jenkins choose a pipeline agent between multiple labels? - jenkins

I have a Jenkins pipeline that I'd like to run on either a params-specified agent or master. The pipeline code that implements this is:
pipeline {
agent { label "${params.agent} || master" }
...
}
I've surmised, from the following posts, that the || operator needs to be inside the (double-)quotes:
Can I define multiple agent labels in a declarative Jenkins Pipeline?
https://serverfault.com/questions/1074089/how-to-apply-multiple-labels-to-jenkins-nodes
Jenkinsfile - agent matching multiple labels
When I run this job, it seems to always run on master.
When I switch the order of ${params.agent} and master in the agent statement, it seems to still always run on master.
If I remove " || master" from the agent statement, then the job runs on the params-specified agent.
Question: Is my observation that Jenkins "prefers" master a coincidence, or is there something wrong with the syntax that's making Jenkins default to master?
Is there some way to have Jenkins prefer not-master so that I can test validity of the agent statement?

So, when Jenkins encounters the line
agent { label "${params.agent} || master" }
it will do exactly one of the following:
schedule your job on one of the nodes that match that label; or
get stuck until there's a node that matches that label, or until aborted.
With regards to option 1, there's no guarantee that it will do a round-robin, a random choice, or prefer some nodes but not the others, etc. In practice, when several nodes match, Jenkins will prefer the node that ran your pipeline in the past. This is a reasonable behavior — if there's a workspace already on that node, some operations (like git checkout) may happen faster, saving time.
With regards to option 2, that's also a reasonable behavior. We actually use that to schedule a job on a non-existing label, while manipulating the labels to produce one that would match.
So, there's nothing wrong with your syntax, and Jenkins is behaving as designed.
If you want to implement some custom rule — like "always try a different node", or "try to use master as little as possible" — you have to code that.
Example pipeline (note I haven't checked it):
import hudson.model.Hudson
properties([
parameters([
string(name: 'DEPLOY_ON', defaultValue: 'node_name',
description: 'try to run on this node, or master'),
])
])
resulting_node_name = ''
pipeline {
agent { node { label 'master' } }
stages {
stage ('Do on master') {
steps {
script {
resulting_node_name = params.DEPLOY_ON
// note: this gets node by name, but you can get by label if you wish
def slave = Jenkins.instance.getNode(resulting_node_name)
if (slave == null) {
currentBuild.result = 'FAILURE'
}
def computer = slave.computer
if (computer == null || computer.getChannel() == null || slave.name != params.DEPLOY_ON) {
println "Something wrong with the slave object, setting master"
resulting_node_name = 'master'
}
printSlaveInfo(slave)
computer = null
slave = null
}
}
}
stage('Do on actual node') {
agent { node { label resulting_node_name } }
steps {
script {
println "Running on ${env.NODE_NAME}"
}
}
}
}
}
#NonCPS
def printSlaveInfo(slave) {
// some info that you can use to choose the least-busy, best-equipped, etc.
println('====================')
println('Name: ' + slave.name)
println('getLabelString: ' + slave.getLabelString())
println('getNumExectutors: ' + slave.getNumExecutors())
println('getRemoteFS: ' + slave.getRemoteFS())
println('getMode: ' + slave.getMode())
println('getRootPath: ' + slave.getRootPath())
println('getDescriptor: ' + slave.getDescriptor())
println('getComputer: ' + slave.getComputer())
def computer = slave.computer
println('\tcomputer.isAcceptingTasks: ' + computer.isAcceptingTasks())
println('\tcomputer.isLaunchSupported: ' + computer.isLaunchSupported())
println('\tcomputer.getConnectTime: ' + computer.getConnectTime())
println('\tcomputer.getDemandStartMilliseconds: ' + computer.getDemandStartMilliseconds())
println('\tcomputer.isOffline: ' + computer.isOffline())
println('\tcomputer.countBusy: ' + computer.countBusy())
println('\tcomputer.getLog: ' + computer.getLog())
println('\tcomputer.getBuilds: ' + computer.getBuilds())
println('====================')
}

h pipeline/job has a trusted agent list, more job succeed on the agent, the agent sit in the top on the list, pipeline/agent will pick top agent from list.
If your pipeline already run on master for several times and all succeed, even you give another agent to choose, pipeline always pick the most trusted agent firstly

Related

Defining agent labels in a single separated file

I am currently facing a problem, I have about 90 jenkinsfile, we recently updated one of the Jenkins agent and it has a new label now, which means that we have to go and update every jenkinsfile with the new label of that agent, you agree that this is a bit of a pain, especially since we will have to do it every time we update the agent.I was thinking if we can define all of the agents is a single file (variable=value) than we reference the variable in our jenkinsfile, so next time we upgrade the agent we do the changes in that particular file instead of 90 jenkinsfile
Yes, you can do this. I'm assuming you have the agent details in the same SCM repo you have the Pipelines. In this case, you can do something like the below.
pipeline {
agent {label getAgentFromFile()}
stages {
stage('Hello6') {
steps {
script {
echo "Hello Something"
}
}
}
}
}
def getAgentFromFile(){
def agent = "default"
node {
agent = new File( pwd() + '/agent.txt').text.trim()
println agent
}
return agent
}

Is it possible to use jenkins-agent with given ip pattern in jenkins?

Basically, there are many jenkins-nodes in a jenkins-server. The launch-method of the majority is launch agents via ssh and the host under the launch method option is the actual ip. However, the labels of the majority are empty.
The main problem is a firewall in front of my server 10.227.10.10. In short, this firewall will block the packets. If using the jenkins-pipeline like below, the packets from some jenkins-nodes may get blocked by the firewall.
I read the pipeline syntax and the label can be the solution. However, there will be massive work to configure the label part of the jenkins-nodes. Any ideas?
pipeline {
agent any
stages {
stage('stage1') {
agent any
// agent {ip '10.227.*.*'} // This would be a better solution
steps {
sh "curl http://10.227.10.10"
}
}
}
}
I don't believe the IP address is available as a property of the Slave or via getComputer()
The Pipeline Agent, label only supports explicit labels, but does allow for conditions. You can just make a new label (eg: "227Net" ), add it to the nodes using the console groovy script below, then reference that label in your pipeline:
agent { node { label '227Net' } }
Should work fine.
def newLabel = '227Net'
Jenkins.instance.slaves.findAll {it.name.contains('10.227') }.each {agent ->
oldLabelString = agent.getLabelString()
println "Relabeling ${agent.name} : ${oldLabelString} + ${newLabel}"
newLabelString = oldLabelString + " " + newLabel
agent .setLabelString(newLabelString)
}
return

How i can trigger build from jenkinsfile using cron syntax?

im using multibranch jenkins style each branch has its own jenkinsfile, i have added triggers in jenkinsfile but it didnt trigger anything on the specified time (8pm), im not sure if im missing something
agent {
node {
label 'master'
}
}
triggers {
cron(env.APP_NAME == 'DICTIONARY' ? '00 20 * * *' : '')
}
stages {
stage('SCM Checkout') {
steps {
git(branch: 'test', url: 'https://gitlab.testral.ba/amramework.git', poll: true, credentialsId: 'GitlabCred')
}
}
For the change in cron to catch, you need to run your Jenkinsfile once manually in the correct branch. After that, check in "View Configuration" that your cron succeeded ("Build periodically" should be checked and contain the schedule).
If it has not, it could be that, at the time when triggers are evaluated, your env.APP_NAME differs from 'DICTIONARY'.
To debug the env, you may add the following:
println "env.APP_NAME is ${env.APP_NAME}" // will run before pipeline
pipeline {
agent {
node {
As a side-note, it's recommended using H instead of minutes, so not all hourly builds fall exactly on the hour:
triggers {
cron(env.APP_NAME == 'DICTIONARY' ? 'H 20 * * *' : '')
}

Can I define multiple agent labels in a declarative Jenkins Pipeline?

I'm using declarative Jenkins pipelines to run some of my build pipelines and was wondering if it is possible to define multiple agent labels.
I have a number of build agents hooked up to my Jenkins and would like for this specific pipeline to be able to be built by various agents that have different labels (but not by ALL agents).
To be more concrete, let's say I have 2 agents with a label 'small', 4 with label 'medium' and 6 with label 'large'. Now I have a pipeline that is very resource-low and I want it to be executed on only a 'small'- or 'medium'-sized agent, but not on a large one as it may cause larger builds to wait in the queue for an unnecessarily long time.
All the examples I've seen so far only use one single label.
I tried something like this:
agent { label 'small, medium' }
But it failed.
I'm using version 2.5 of the Jenkins Pipeline Plugin.
You can see the 'Pipeline-syntax' help within your Jenkins installation and see the sample step "node" reference.
You can use exprA||exprB:
node('small||medium') {
// some block
}
EDIT: I misunderstood the question. This answer is only if you know
which specific agent you want to run for each stage.
If you need multiple agents you can declare agent none and then declare the agent at each stage.
https://jenkins.io/doc/book/pipeline/jenkinsfile/#using-multiple-agents
From the docs:
pipeline {
agent none
stages {
stage('Build') {
agent any
steps {
checkout scm
sh 'make'
stash includes: '**/target/*.jar', name: 'app'
}
}
stage('Test on Linux') {
agent {
label 'linux'
}
steps {
unstash 'app'
sh 'make check'
}
post {
always {
junit '**/target/*.xml'
}
}
}
stage('Test on Windows') {
agent {
label 'windows'
}
steps {
unstash 'app'
bat 'make check'
}
post {
always {
junit '**/target/*.xml'
}
}
}
}
}
This syntax appears to work for me:
agent { label 'linux && java' }
As described in Jenkins pipeline documentation and by Vadim Kotov one can use operators in label definition.
So in your case if you want to run your jobs on nodes with specific labels, the declarative way goes like this:
agent { label('small || medium') }
And here are some examples from Jenkins page using different operators
// with AND operator
agent { label('windows && jdk9 )') }
// a more complex one
agent { label('postgres && !vm && (linux || freebsd)') }
Notes
When constructing those definitions one just needs to consider following rules/restrictions:
All operators are left-associative
Labels or agent names can be surrounded with quotation marks if they contain characters that would conflict with the operator syntax
Expressions can be written without whitespace
Jenkins will ignore whitespace when evaluating expressions
Matching labels or agent names with wildcards or regular expressions is not supported
An empty expression will always evaluate to true, matching all agents
Create a another label call 'small-or-medium' that has 6 all agents. Then in Jenkinsfile:
agent { label 'small-or-medium' }

How to limit each Jenkins build instance to a maximum of x nodes

Is it possible to limit a job's build instance to run on up to x number of nodes?
For example, let's say I have 20 slaves and a parent job called "ParentJob". "ParentJob" is configured to run 20 child jobs concurrently, called "ChildJob", on any available slave. Since I need to kick off multiple ParentJobs, I want to limit the child jobs to 5 nodes per ParentJob build instance.
Basically, I want to kick off ParentJob #1, #2, and #3 concurrently, but I don't want all the ChildJobs spawned from ParentJob #1 to hog all the slaves. ChildJobs from ParentJob #1 should use nodes 1-5, ChildJobs from ParentJob #2 should use nodes 6-10, and ChildJobs from ParentJob #3 should use nodes 11-15.
I've looked at Throttle Concurrent Builds plugin and the Lockable Resource plugin, but they don't seem to address what I want to accomplish. They seem to work at the job level only, and not at the build level.
My other option is to write Groovy code to grab x available nodes, label them uniquely, and run the child jobs on these nodes. When the job is finished, I can clear the labels so they're available for the other builds.
Is there a simpler option or plugin that can do this? Maybe I'm over complicating this.
Hope that wasn't too confusing. Thanks in advance.
Since I couldn't find anything, I wrote the following Groovy script for my pipeline job.
This script will grab the first 3 slaves without a "TESTING_.*" label and append a new label. The jobs are then executed on those nodes only, without taking up all the slaves in my farm. Once the jobs are finished, the labels are removed.
node ("master") {
def new_label = "TESTING_" + org.apache.commons.lang.RandomStringUtils.random(7, true, true)
echo "New label for slaves: " + new_label
try {
stage('Reserve slaves') {
reserve_slaves(3, new_label)
}
stage('Smoke tests') {
// Do your parallel jobs here
// Pass in new_label to the job
}
}
finally {
stage('Return slaves') {
remove_label_from_slaves(new_label)
}
}
}
def reserve_slaves(number_of_slaves, new_label) {
def label_prefix = "TESTING_"
def slaves_with_new_label = 0
while (slaves_with_new_label < number_of_slaves) {
for (slave in jenkins.model.Jenkins.instance.slaves) {
def current_labels = slave.getLabelString()
if (!current_labels.contains(label_prefix)) {
echo "Adding label '${new_label}' to " + slave.name + ". Existing labels are: ${current_labels}"
slave.setLabelString(current_labels + " " + new_label)
slaves_with_new_label += 1
}
if (slaves_with_new_label >= number_of_slaves) {
break
}
}
if (slaves_with_new_label < number_of_slaves) {
echo "Waiting for nodes to become available..."
sleep(10) // in seconds
}
}
}
def remove_label_from_slaves(label) {
for (slave in jenkins.model.Jenkins.instance.slaves) {
def current_labels = slave.getLabelString()
if (current_labels.contains(label)) {
echo "Removing label '${label}' from " + slave.name + ". Existing labels are: ${current_labels}"
current_labels = current_labels.replace(label, '')
slave.setLabelString(current_labels)
}
}
}

Resources