В данной статье цикла “Jenkins as a code” рассмотрим самый интересный (и полезный) пример — автоматическое создание задач (job) при запуске сервиса. Давайте разберемся!
Создавать задания (jobs) в Jenkins можно несколькими способами — традиционно (через webUI), через REST API или “подкладывая” файлы .xml.import
в ${JENKINS_HOME}
.
Но традиционный способ настройки занимает слишком много времени, использование API невероятно неудобно (см. пример ниже), а создание файлов с расширением .xml.import
для каждой задачи и рестарт сервиса Jenkins после их добавления даже звучит несерьезно.
Чем плох REST API:
- сначала необходимо получить описание уже существующей задачи в формате
.xml
. Например, это можно сделать используя API:
curl -X GET -u username:API_TOKEN 'http://JENKINS_HOST/job/MY_JOB_NAME/config.xml' -o config.xml
или
curl -u username:password "http://JENKINS_HOST/job/MY_JOB_NAME/config.xml" > config.xml
- потом полученный
xml
-файл нужно отредактировать под свои нужды; - после редактирования можно создать новую задачу “запостив” этот файл через API, например:
curl -u username:password -s -XPOST 'http://JENKINS_HOST/createItem?name=newJobName' --data-binary config.xml -H "Content-Type:text/xml"
Ах да, если вдруг столкнетесь с ошибкой:
Error 403 No valid crumb was included in the request
то придется сделать на одно действие больше:
CRUMB=$(curl -s 'http://JENKINS_HOST/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)' -u username:password)//crumb)' -u us
curl -u username:password -s -XPOST 'http://JENKINS_HOST/createItem?name=newJobName' --data-binary config.xml -H "$CRUMB" -H "Content-Type:text/xml"
Вообщем, так себе вариант, особенно если у вас 150-200 заданий…
Вспоминаем о возможностях настройки экземпляра Jenkins еще в момент старта с помощью хуков, которые мы рассматривали в первой и второй статьях данного цикла. Идея — хранить в отдельном файле определенного формата (.json
или .xml
) минимально необходимый набор параметров (ссылка на git-репозиторий, название ветки, путь к Jenkinsfile
и т. д.) для создания задачи. При запуске Jenkins groovy-скрипт берет из данного файла параметры и создает задачи, в качестве дополнительного бонуса — сразу распределяет эти задачи по представлениям (views).
Создаем файл job_list.yaml
с набором параметров для задач, в моем случае выглядящий так:
dev:
1st-project:
jobDisabled: false
enableGithubPolling: false
daysBuildsKeep: '1'
numberBuildsKeep: '5'
githubRepoUrl: 'https://github.com/ealebed/hn1/'
githubRepo: 'https://github.com/ealebed/hn1.git'
githubBranch: '*/master, */test'
credentialId: ''
jenkinsfilePath: 'Jenkinsfile'
2nd-project:
jobDisabled: false
enableGithubPolling: true
daysBuildsKeep: '2'
numberBuildsKeep: '10'
githubRepoUrl: 'https://github.com/ealebed/hn1/'
githubRepo: 'https://github.com/ealebed/hn1.git'
githubBranch: '*/master'
credentialId: ''
jenkinsfilePath: 'Jenkinsfile'
stage:
3rd-project:
jobDisabled: false
enableGithubPolling: true
daysBuildsKeep: '3'
numberBuildsKeep: '15'
githubRepoUrl: 'https://github.com/ealebed/hn1/'
githubRepo: 'https://github.com/ealebed/hn1.git'
githubBranch: '*/master'
credentialId: ''
jenkinsfilePath: 'dir/Jenkinsfile'
test:
4th-project:
jobDisabled: true
enableGithubPolling: true
daysBuildsKeep: '4'
numberBuildsKeep: '20'
githubRepoUrl: 'https://github.com/ealebed/hn1/'
githubRepo: 'https://github.com/ealebed/hn1.git'
githubBranch: '*/master'
credentialId: ''
jenkinsfilePath: 'Jenkinsfile'
Примечание. Если планируется переезд со старого экземпляра Jenkins на новый, то можно легко написать скрипт, который возьмет нужные параметры на старом сервере и сформирует yaml
-файл в нужном формате.
Скрипт, который должен “разобрать” данный файл и создать задачи у меня называется 05-create-jobs.groovy
и выглядит следующим образом:
@Grab(group='org.yaml', module='snakeyaml', version='1.18')
import jenkins.model.*
import hudson.model.*
import hudson.tasks.LogRotator
import hudson.plugins.git.*
import org.jenkinsci.plugins.workflow.job.WorkflowJob
import org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition
import com.coravy.hudson.plugins.github.*
import com.cloudbees.jenkins.GitHubPushTrigger
import org.yaml.snakeyaml.Yaml
Jenkins jenkins = Jenkins.getInstance()
def pluginParameter = "workflow-aggregator github-pullrequest"
def plugins = pluginParameter.split()
def pm = jenkins.getPluginManager()
def installed = false
plugins.each {
if (!pm.getPlugin(it)) {
println("Plugin ${it} not installed, skip creating jobs!")
} else {
installed = true
}
}
if(installed) {
def listExistingJob = jenkins.items.collect { it.name }
def listExistingViews = jenkins.views.collect { it.name }
def jobParameters = new Yaml().load(new FileReader('/temp/job_list.yaml'))
for (view in jobParameters) {
if (jobParameters.any { listExistingViews.contains(view.key) }) {
println("--- View ${view.key} already exist, skip")
} else {
println("--- Create new view ${view.key}")
jenkins.addView(new ListView(view.key))
}
for (item in view.value) {
if (view.value.any { listExistingJob.contains(item.key) }) {
println("--- Job ${item.key} already exist, skip")
continue
} else {
println("--- Create new job ${item.key}")
def jobName = item.key
def jobDisabled = view.value.get(item.key).getAt("jobDisabled")
def enableGithubPolling = view.value.get(item.key).getAt("enableGithubPolling")
def daysBuildsKeep = view.value.get(item.key).getAt("daysBuildsKeep")
def numberBuildsKeep = view.value.get(item.key).getAt("numberBuildsKeep")
def githubRepoUrl = view.value.get(item.key).getAt("githubRepoUrl")
def githubRepo = view.value.get(item.key).getAt("githubRepo")
def githubBranch = view.value.get(item.key).getAt("githubBranch")
def credentialId = view.value.get(item.key).getAt("credentialId")
def jenkinsfilePath = view.value.get(item.key).getAt("jenkinsfilePath")
def githubBranchList = githubBranch.split(', ')
def branchConfig = new ArrayList<BranchSpec>()
for (branch in githubBranchList) {
if( branch != null )
{
branchConfig.add(new BranchSpec(branch))
}
else
{
branchConfig.add(new BranchSpec("*/master"))
}
}
def userConfig = [new UserRemoteConfig(githubRepo, null, null, credentialId)]
def scm = new GitSCM(userConfig, branchConfig, false, [], null, null, null)
def flowDefinition = new CpsScmFlowDefinition(scm, jenkinsfilePath)
flowDefinition.setLightweight(true)
def job = new WorkflowJob(jenkins, jobName)
job.definition = flowDefinition
job.setConcurrentBuild(false)
job.setDisabled(jobDisabled)
job.addProperty(new BuildDiscarderProperty(new LogRotator(daysBuildsKeep, numberBuildsKeep, null, null)))
job.addProperty(new GithubProjectProperty(githubRepoUrl))
if (true == enableGithubPolling) {
job.addTrigger(new GitHubPushTrigger())
}
jenkins.save()
jenkins.reload()
hudson.model.Hudson.instance.getView(view.key).doAddJobToView(jobName)
}
}
}
}
Сначала мы проверяем, установлены ли необходимые плагины (без них создать представление / задачу не получится). Далее получаем список уже существующих представлений и задач (может так случиться, что сервер с Jenkins будет перезагружен) — нам совершенно нет нужды пересоздавать задачи при каждом старте Jenkins. Только если нужного нам представления (view) нет — оно будет создано, после чего создаются добавляются в представление связанные с ним задачи.
Остается только позаботиться о том, чтобы файл с параметрами задач появился в /temp/job_list.yaml
(или другом удобном для вас месте), а скрипт 05-create-jobs.groovy
— в каталоге ${JENKINS_HOME}/init.groovy.d/
.
В заключительной статье цикла рассмотрим еще один важный вопрос разграничения прав доступа пользователей.