
С чего все началось
За более 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" фичой

