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

Поделиться с друзьями
-->

Комментарии (5)


  1. Losted
    07.09.2016 16:52

    А как решаете вопрос отсутствия checkpoint'ов в бесплатной версии дженкинса?


    1. aatarasoff
      08.09.2016 13:54

      Пока что не было потребности. У нас в основном довольно короткий и линейный цикл поставки, поэтому проще или запустить весь цикл с начала, или использовать поправленный ручками Replay. Подробнее есть у меня в презентации. Видео, к сожалению, выступления нет.


      1. Losted
        08.09.2016 14:00

        Понятно. Мы, с свое время, отказались от этого плагина в пользу более примитивного Delivery Pipeline как-раз из-за необходимости рестарта с точки падения.


  1. varnav
    08.09.2016 13:13

    Кстати, как вам Blueocean? Уже достаточно стабилен для прода?


    1. aatarasoff
      08.09.2016 13:53

      Он вполне себе ок на чтение. Нам больше пока от него ничего и не нужно, поскольку имеем автоматическую настройку, заскриптованную через Ansible. Ставили его из исходников через собственный Update Center, но можно и через официальный экспериментальный.