Ветки, стенды, API Git’a. Заливаем автоматически типы сборок на стенд

от автора

В приступе горячей любви к автоматизации всего, что только возможно, я взял в работу задачу своих братьев-ручных-тестировщиков. Суть: релиз-кандидаты (далее RC) для регрессионного тестирования на 3 стенда ставятся из GitLab CI вручную, что отнимает либо личное время дежурного тестировщика, либо время у регресс тестирования (от 1.5 опытного до 3 новичковых ощутимых часов). Такое количество часов обусловлено тем, что в нашей системе много сервисов, работа над ними ведется командами параллельно. А как известно, разработчики хотят видеть свой код в продуктиве регулярно. Из чего мы и получаем охапку веточек RC, которые нужно вылить на тестовые стенды.

Надо автоматизировать. Но чтобы автоматизировать, нужно узнать, каков алгоритм накатки сейчас. 

Алгоритм накатки:

  1. Создание ветки от RC

  2. Создание pipeline от ветки с указанием номера стенда

  3. Прожатие (тут и далее — последовательное нажатие) кнопок. Причем:

    1. кнопки не связаны друг с другом — нельзя зажать 1 и успокоиться, нужно прожать самостоятельно некоторое количество;

    2. количество кнопок от сервиса к сервису отличается;

    3. деплой на стенд уже существующего там сервиса и деплой отсутствующего — два разных мероприятия;

    4. существуют особые конфигурации кнопок (в одном случае используется один набор кнопок, в другом —отличный вне зависимости от того, находится ли сервис на стенде).

Проведение исследования

Исследовав GitLab API, я понял, что общение с репозиторием устроено дружелюбно и даже мило: генерируешь токен в настройках — все двери открываются (при условии, что на учетке, от которой создан токен, есть нужные разрешения). Я наскреб нужных методов в ладошку по алгоритму выше:

  • создание ветки от RC;

  • создание pipeline от ветки с указанием номера стенда;

  • прожатие кнопок:

    • получение списка джобов (кнопок) для получения по названию ID;

    • прожатие кнопки по ID.

Важно отметить, что все действия требуют branch-ID (project-ID)/pipeline-ID/job-ID.

Формулирование видения реализации

В самом начале идея была такая:

  1. в корпоративный мессенджер пишется информация о том, что надо накатить и куда;

  2. информация попадает через роут на сервис;

  3. сервис внутри себя генерирует магию.

Итог: указанная веточка с кодом от разработчика доставлена на стенд.

Вещи, которые я реализовывал ранее для коллег, прокидывал через мессенджер (бота). Поэтому для данной задачи я решил, что хорошая идея тут тоже его использовать (спойлер: не особо).

Первое приближение

Я начал с данных. GitLab понимает обращения к репозиториям (и всему остальному) по ID, а ручные тестировщики знают только их названия, поэтому я создал JSON файл для мапы одного с другим для удобства использования. То есть на вход мы подаем имя репы — на выходе имеем ID для работы с ним. Вышел файл вида:

{    {      "name":  "service_name_1",      "id": "service_id_1"    },  …    {      "name":  "service_name_n",      "id": "service_id_n"    }  }  

Далее я определился с общим набором данных для запуска:

  • имя ветки;

  • название репозитория;

  • стенд для накатки;

  • ник юзера в мессенджере (для обратного оповещения о результатах).

Чтобы упростить для себя (как я тогда думал) мешанину кнопок, я разделил сервисы на группы по количеству кнопок — микросервисы/backend/frontend. Для каждого создал свой роут.

Роут первого приближения

Роут первого приближения
Псевдокод
routes.post('/deploy', async (req, res, next) => {     //находим ID сервиса     projectID = _.find(projectID, function (o) { return o.name === req.body.service })      //создаем веточку     branch = createGitlabBranch(req.body.branch, projectID)      if (branch != undefined) {         writeMessage(`Создана ветка ${branch.web_url}`, 'chat')          //создаем pipe для веточки         pipe = createGitlabPipeline(projectID.id, branch.name, req.body.stand)          if (pipe != undefined) {             writeMessage(`Создан pipeline ${pipe.web_url}`, 'chat')             //забираем объект стейджей из pipe             stagesObj = await getStagesID(projectID.id, pipe.id)              //забираем из k8s сервисы, чтобы понять делать update или install             if (_.find(k8sServ, function (o) { return o.Name === `${req.body.service}-${req.body.stand}` })) {                 //запускаем job                 runStage(projectID.id, stagesObj, `${req.body.inst_target} update`)                 //ожидаем окончание job                 waitStageResp(projectID, stagesObj, `${req.body.inst_target} update`)             } else {                 runStage(projectID.id, stagesObj, `${req.body.inst_target} install`)                 waitStageResp(projectID, stagesObj, `${req.body.inst_target} install`)             }             writeMessage(`Стейджи сервиса ${req.body.service} отработали`, 'chat')         } else {             writeMessage(`${req.body.user} Pipeline не создался`, 'chat')             return next(new Error(`Pipelin не создался`))         }      } else {         writeMessage(`${req.body.user} Ветка не создалась`, 'chat')         return next(new Error(`Ветка не создалась`))     }     res.status(200).json({ status: '200', message: "успех" }) })

* И так для каждой конфигурации сервиса и разных типов сервисов.

Реализовать очень хотелось как можно скорее, не привлекая дополнительно DevOps-инженеров для решения здорового человека (о нем позднее). Из-за этого возник другой интересный момент: кнопки в CI между собой не связаны — нужно знать, когда job кнопки прошел, чтобы инициировать запуск следующего. Это подтолкнуло меня на реализацию бесконечного ожидания ответа от запущенного job:

function waitStade(projectID, pipelineStageID)          while (!response.includes('Cleaning up file based variables')) {              response = await getStageLog(projectID, pipelineStageID)          }       response = response.split('Cleaning up file based variables')       return response[1]  }

Позже коллега показал мне, что можно вместо опроса логов написать через опрос статуса job, но это было сделано во втором приближении.

В части бота решение выглядело как расщепление строки входа по разделителям на цикл запусков API-вызовов для всех переданных веточек, что имитировало параллельно ставящиеся сборки.

Я отдал эту красоту на “потрогать” ручникам, и оказалось, что решение не настолько жизнеспособное, как мне представлялось:

  1. Выплевывание на каждый чих ссылок с созданной веткой/pipeline плюс результат работы каждого job, когда поставить нужно больше, чем три сборки, делали разбор полетов и сам информационный чатик утомительными.

  2. Люди могли запутаться в обозначениях и поставить микросервис через front или наоборот, что генерировало ошибки в чат и вызывало грусть.

  3. Из-за бесконечного ожидания ответа от job мы ловили ситуацию: три сборки поставились нормально, четвертая после создания pipeline просто не ставилась (причем выстрелить такое могло в середине цикла: сборки 1, 2, 4 отработали как надо, а третья устала на первой кнопке). Кнопки не прожимались.

Все эти пункты для поставки релиза,состоящего из 10-20 и более задач, которые нужно вылить на три стенда, делали решение, мягко говоря, так себе.

Вторая (текущая) реализация

Далее я попытался своими силами решить те проблемы, которые было возможно, чтобы дотянуть решение до поставки целого RC на стенд. 

Первое, что я сделал, — выпилил излишние обратные сообщения в мессенджер. Оставил только ссылку на pipeline и конечное сообщение об успехе или ошибке.

Естественно, нужно было объединить три роута в один. Файл JSON, мапящий id и название репозитория я превратил в конфиг, который содержит в себе дополнительно наборы кнопок для разного рода накатки. Он превратился в:

{    {      "name":  "service_name_1",      "id": "service_id_1"      "btns":  {          "config_1": ["job_1", "job_2", "job_3"],          "config_2": ["job_1", "job_4", "job_3"]          }    },  …    {      "name":  "service_name_n",      "id": "service_id_n"      "btns":  {          "config_1": ["job_1", "job_2"],          "config_2": ["job_1", "job_4"]          }    }  }

За счет этого роут приобрел унифицированный вид.

Роут второго приближения

Роут второго приближения
Псевдокод
routes.post('/deploy', async (req, res) => {      //находим конфиг для сервиса      projectConfig = _.find(projectConfig, { name: service })      //подбор кнопок для мс     if (projectConfig.type === 'ms') {          if (req.body.deploy_type === 'usual' || req.body.deploy_type === 'eac') {              //забираем из k8s сервисы, чтобы понять, делать update или install             btns = (k8sServ) ? projectConfig.btns.update : projectConfig.btns.install          } else {             btns = projectConfig.btns.force         }     } else if (projectConfig.type === 'be') {          btns = (req.body.deploy_type === 'usual') ? projectConfig.btns.usual : projectConfig.btns.eac         buildName = projectConfig.btns.build_name      } else {         //front         btns = projectConfig.btns         buildName = projectConfig.btns.build_name     }      deploy(req.body, standNumber, projectConfig, buildName, btns)      res.status(200).json({ status: '200', message: "успех" })  })

Проблему с поставкой пачки сервисов, состоящей больше чем из 3-4 сборок, своими силами решить без костылей (а куда уж дальше) не удастся. Для этого нужно переделать немного логику внутри решения и продеть часть ее в .yml файлы GitLab CI репозиториев (об этом далее).

Однако на время можно пойти в обход. Если ставить сборки (обновления стенда в нерегрессионную неделю и накатку RC в неделю регресса) ночью, то можно пренебречь временем и проливать их по одной. Для этого я поместил роут в крону, а в кроне организовал цикл.

Псевдокод
cron(night  () => {    //вытаскиваем включенные сборки records = getTestMap({ flag: true })    //цикл по включенным типам сборок for (const { stand, type, assembly } of records) {    //цикл по сборкам for (const { service, branch } of assembly) {    //находим конфиг для сервиса projectConfig = _.find(projectConfig, { name: service })     //подбор кнопок для мс if (projectConfig.type === 'ms') {    //забираем из k8s сервисы, чтобы понять, делать update или install if (k8sServ)    btns = projectConfig.btns.update else btns = projectConfig.btns.install   }    //подбор кнопок для be else if (projectConfig.type === 'be') btns = projectConfig.btns.usual    //подбор кнопок для front else btns = projectConfig.btns.btn  body = {  branch,  service,  inst_target: stand,  }  autoDeploy(body, standNumber, projectConfig, buildName, btns) } })

Но откуда брать данные о сборках, которые нужно поставить? Их хранение я организовал в своей mongoDB. Сам объект имеет вид:

  • Стенд

  • Тип пачки сборки

  • Флаг

  • Массив сборок

Я набросал роуты: простенькую логику для добавления/удаления/изменения типов сборок. Мысль такая — типов сборок может храниться несколько (в зависимости от номера стенда и цели), их можно включать с помощью флага и условно можно разделить на regress/stable/other.

Для тех, кто не хочет слать запросики в Postman, я организовал тот же функционал через Jenkins pipeine: выбрал из выпадающих списков стенды/типы, передал объект сборок и нажал кнопочку.

Доработка решения, которая меня устроит

Тут я наконец-то могу сказать, что решение бесконечного опроса от job до момента, пока я не получу ее статус — такое себе удовольствие. Я бы хотел (WIP) вот как модифицировать логику:

Роут_1:

  • приходит сигнал поставить сборку;

  • создаем веточку;

  • создаем pipeline;

  • жмем первую кнопку.

Не опрашиваем никого сами — забыли о сделанном действии.

Роут_2:

  • от кнопки GitLab из post секции получили обращение:

    • ошибка — вывели ее;

    • успех — выкачали из JSON файла конфиг репозитория;

      • последняя кнопка — вывели сообщение об успехе;

      • не последняя кнопка — запустили нажатие  кнопки[index + 1].

Конечно, для работы такой схемы мне из GitLab нужно получать определенный список параметров.

Итог

Получилось существенно сократить по времени поставку RC перед регрессионным тестированием: вместо 1,5-3 часов ребятам нужно минут 15 вечером (собрать веточки) и минут 10 утром (посмотреть, что все действительно хорошо). Потратил я на это дело примерно недели 2-2,5 (с исследованием/разговорами/написанием кода/формированием полной картинки).

Порог входа для новых сотрудников/стажеров упал “в ноги”. Достаточно ознакомиться раз с инструкцией по пользованию, чтобы сразу начать использовать автонакатку.

Масштабируемость у текущей реализации приятная. Для добавления нового сервиса достаточно в JSON-файл прописать параметры, о которых я уже упоминал. 

Поддерживаемость по ощущениям средняя. До внесения правок в код нужно войти в контекст глубже, чем хотелось бы. Однако сейчас ведется разработка по образу идеи, описанной выше, что в том числе должно повлиять на поддерживаемость в позитивном ключе.

Если у вас есть опыт решения подобной задачи с использованием, например, другой логики, или ваша задача осложняется большими вводными — мне будет интересно это обсудить.


ссылка на оригинал статьи https://habr.com/ru/company/tele2/blog/720670/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *