in jenkins continuous-delivery ~ read.

Получаем управление обратно в Jenkins Pipeline

Jenkins Pipeline Plugin очень удобная штука, чтобы организовать у себя Continuous Delivery. Плагин даёт возможность разбить доставку ПО до конечного потребителя на стадии (stage), каждой из которой можно управлять (какая нода, что именно на ней нужно сделать) и визуализировать. Вкупе с Blueocean plugin всё это выглядит очень вкусно. В реальной же жизни подчас оказывается так, что кроме Jenkins-а есть ещё и другие системы, которые участвуют в workflow, и встаёт вопрос - как их интегрировать с имеющимися решениями. Примером тут может служить Jira, в которой есть некий issue падающий на тестировщика, прокликивающего интерфейс (ну или совершающего другую полезную работу), и только после его благословения, наш артефакт имеет право двигаться по пайплайну дальше.

Выхода мы можем придумать, очевидно, два:

  • "засыпать" на время и проверять состояние чего-то во внешней системе
  • использовать 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 ещё раз, мы увидим, что с нас теперь требуют выполнить подтверждающее действие в интерфейсе: alt Но интерфейс это круто для любителей щелкать мышью, но он никак не решает нашей задачи, поэтому идём курить 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']  

В интерфейсе у нас теперь будет как-то так: alt Но нам опять таки GUI малоинтересен и идём дальше изучать API.

Чтобы пробросить параметр, нужно использовать другой путь 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 соответственно его значение.

Теперь встаёт вопрос как его правильно передать, и тут у непосвященных начинаются проблемы. Этот контент не нужно передавать непосредственно в теле запроса, его нужно обернуть в форму и запихнуть в поле 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 для этих действий.

comments powered by Disqus