Jenkins Pipeline Plugin очень удобная штука, чтобы организовать у себя непрерывную доставку ПО (Continuous Delivery). Плагин даёт возможность разбить доставку ПО до конечного потребителя на стадии (stage), каждой из которых можно управлять (на каком узле, что и как нужно сделать) и, в конечном счёте, визуализировать процесс доставки. Вкупе с Blueocean plugin всё это выглядит очень вкусно. В реальной же жизни подчас оказывается так, что кроме Jenkins-а есть ещё и другие системы, которые участвуют в этом процессе (workflow), и встаёт вопрос — как их интегрировать с имеющимися решениями. Примером тут может служить Jira, в которой есть некий issue падающий на тестировщика, прокликивающего интерфейс (ну или совершающего другую полезную работу), и только после его благословения, наш артефакт имеет право двигаться дальше в сторону ожидающего его клиента.
Так какие у нас есть варианты реализации?
Очевидно, что их не меньше двух:
- "засыпать" на некоторое время и проверять состояние во внешней системе (polling)
- использовать webhook для продолжения или отмены движения артефакта по пайплайну
Первый вариант как минимум неудобен тем, что нужно писать цикл, который будет что-то проверять, не забыть о его прерывании после некоторого количества времени, и, вообще, поллинг не самый крутой метод, как мне кажется. Поэтому будем смотреть сразу в сторону веб-хука.
Пошерстив документацию, фичи с названием веб-хук я не нашел, и это, на самом деле, не очень хорошо, потому что то, что есть — скорее некий workaround нежели целевое решение.
Ставить опыты будем на очень простой конфигурации сферического коня в сферическом вакууме (буквально берём пример из example-ов):
node {
stage 'Stage 1'
echo 'Hello World 1'
stage 'Stage 2'
echo 'Hello World 2'
stage 'Stage 3'
build job: 'hello-task', parameters: [[$class: 'StringParameterValue', name: 'CoolParam', value: 'hello']]
}
Для описания шагов последовательности действий, в плагине используется groovy-dsl. В приведённом примере у нас всё будет выполняться на одной ноде (причем это мастер, поэтому не делайте так ;)). Как видно, есть три стадии исполнения, две из которых просто пишут Hello World
в консоль (какая неожиданность), а третья вызывает не менее простой job и передаёт в него параметр, который также нужно напечатать в консоли.
Если выполнить этот таск, то мы увидим в логах нечто подобное:
Started by user admin
[Pipeline] node
Running on master in /var/jenkins_home/jobs/pipeline-test/workspace
[Pipeline] {
[Pipeline] stage (Stage 1)
Entering stage Stage 1
Proceeding
[Pipeline] echo
Hello World 1
[Pipeline] stage (Stage 2)
Entering stage Stage 2
Proceeding
[Pipeline] echo
Hello World 2
[Pipeline] stage (Stage 3)
Entering stage Stage 3
Proceeding
[Pipeline] build (Building hello-task)
Scheduling project: hello-task
Starting building: hello-task #2
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS
Ура, у нас выполнились и наши команды из скрипта, и наш дочерний таск, который мы определили отдельно.
Теперь представим, что между первой и второй стадией, нам нужно одобрение от внешней системы на продолжение исполнения работы. Для того, чтобы его реализовать, будем использовать механизм ожидания ввода данных пользователем через конструкцию input. В простейшем виде это будет выглядеть так:
input 'Ready to go?'
Запустив наш job ещё раз, мы увидим, что с нас теперь требуют выполнить подтверждающее действие в интерфейсе:
Но интерфейс это круто для любителей щелкать мышью, но он никак не решает нашей задачи, поэтому идём курить API. И тут с документацией не всё в порядке. Чтобы понять что и как вызвать, нужно спросить совета у знающих в чатике людей, и исследовать код.
Так как в нашем примере нет никаких параметров, то можно использовать метод proceedEmpty
, для подтверждения действия. Чтобы это сделать, нужно кинуть POST-запрос на урл:
JENKINS_ROOT_URL/job/JOB_NAME/BUILD_NUMBER/input/INPUT_ID/proceedEmpty?token=YOUR_TOKEN
Основная сложность тут именно в получении INPUT_ID
, потому что через API его у меня достать не получилось, а понять какой он, можно только распарсив страницу или просмотрев трафик сабмита формы. Хорошая новость в том, что INPUT_ID
всегда постоянный. Плохая — по-умолчанию он генерируется рандомно и представляет собой строку символов. Ходить и каждый раз её узнавать не самое веселое занятие, поэтому надо задать этот ID вручную через свойство id
:
input message: 'Ready to go?', id: 'go'
Тут стоит обратить внимание, что реальный ID всегда будет начинаться с заглавной буквы. В итоге в моём случае запрос оказался следующим:
http://localhost:8080/job/pipeline-test/16/input/Go/proceedEmpty?token=f7614a8510b59569347714f53ab1e764
Дополнительной плюшкой механизма input-ов является возможность задавать дополнительные параметры, которые потом можно использовать:
def testPassParamInput = input(
id: 'testPassParam', message: 'Pass param?', parameters: [
[$class: 'StringParameterDefinition', defaultValue: 'hello', description: 'Test parameter', name: 'testParam']
])
Для этого мы можем определить некоторый параметр, который хотим передавать в дочерний job, в нашем случае testParam
. Соответственно мы можем переписать вызов дочернего job-а для того, чтобы он принимал этот параметр:
build job: 'hello-task', parameters: [[$class: 'StringParameterValue', name: 'CoolParam', value: testPassParamInput]]
Обратите внимание, что в value передаётся весь объект целиком. В случае если параметров будет несколько, необходимо указывать явно какой параметр нужно взять:
testPassParamInput['testParam']
В интерфейсе у нас теперь будет как-то так:
Но нам опять таки GUI малоинтересен и идём дальше изучать API. Чтобы пробросить параметр через обычный HTTP, нужно использовать другой метод: proceed
:
JENKINS_ROOT_URL/job/JOB_NAME/BUILD_NUMBER/input/INPUT_ID/proceed?token=YOUR_TOKEN
При этом нам нужно передать форму с параметрами и их значениями. Для этого в первую очередь сформируем правильный JSON:
{
"parameter" : [
{
"name" : "testParam",
"value" : "new cool value"
}
]
}
Здесь name
— имя параметра, а value
соответственно его значение.
Теперь встаёт вопрос как его правильно передать, и тут у непосвященных начинаются проблемы. Так как Jenkins реализует у себя JSONP, то этот контент не передать непосредственно в теле запроса. Вместо этого, его необходимо обернуть в форму и запихнуть в поле json
. Если делать это через Postman, то итоговый запрос будет выглядеть следующим образом:
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="json"
{ "parameter": [ { "name" : "testParam", "value" : "new cool value" } ] }
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Не слишком красиво, но это работает. Теперь в логах мы сможем наблюдать, что действительно действия были подтверждены пользователем (в нашем случае администратором):
Hello World 2
[Pipeline] input
Ready to go?
Proceed or Abort
Approved by admin
[Pipeline] input
Input requested
Approved by admin
[Pipeline] stage (Stage 3)
Entering stage Stage 3
Proceeding
[Pipeline] build (Building hello-task)
Scheduling project: hello-task
Starting building: hello-task #11
В случае, когда внешняя система добро не даёт, ей нужно дёрнуть метод abort
:
JENKINS_ROOT_URL/job/JOB_NAME/BUILD_NUMBER/input/INPUT_ID/abort?token=YOUR_TOKEN
Никакие данные передавать при этом не надо. В логах после исполнения этого запроса мы увидим, что выполнение действительно было отклонено пользователем:
Rejected by admin
Finished: ABORTED
Ну и напоследок. Не забывайте, что все эти запросы требуют basic-авторизации, токена и crumbs. Последние можно получить по адресу: JENKINS_ROOT_URL/crumbIssuer/api/json
:
{
"_class":"hudson.security.csrf.DefaultCrumbIssuer",
"crumb":"f4c1a2dc6a67c70e66c35c807e542f4e",
"crumbRequestField":"Jenkins-Crumb"
}
После этого нужно вставить в заголовки http-запроса новый заголовок Jenkins-Crumb
и его значение из поля crumb
.
Резюме
В текущем виде Pipeline Plugin даёт возможности по встраиванию управляющих воздействий со стороны внешних систем, что открывает массу возможностей для автоматизации доставки ПО при сложных и переходных процессах внедрения. В то же время, хочется всё-таки более очевидного и красивого API для этих действий.