В статье решается задача управления описанием сборки для большого количества однотипных приложений. Чтобы в проекте заработал GitLab CI, нужно в репозиторий добавить файл
.gitlab-ci.yml
. Но что, если в сотне репозиториев это файл с одинаковым содержимым? Даже если разложить его по репозиториям один раз, то как его потом изменять? А что, если одного .gitlab-ci.yml
мало для сборки — нужны Dockerfile
или Dappfile
, разные скрипты и структура YAML-файлов для Helm? Как обновлять их?С чего начать решение задачи по сборке сотни однотипных приложений? Конечно же, посмотреть, можно ли GitLab CI указать использовать
.gitlab-ci.yml
из другого репозитория или компоновать .gitlab-ci.yml
из файлов в других репозиториях…В поисках такой возможности сразу всплывают следующие issues:
- Add 'include' key to .gitlab-ci.yml;
- Import/include common CI/CD content from one project into another's `.gitlab-ci.yml` (EE only?);
- Add includes capability in gitlab-ci YAML format.
Видно, что возможность иметь какой-то общий
.gitlab-ci.yml
интересует сообщество. Решение добавить include секций из файла в другом репозитории кажется очень простым: оно основано на многолетней практике программирования и будет понятно любому. Однако include как концепция хорошо работает в случае с деревом исходников, но в случае нескольких Git-репозиториев в этом решении можно увидеть такие минусы:- В include надо указывать, из какой ветки брать файл для подключения, поэтому сборка не будет воспроизводиться.
- В include надо указывать, из какой ветки брать файл для подключения, поэтому нужно кэшировать эффективный
.gitlab-ci.yml
, хранить его и пересобирать уже на основе него. - В некоторых проектах нужно решить пункт 1, а в некоторых — пункт 2, однако они взаимоисключающи.
- Если в подключаемом файле что-то поменялось, то по сути изменяется
.gitlab-ci.yml
того проекта, который собирается, но истории изменений не будет видно.
Для случая с однотипными приложениями добавляются ещё два минуса:
- Проблема с сотней одинаковых
.gitlab-ci.yml
остаётся. - Проблема с обновлением дополнительных файлов тоже остаётся.
Взгляд под другим углом
Решение с include — это pull-модель, т.е. проект при сборке вытягивает часть конфигурации CI. Если заменить pull на push, то получится так:
- создаётся проект common-ci-config, в котором хранится общий
.gitlab-ci.yml
и другие файлы, необходимые для сборки; - создаётся пользователь gitlab-ci-distributor, которому даются права на push (Master) в нужные проекты.
Этот вариант работает следующим образом: в проекте common-ci-config хранится общий для сотни других проектов файл
.gitlab-ci.yml
. При изменении этого файла от пользователя gitlab-ci-distributor рассылаются коммиты в другие проекты.Для файлов сборки можно выбрать: либо добавлять их в коммит, либо в
.gitlab-ci.yml
у проектов, в задачу сборки, добавить git clone
проекта common-ci-config.Плюсы такого подхода:
- В каждом проекте становится видно, когда изменился
.gitlab-ci.yml
и кто его изменил. Пропадает проблема хранения эффективного.gitlab-ci.yml
, т.к. в каждом проекте всегда видно полную версию без include. - В проектах, где не нужна последняя версия сборочных файлов на момент сборки, сборочные файлы добавляются коммитом.
- В проектах, где на момент сборки всегда нужна последняя версия, сборочные файлы клонируются.
- Можно часть файлов добавлять в коммит, а часть — использовать из клонированной копии.
- Можно реализовать концепцию include для
.gitlab-ci.yml
с помощью вызова скрипта. То есть, если нужно, чтобы.gitlab-ci.yml
при сборке всегда использовал последнюю версию конфигурации тестирования, то тестирование выносится в скрипт в проекте common-ci-config.
GitLab API
Итак, проблема обозначена и есть вариант решения. Для продолжения нужно рассказать о GitLab API (документация на сайте GitLab). Понадобятся следующие методы:
- Коммит файла. API предлагает два варианта: изменение одного файла и конструктор коммита с несколькими файлами. Второй вариант чуть сложнее, но у него больше возможностей.
- Получение списка проектов. Этот метод поможет узнать, в какие проекты нужно делать коммиты.
- Информация о текущем коммите. Метод понадобится, чтобы скопировать сообщение коммита и его автора из общего репозитория в остальные.
Методы API можно вызывать с помощью curl, а JSON, который приходит в ответ, обрабатывать с помощью jq (документация по фильтрам).
Для вызова методов понадобится создать access token. Об этом будет дальше в статье, а пока — пример того, как получать список проектов в группе:
$ curl -s --header "PRIVATE-TOKEN: $TOKEN" https://gitlab.example.com/api/v4/groups/group-of-alike-projects/projects?simple=true | jq -r '.[] | "\(.path_with_namespace)\t\(.id)"'
group-of-alike-projects/project-pasiphae 7
group-of-alike-projects/project-megaclite 6
group-of-alike-projects/project-helike 5
group-of-alike-projects/project-erinome 4
group-of-alike-projects/project-callisto 3
group-of-alike-projects/project-aitne 2
group-of-alike-projects/project-adrastea 1
Настройка GitLab
Вызов методов API невозможен без авторизации. GitLab предлагает авторизацию через access tokens. Чтобы получить такой токен, нужно создать отдельного пользователя, которому будут даны права на управление нужными репозиториями. Пусть это будет пользователь gitlab-ci-distributor:
Далее нужно стать этим пользователем и создать access token:
Для доступа к проектам, где нужно управлять сборочными файлами, нужно добавить пользователя gitlab-ci-distributor в группу:
Общие для проектов файлы будут храниться в проекте сommon-ci-config. Проект нужно создать в отдельной группе — например, infra. В настройках проекта добавляется секретная переменная со значением полученного токена:
Описанные действия выполняются администратором один раз. Далее вся настройка производится через файлы в репозитории common-ci-config.
Репозиторий common-ci-config
Теперь можно протестировать работу с API через GitLab CI. Для этого в проект common-ci-config добавляется простой
.gitlab-ci.yml
:stages:
- distribute
distribute:
stage: distribute
script:
- ./distribute.sh
… и скрипт
distribute.sh
, который пока покажет информацию о коммите и проекты из выбранной группы:#!/usr/bin/env bash
curl -s --header "PRIVATE-TOKEN: $DISTRIBUTOR_TOKEN" https://gitlab.example.com/api/v4/projects/infra%2Fcommon-ci-config/repository/commits/$CI_COMMIT_SHA | jq '.'
curl -s --header "PRIVATE-TOKEN: $DISTRIBUTOR_TOKEN" https://gitlab.example.com/api/v4/groups/group-of-alike-projects/projects?simple=true | jq -r '.[] | "\(.path_with_namespace)\t\(.id)"'
Результат выполнения задания distribute:
Running with gitlab-runner 10.1.0 (c1ecf97f)
on gitlab (d82a6d8f)
Using Shell executor...
Running on gitlab...
Fetching changes...
HEAD is now at 08dcc92 Initial .gitlab-ci.yml and distribute.sh
Checking out 08dcc92a as master...
Skipping Git submodules setup
$ ./distribute.sh
{
"id": "08dcc92abf0d951194ad1ffcc23deeb875855320",
"short_id": "08dcc92a",
"title": "Initial .gitlab-ci.yml and distribute.sh",
"created_at": "2017-10-25T16:35:15.000+03:00",
"parent_ids": [
"d9bdea91d081025c2af658209f23f684c96b5cee"
],
"message": "Initial .gitlab-ci.yml and distribute.sh\n",
"author_name": "root root",
"author_email": "root.root@gitlab.example.com",
"authored_date": "2017-10-25T16:35:15.000+03:00",
"committer_name": "root root",
"committer_email": "root.root@gitlab.example.com",
"committed_date": "2017-10-25T16:35:15.000+03:00",
"stats": {
"additions": 0,
"deletions": 0,
"total": 0
},
"status": "running",
"last_pipeline": {
"id": 2,
"sha": "08dcc92abf0d951194ad1ffcc23deeb875855320",
"ref": "master",
"status": "running"
}
}
group-of-alike-projects/project-pasiphae 7
group-of-alike-projects/project-megaclite 6
group-of-alike-projects/project-helike 5
group-of-alike-projects/project-erinome 4
group-of-alike-projects/project-callisto 3
group-of-alike-projects/project-aitne 2
group-of-alike-projects/project-adrastea 1
Job succeeded
Доработка скрипта distribute.sh
Скрипт будет распространять общий файл
.gitalb-ci.yml
. Чтобы не путать его с .gitlab-ci.yml
проекта common-ci-config, файл расположен в директории common
. В файле описывается простое автоматическое задание:# common/.gitlab-ci.yml
stages:
- build
build:
stage: build
script:
- echo Building project $CI_PROJECT_PATH
В скрипте
distribute.sh
уже есть получение информации о коммите и получение списка проектов. Чтобы в проекты попадал красивый коммит, нужно выделить имя и почту автора и полное сообщение коммита. Также нужно добавить цикл по полученным проектам и для каждого проекта вызвать метод, создающий коммит.Доработанный
distribute.sh
:#!/usr/bin/env bash
COMMIT_INFO=$(curl -s --header "PRIVATE-TOKEN: $DISTRIBUTOR_TOKEN" https://gitlab.example.com/api/v4/projects/infra%2Fcommon-ci-config/repository/commits/$CI_COMMIT_SHA)
# Сообщение коммита может быть многострочным, поэтому jq без -r
MESSAGE=$(echo "$COMMIT_INFO" | jq '.message')
AUTHOR_NAME=$(echo "$COMMIT_INFO" | jq -r '.author_name')
AUTHOR_EMAIL=$(echo "$COMMIT_INFO" | jq -r '.author_email')
CONTENT=$(base64 -w0 common/.gitlab-ci.yml)
PAYLOAD=$(cat <<- JSON
{
"branch": "master",
"commit_message": $MESSAGE,
"author_name": "$AUTHOR_NAME",
"author_email": "$AUTHOR_EMAIL",
"actions": [
{ "action": "update",
"file_path": ".gitlab-ci.yml",
"content": "$CONTENT",
"encoding": "base64"
}
]
}
JSON
)
echo "$PAYLOAD"
curl -s --header "PRIVATE-TOKEN: $DISTRIBUTOR_TOKEN" https://gitlab.example.com/api/v4/groups/group-of-alike-projects/projects?simple=true | jq -r '.[] | "\(.path_with_namespace)\t\(.id)"' | while read project
do
name=`echo $project | awk '{print $1}'`
id=`echo $project | awk '{print $2}'`
echo Update project $name
curl -s --request POST --header "PRIVATE-TOKEN: $DISTRIBUTOR_TOKEN" --header "Content-Type: application/json" --data "$PAYLOAD" https://gitlab.example.com/api/v4/projects/$id/repository/commits
done
echo Stop
Результат выполнения задания distribute:
В проекте group-of-alike-projects/project-pasiphae коммит будет выглядеть так:
Результат выполнения задания build в проекте group-of-alike-projects/project-pasiphae:
Видно, что пользователь, который запускает задание, — gitlab-ci-distributor. Но при этом автор коммита — пользователь, который сделал коммит в common-ci-config.
Отключение одновременной автоматической сборки
Скрипт
distribute.sh
добавляет коммиты сразу в несколько проектов. Это приводит к созданию новых pipeline и одновременному запуску заданий на сборку. Такой эффект не всегда нужен. Чтобы коммит, обновляющий .gitlab-ci.yml
, не запускал сборку, можно вначале задания поставить условие с предупреждающим сообщением:script:
- 'if [ "x$GITLAB_USER_NAME" == "xgitlab-ci-distributor" ] ; then echo -e "\033[0;31m\n\nАвтоматическая сборка после обновления .gitlab-ci.yml отключена.\n\n\033[0m"; exit 1; fi'
Внимание! Переменная
GITLAB_USER_NAME
появилась в GitLab 10.0 (релиз от 22 сентября 2017). В более ранних версиях есть только GITLAB_USER_ID
и для условия придётся использовать ID пользователя. Этот ID можно узнать, например, выполнив задание со script: [export]
или с таким запросом к API:curl -s --header "PRIVATE-TOKEN: $DISTRIBUTOR_TOKEN" https://gitlab.example.com/api/v4/users?username=gitlab-ci-distributor | jq '.[] | .id'
Результат:
Если запустить это задание ещё раз, но от обычного пользователя, то всё выполнится успешно:
Заключение
В целом данной информации достаточно для того, чтобы дальше самостоятельно экспериментировать с массовым управлением проектами.
Для простоты экспериментов и повторения того, что описано в статье, можно установить GitLab в виртуальной машине, например, с помощью проекта gitlab-vagrant. Учтите, что придётся исправить
Vagrantfile
: сменить базовый образ на ubuntu/xenial64
и увеличить память vb.memory = "3072"
. А после запуска добавить gitlab-runner по инструкции.При разработке решения использовались следующие ресурсы:
P.S.
Читайте также в нашем блоге (и подписывайтесь, чтобы не пропустить новые публикации!):
Комментарии (6)
scor2k
26.10.2017 21:59Что бы не запускать сборку надо в сообщении к коммиту написать в любом месте “[CI SKIP]”. Это, по-моему, немного проще :)
diafour Автор
27.10.2017 13:24Это уже вопрос требований в конкретной задаче. Дело в том, что по коммиту с [CI SKIP] pipeline создаётся без задач. Поэтому, чтобы запустить сборку с новой конфигурацией, потребуется, например, добавить новый коммит или подмёржить master в ветку release — повторюсь, всё зависит от требований и от принятого на проекте процесса внесения изменений.
P.S. Навесить тэг на прилетевший коммит с [ci skip] — не вариант, pipeline тоже будет пустой.
Pipelinesallburov
29.10.2017 18:35Интересное решение, как раз сейчас изучаем возможности gitlab-ci, чтобы переехать на него.
Верно понимаю, что разработка идёт в мастер-ветках и новый yml коммитится именно туда?diafour Автор
30.10.2017 13:53+1В проекте, для которого создавалось решение — да, достаточно коммита в master, а из него cherry-pick в feature-ветку.
Если проекты не совсем однотипны, есть отклонения, то можно в переменных проекта указывать особенности, а в distribute.sh получать переменные через API: https://docs.gitlab.com/ce/api/project_level_variables.html
Например, где-то нужно в master сделать коммит, а где-то в develop.
SlavikF
Возникает чувство, что статья написана несколько абстрактно. Может быть стоило вначале более чётко обозначить проблему, которую вы пытались решить? А то возникает подозрение, что здесь приведён список костылей для конкретной ситуации и не совсем понятно, что я могу взять из этого для себя.
diafour Автор
Есть сотня проектов, у которых один и тот же .gitlab-ci.yml, Dappfile и папка с helm chart-ом. Задача — обновлять эти файлы, желательно из одного репозитория.
На практике это может быть что угодно, лишь бы этого много и не хотелось бегать по репозиториям, обновлять файлы. Например, статические сайты на jekyll, собираемые в docker-образ (сайты правят отдельные команды, за сборку отвечает одна команда). Внутренние java-библиотеки, собираемые в jar-артефакты, да вообще любые компоненты, которые не требуют индивидуальных правил сборки (тоже самое — компоненты правят разные люди, сборкой занимается одна команда).