[recovery mode] Gitlab CI «Smart» Pipeline: родители и дети

Раньше даже не мог себе представить, что между Русской классикой и современными CICD инструментами есть связь)Раньше даже не мог себе представить, что между Русской классикой и современными CICD инструментами есть связь)

С чего все началось

За более 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

В такой реализации обычный пайплайн трансформировался в мультипайплайн: родительский пайплайн инициировал запуск пайплайна-ребенка

Gitlab CI: Pipeline родитель, у которого есть pipeline ребенокGitlab CI: Pipeline родитель, у которого есть pipeline ребенок

Такие образом появилось 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» фичой

Gitlab CI Multi-project pipelinesGitlab CI Multi-project pipelines

© Habrahabr.ru