I have been trying to figure out this problem for a long time now. I thought I had found a solution, but it looks like the world has moved on from it.
Some background
First, a bit of background. I have used the JobDSL plugin to provision Jenkins jobs using IaC principles. My jobs definition
section would be configured to point to a jenkins-scripts repo that contained the pipeline scripts to run for certain jobs (some jobs would run the same scripts with different input values). And my scripts would import a jenkins-shared-library to import shared functions for things like “compile Java code”, “unit test Java code”, “build container image”, “trivy scan container image”, “push container image”, etc.
I am quite happy with my scripts repo and shared-library repo as they allowed me to follow DRY and KISS principles, but I was not able to do the same with my job definitions written in JobDSL syntax.
Project repo structures & code examples
Here are some code example to better demonstrate the structure I am used to:
Here’s the project structure. Just consider all project
and app
directories to have the same structure, so I only show the 1st one.
jenkins-jobs/
├── project1/
│ ├── directory.groovy
│ ├── app1/
│ │ ├── directory.groovy
│ │ ├── automation_testing.groovy
│ │ ├── build/
│ │ │ ├── directory.groovy
│ │ │ ├── java_services.groovy
│ │ │ ├── node_services.groovy
│ │ │ └── python_services.groovy
│ │ ├── deploy/
│ │ │ ├── directory.groovy
│ │ │ ├── deploy_services.groovy
│ │ │ └── deploy_all_services.groovy
│ │ └── release/
│ │ └── directory.groovy
│ └── app2/
│ ├── build
│ ├── deploy
│ └── release
└── project2/
├── app1/
│ ├── build
│ ├── deploy
│ └── release
└── app2/
├── build
├── deploy
└── release
Here are some examples of the directory.groovy
files. These are done in a way so that the repo structure appears the same as in the Jenkins webUI:
# project1/directory.groovy
String F_ROOT = 'PROJECT-1'
folder(F_ROOT) {
configure {
# I add some permissions config here to ensure teams only see folders related to them
}
}
}
# project1/app1/directory.groovy
String F_ROOT = 'PROJECT-1/APP-1'
folder(F_ROOT) {}
# project1/app1/build/directory.groovy
String F_ROOT = 'PROJECT-1/APP-1/BUILD'
folder(F_ROOT) {}
# project1/app1/deploy/directory.groovy
String F_ROOT = 'PROJECT-1/APP-1/DEPLOY'
folder(F_ROOT) {}
And here is an example of the project1/app1/build/java_services.groovy
:
String F_ROOT = 'PROJECT-1/APP-1/BUILD'
String JOB_NAME = 'job_build'
// Execute this task at a random minute within the 11 PM hour, every day
// Execute this task at a random minute within the 11 PM hour, every Sunday
def CRON_SCHEDULE_EVERY_DAY = 'H 23 * * 1,2,3,4,5,6,0'
def CRON_SCHEDULE_EVERY_SUNDAY = 'H 23 * * 0'
def SERVICES = [
"service1-name",
"service2-name",
"service3-name"
]
// TODO: Try to use this list in the activeChoiceReactiveParam
def ENVIRONMENTS = [
'ALPHA',
'STAGING',
'AUTOMATION',
'DEV',
'TEST1',
'TEST2'
]
for (SVC in SERVICES) {
SVC_NAME = SVC.replaceAll('-', ' ')
SVC_PATH = SVC.replaceAll('-', '_')
pipelineJob("${F_ROOT}/${JOB_NAME}_${SVC_PATH}") {
displayName(SVC_NAME.toUpperCase())
parameters {
gitParameter {
name('SERVICE_BRANCH')
type('PT_BRANCH')
branch('')
defaultValue('')
description('Please select the branch to build the service')
tagFilter('')
sortMode('DESCENDING_SMART')
selectedValue('TOP')
useRepository(SVC)
quickFilterEnabled(true)
}
gitParameter {
name('BASE_IMAGE_BRANCH')
type('PT_BRANCH')
branch('')
defaultValue('main')
description('Please select the branch for the base image')
tagFilter('')
sortMode('DESCENDING_SMART')
selectedValue('TOP')
useRepository('docker-images')
quickFilterEnabled(true)
}
activeChoiceParam('DEPLOY_AFTER_BUILD') {
description('Do you want to deploy to environment after a successful build?')
filterable(false)
choiceType('SINGLE_SELECT')
groovyScript {
script('return ["NO:selected", "YES"]')
fallbackScript('"The choice script failed"')
}
}
activeChoiceReactiveParam('ENVIRONMENT') {
description('Select an environment to deploy to')
filterable(false)
choiceType('SINGLE_SELECT')
groovyScript {
// TODO: Find a better way to load this list into the script (try to use ENVIRONMENTS ArrayList)
script("if (DEPLOY_AFTER_BUILD.equals('YES')) { return ['ALPHA', 'STAGING', 'DEV', 'AUTOMATION', 'TEST1', 'TEST2'] }")
fallbackScript('"The choice script failed"')
}
referencedParameter('DEPLOY_AFTER_BUILD')
}
hidden {
name('SERVICE_NAME')
defaultValue(SVC)
description('The name of the service being built')
}
}
environmentVariables {
env('FAILED_STAGE', 'Pipeline start')
keepBuildVariables(true)
keepSystemVariables(true)
overrideBuildParameters(false)
}
properties {
pipelineTriggers {
triggers {
parameterizedTimerTrigger {
parameterizedSpecification("""
${CRON_SCHEDULE_EVERY_DAY} %SERVICE_BRANCH=main;BASE_IMAGE_BRANCH=main;DEPLOY_AFTER_BUILD=NO;
${CRON_SCHEDULE_EVERY_DAY} %SERVICE_BRANCH=main;BASE_IMAGE_BRANCH=main;DEPLOY_AFTER_BUILD=NO;
${CRON_SCHEDULE_EVERY_DAY} %SERVICE_BRANCH=main;BASE_IMAGE_BRANCH=main;DEPLOY_AFTER_BUILD=NO;
${CRON_SCHEDULE_EVERY_SUNDAY} %SERVICE_BRANCH=main;BASE_IMAGE_BRANCH=main;DEPLOY_AFTER_BUILD=NO;
${CRON_SCHEDULE_EVERY_SUNDAY} %SERVICE_BRANCH=main;BASE_IMAGE_BRANCH=main;DEPLOY_AFTER_BUILD=NO;
${CRON_SCHEDULE_EVERY_SUNDAY} %SERVICE_BRANCH=main;BASE_IMAGE_BRANCH=main;DEPLOY_AFTER_BUILD=NO;
""")
}
}
}
}
logRotator {
numToKeep(30)
}
definition {
cpsScm {
scm {
git {
remote {
url('ssh://git@${my-git-domain}/jenkins-scripts.git') # this is the repo where all the scripts are stored
}
remote {
url('ssh://git@${my-git-domain}/docker-images.git')
}
remote {
url("ssh://git@${my-git-domain}/${SVC}.git")
}
branch('main')
extensions {
cleanBeforeCheckout()
}
}
}
scriptPath('build/Jenkinsfile_build_java_service.groovy') # this is the path for the script to run, which is found in the `jenkins-scripts` repo.
# all my scripts utilise a jenkins-shared-library to follow the DRY principle.
}
}
}
}
So you can see that I have some repeatability using the for
to iterate over the SERVICES
list, which is fine and I have 1 script for 1 type of job/service (build/java, deploy/java, build/python, etc.), but I want more re-usability.
The primary issue
My top problem here is that for any new project, or new app within a project, I have to copy/paste a file and then edit it according to what is needed, for example, the SERVICES
list or the ENVIRONMENTS
list.
There are usually some other larger changes as well like different
Cron schedules, potentially checking out code from a different git repository domain, etc.
I don’t want to copy/paste and then edit the new files in this way as it just seems silly, wrong and while it is scalable to some extent, I would like it to be even easier, and more scalable.
A possible solution
A came across some amazing articles by Marc Esher, here is a link to the 1st article. There are 4 or 5 different articles on Marc’s website detailing how he used custom groovy classes as a wrapper around JobDSL syntax to create re-usable job definition configurations. Here is a link to 1 such article that best describes the entire workflow of creating a re-usable JobBuilder
class and the using it in a groovy script that is read and used by the JobDSL Seed job.
As soon as I saw this, I was jumping for joy! It was exactly the sort of thing I needed. It wrapped up job configurations very nicely, while still allowing some customisation when calling the JobBuilder
class in case it wasn’t quite what you needed.
I immediately set about trying to make a simple repository to prove that this works.
A new problem
Unfortunately, the articles I have linked are almost 10 years old and in that time there have been many updates to Jenkins and it’s plugin ecosystem.
The main issue I came across was JobDSL’s integration with the Script Security plugin (some info about that in a wiki page here).
This essentially blocks the use of custom classes within script execution, which directly breaks this method of wrapping JobDSL syntax in custom Groovy classes.
The only way that I found enabled the use of custom Groovy classes was disabling JobDSL script security entirely, which is more than undesirable!
I also had some annoying issues with the JobDSL steps AdditionalClasspath
setting, but that is minor compared to disabling script security globally.
A call for help
I’ve been trying a bunch of different things for about 2-3 months trying to get this sort of thing to work, as it is such a great solution, but I can’t quite find how to get it working without globally disabling JobDSL script security, which I definitely do not want to do.
I would be ok with disabling script security for only the JobDSL seed job, as that will only be accessed and ran by Jenkins administrator users, but I don’t think it is possible to disable script security for 1 single job.
I have also seen that the Script Console can be used with no restrictions, so this could be another option as that would only be accessed by administrator users as well, but I’d much prefer to use the JobDSL Seed job as it’s fairly easy to understand and it is pre-configured to run all the correct files, so all a Jenkins dev needs to do is update the branch it looks at and run the job.
Any help on this, even just suggestions and nudges in the right direction would be greatly appreciated.
Thank you for taking the time to read this rather long post!