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 для этих действий.
