в deploy, jenkins, scripts, tips

Jenkins as a code. Часть 3


В заключительной статье цикла «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/.

Добавить комментарий