С чего все началось
За более 3х летний срок существования продукта у нас собралось более чем 20 репозиториев со spark проектами. Процесс CICD был реализован на Jenkins. С определенного момента у GitLab CI появилась возможность создавать собственные CICD. Но долгое время я совершенно не воспринимал всерьез этот инструмент. Так как мне нравилось, что в Jenkins можно взять и дописать то чего тебе не хватает на Groovy. Настройка WebUI предоставляет широкие возможности для организации параметризованных сборок. Поначалу функционал GitlabCI я воспринимал это как жалкое подобие Jenkins: чтобы реализовать ну что-то очень очевидное и простое, я уже молчу про параметризованную сборку.
Но прошло время и мне показали как возможно шарить между проектами джобы, чтобы реализация под конкретный проект выглядела с наименьшим количеством кода.
Для примера у вас где-то в отдельном репозитории лежат yml, которые выполняют что-то вполне определенное, которое у вас может повторяться не только в одном проекте.
include:
- project: 'gitlabci/cicd'
ref: v1
file:
- 'pipelines/product1/.base_pipelines_spark_project.yml'
Выполнять include какого-то джоба у себя в конвейере и прям стало одной из киллер фич. И в какой-то момент перевод пайплайн на GitLabCI уже не выглядело как необходимость, а собственным желанием реализовать интересную задачу.
Что было
более 20 репозиториев в gitlab spark проектов;
часть из них работают со spark часть из них spark + kafka;
CICD реализован на Jenkins
Что хотелось сделать
Выполнить трансформацию CICD с Jenkins на Gitlab CI c наименьшим количеством шагов: чтобы команда разработки, если хотела бы вникнуть то могла это сделать, а если нет то было бы что-нибудь типа создать такой-то файл и скопировать туда такой-то yaml код и чтобы гарантированно заработало причем без активной помощи со стороны devops разработчика.
Начало
В каждом из Spark проектов реализовано было тестирование по одному из 2-х сценариев: с кафка или без. Описать сценарий в одном job было не возможно и поэтому были созданы 2 yaml, которые подключались следующим образом
include:
# PRODUCT1
- project: 'gitlabci/integration-test'
ref: v2
file:
- 'product1/etl/.base_integration_test.yml'
- 'product1/etl/.base_integration_test_with_kafka.yml'
Для того, чтобы .gitlab-ci.yml выглядел для каждого проекта одинаковым необходимо было придумать логику таким образом, чтобы пайплайн на основании семантического анализа кода в test/fixtures.py мог определить какой сценарий необходим. Решить эту задачу оказалось достаточно тривиальной задачей, первая проблема была дальше. Предполагалось создать job, который в процессе анализа определял переменную CICD_KAFKA_HOST либо в true либо в false
prepare_test:
script:
- export CICD_KAFKA_HOST=$(cat test/fixtures.py | grep KAFKA_HOST)
- >
if [ "$CICD_KAFKA_HOST" != "" ]; then
export CICD_KAFKA_HOST="true"
else
export CICD_KAFKA_HOST="false"
fi
- echo "CICD_KAFKA_HOST=$CICD_KAFKA_HOST" >> dotenv.env
artifacts:
reports:
dotenv:
- dotenv.env
и в последующих job нужно запускать тесты либо c кафка либо без. Но по ходу реализации выяснилось, что использовать rules нельзя, потому variables для rules определяются при старте пайплайна и не могут быть переопределены/изменены в процессе работы конвейера и расширения extends должны быть определены в пайплайне однозначным образом.
integration_test:
extends: .base_integration_test_with_kafka
rules:
- if: '$CICD_KAFKA_HOST == "true"'
Реализация идеи "smart" пайплайна первый раз подверглась сомнению. НО по на помощь должны были прийти триггеры.
Триггер
Триггер предоставляет возможность запустить в текущем пайплайне другой пайплайн. Текущий пайплайн становится родительским, а запускаемый другой пайплан ребенком.
Реализация получилась такой
# --------------- Prepare Test ---------------
prepare_test:
image: platform/docker-images/vault:1.8
variables:
CONTEXT_TEST: |
include:
# PRODUCT
- project: 'gitlabci/integration-test'
ref: v2
file:
- 'product1/etl/.base_integration_test.yml'
- 'product1/etl/.base_integration_test_with_kafka.yml'
integration_test:
variables:
COVERAGE_SOURCE: "./src"
INTEGRATION_TEST: |
$CONTEXT_TEST
extends: .base_integration_test
INTEGRATION_TEST_WITH_KAFKA: |
$CONTEXT_TEST
extends: .base_integration_test_with_kafka
stage: prepare_test
script:
- export CICD_KAFKA_HOST=$(cat test/fixtures.py | grep KAFKA_HOST)
- >
if [ "$CICD_KAFKA_HOST" != "" ]; then
export CICD_KAFKA_HOST="true"
echo "$INTEGRATION_TEST_WITH_KAFKA" >> test.yml
else
export CICD_KAFKA_HOST="false"
echo "$INTEGRATION_TEST" >> test.yml
fi
- env | sort -f
artifacts:
paths:
- test.yml
expire_in: 7200 seconds
# --------------- Integration test ---------------
integration_test:
stage: test
trigger:
include:
- artifact: test.yml
job: prepare_test
strategy: depend
В такой реализации обычный пайплайн трансформировался в мультипайплайн: родительский пайплайн инициировал запуск пайплайна-ребенка
Такие образом появилось smart начало: он умеет определять какой сценарий выбрать и в job с интеграционным тестированием переиспользует именно тот сценарий который необходим: либо с кафка либо без. Начало положено, НО возникла проблема №2: результатом выполнения pipeline ребенка - формирование coverage отчета, который не мультипайплайнах мы далее передаем в job c SonarQube. Решить задачу по передаче между job artifact в виде файлов как раньше было нельзя, вернуть artifact из child в parent оказалось невозможно.
Очевидное решение - добавить upload artifact в наш aftifactory и в job c SonarQube просто его скачать. Но хотелось найти более изящный способ, чтобы исключить дополнительные обращения к Artifactory. И способ был найден: Gitlab CI API
Gitlab CI API: download child artifacts
Чтобы иметь возможность подключаться к Gitlab CI API необходимо для пользователя, который имеет права на репозиторий сгенерировать token. Для того чтобы воспользоваться API скачать artifact из pipeline ребенка необходимо выяснить его CI_JOB_ID.
GET /projects/:id/jobs/:job_id/artifacts
Как это сделать из pipeline родителя?
- определяем ID pipeline ребенка
GET /projects/:id/pipelines/:pipeline_id/bridges
- по id pipeline ребенка определяем id job
GET /projects/:id/pipelines/:pipeline_id/jobs
- после этого уже выполняем скачивание методом /projects/:id/jobs/:job_id/artifacts
Итоговая реализация job по скачиванию artifacts будет выглядеть так: в список переменных группы проектов куда входит и наш репозиторий положили значение token - GITLAB_USER_TOKEN и для разбора json ответа от Gitlab API использовали jq
get_cicd_artifact:
image: platform/docker-images/ansible:2.9.24-9
stage: get_cicd_artifact
script:
- >
export CI_CHILD_PIPELINE_ID=$(curl --header "PRIVATE-TOKEN: $GITLAB_USER_TOKEN" "$CI_API_V4_URL/projects/$CI_PROJECT_ID/pipelines/$CI_PIPELINE_ID/bridges" | jq ".[].downstream_pipeline.id")
- echo $CI_CHILD_PIPELINE_ID
- >
export CI_CHILD_JOB_ID=$(curl --header "PRIVATE-TOKEN: $GITLAB_USER_TOKEN" "$CI_API_V4_URL/projects/$CI_PROJECT_ID/pipelines/$CI_CHILD_PIPELINE_ID/jobs" | jq '.[].id')
- echo $CI_CHILD_JOB_ID
- 'curl --output artifacts.zip --header "PRIVATE-TOKEN: $GITLAB_USER_TOKEN" "$CI_API_V4_URL/projects/$CI_PROJECT_ID/jobs/$CI_CHILD_JOB_ID/artifacts"'
- unzip artifacts.zip
- ls -las coverage-reports
- rm -rf artifacts.tar
dependencies:
- integration_test
artifacts:
paths:
- coverage-reports/
Таким образом удалось реализовать Multi-project пайплайн имхо со "smart" фичой