JUnit в GitLab CI с Kubernetes

Въпреки факта, че всеки знае отлично, че тестването на вашия софтуер е важно и необходимо и мнозина го правят автоматично от дълго време, в необятността на Habr нямаше нито една рецепта за създаване на комбинация от толкова популярни продукти в тази ниша като (любимите ни) GitLab и JUnit. Нека запълним тази празнина!

JUnit в GitLab CI с Kubernetes

Уводна

Първо, нека дам малко контекст:

  • Тъй като всички наши приложения работят на Kubernetes, ще обмислим провеждането на тестове на подходящата инфраструктура.
  • За сглобяване и разгръщане използваме werf (по отношение на инфраструктурните компоненти, това също автоматично означава, че Helm е включен).
  • Няма да навлизам в подробности за действителното създаване на тестове: в нашия случай клиентът сам пише тестовете и ние осигуряваме само тяхното стартиране (и наличието на съответен отчет в заявката за сливане).


Как ще изглежда общата последователност от действия?

  1. Изграждане на приложението – ще пропуснем описанието на този етап.
  2. Разположете приложението в отделно пространство от имена на клъстера Kubernetes и започнете да тествате.
  3. Търсене на артефакти и анализиране на JUnit отчети с GitLab.
  4. Изтриване на предварително създадено пространство от имена.

Сега - към изпълнение!

регулиране

GitLab CI

Да започнем с един фрагмент .gitlab-ci.yaml, който описва внедряването на приложението и провеждането на тестове. Списъкът се оказа доста обемен, така че беше старателно допълнен с коментари:

variables:
# объявляем версию werf, которую собираемся использовать
  WERF_VERSION: "1.0 beta"

.base_deploy: &base_deploy
  script:
# создаем namespace в K8s, если его нет
    - kubectl --context="${WERF_KUBE_CONTEXT}" get ns ${CI_ENVIRONMENT_SLUG} || kubectl create ns ${CI_ENVIRONMENT_SLUG}
# загружаем werf и деплоим — подробнее об этом см. в документации
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#deploy-stage)
    - type multiwerf && source <(multiwerf use ${WERF_VERSION})
    - werf version
    - type werf && source <(werf ci-env gitlab --tagging-strategy tag-or-branch --verbose)
    - werf deploy --stages-storage :local
      --namespace ${CI_ENVIRONMENT_SLUG}
      --set "global.commit_ref_slug=${CI_COMMIT_REF_SLUG:-''}"
# передаем переменную `run_tests`
# она будет использоваться в рендере Helm-релиза
      --set "global.run_tests=${RUN_TESTS:-no}"
      --set "global.env=${CI_ENVIRONMENT_SLUG}"
# изменяем timeout (бывают долгие тесты) и передаем его в релиз
      --set "global.ci_timeout=${CI_TIMEOUT:-900}"
     --timeout ${CI_TIMEOUT:-900}
  dependencies:
    - Build

.test-base: &test-base
  extends: .base_deploy
  before_script:
# создаем директорию для будущего отчета, исходя из $CI_COMMIT_REF_SLUG
    - mkdir /mnt/tests/${CI_COMMIT_REF_SLUG} || true
# вынужденный костыль, т.к. GitLab хочет получить артефакты в своем build-dir’е
    - mkdir ./tests || true
    - ln -s /mnt/tests/${CI_COMMIT_REF_SLUG} ./tests/${CI_COMMIT_REF_SLUG}
  after_script:
# после окончания тестов удаляем релиз вместе с Job’ом
# (и, возможно, его инфраструктурой)
    - type multiwerf && source <(multiwerf use ${WERF_VERSION})
    - werf version
    - type werf && source <(werf ci-env gitlab --tagging-strategy tag-or-branch --verbose)
    - werf dismiss --namespace ${CI_ENVIRONMENT_SLUG} --with-namespace
# мы разрешаем падения, но вы можете сделать иначе
  allow_failure: true
  variables:
    RUN_TESTS: 'yes'
# задаем контекст в werf
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#infrastructure)
    WERF_KUBE_CONTEXT: 'admin@stage-cluster'
  tags:
# используем раннер с тегом `werf-runner`
    - werf-runner
  artifacts:
# требуется собрать артефакт для того, чтобы его можно было увидеть
# в пайплайне и скачать — например, для более вдумчивого изучения
    paths:
      - ./tests/${CI_COMMIT_REF_SLUG}/*
# артефакты старше недели будут удалены
    expire_in: 7 day
# важно: эти строки отвечают за парсинг отчета GitLab’ом
    reports:
      junit: ./tests/${CI_COMMIT_REF_SLUG}/report.xml

# для упрощения здесь показаны всего две стадии
# в реальности же у вас их будет больше — как минимум из-за деплоя
stages:
  - build
  - tests

build:
  stage: build
  script:
# сборка — снова по документации по werf
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#build-stage)
    - type multiwerf && source <(multiwerf use ${WERF_VERSION})
    - werf version
    - type werf && source <(werf ci-env gitlab --tagging-strategy tag-or-branch --verbose)
    - werf build-and-publish --stages-storage :local
  tags:
    - werf-runner
  except:
    - schedules

run tests:
  <<: *test-base
  environment:
# "сама соль" именования namespace’а
# (https://docs.gitlab.com/ce/ci/variables/predefined_variables.html)
    name: tests-${CI_COMMIT_REF_SLUG}
  stage: tests
  except:
    - schedules

Kubernetes

Сега в директорията .helm/templates нека създадем YAML с Job - tests-job.yaml — за провеждане на тестове и необходимите ресурси на Kubernetes. Вижте обясненията след изброяването:

{{- if eq .Values.global.run_tests "yes" }}
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: tests-script
data:
  tests.sh: |
    echo "======================"
    echo "${APP_NAME} TESTS"
    echo "======================"

    cd /app
    npm run test:ci
    cp report.xml /app/test_results/${CI_COMMIT_REF_SLUG}/

    echo ""
    echo ""
    echo ""

    chown -R 999:999 /app/test_results/${CI_COMMIT_REF_SLUG}
---
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ .Chart.Name }}-test
  annotations:
    "helm.sh/hook": post-install,post-upgrade
    "helm.sh/hook-weight": "2"
    "werf/watch-logs": "true"
spec:
  activeDeadlineSeconds: {{ .Values.global.ci_timeout }}
  backoffLimit: 1
  template:
    metadata:
      name: {{ .Chart.Name }}-test
    spec:
      containers:
      - name: test
        command: ['bash', '-c', '/app/tests.sh']
{{ tuple "application" . | include "werf_container_image" | indent 8 }}
        env:
        - name: env
          value: {{ .Values.global.env }}
        - name: CI_COMMIT_REF_SLUG
          value: {{ .Values.global.commit_ref_slug }}
       - name: APP_NAME
          value: {{ .Chart.Name }}
{{ tuple "application" . | include "werf_container_env" | indent 8 }}
        volumeMounts:
        - mountPath: /app/test_results/
          name: data
        - mountPath: /app/tests.sh
          name: tests-script
          subPath: tests.sh
      tolerations:
      - key: dedicated
        operator: Exists
      - key: node-role.kubernetes.io/master
        operator: Exists
      restartPolicy: OnFailure
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: {{ .Chart.Name }}-pvc
      - name: tests-script
        configMap:
          name: tests-script
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: {{ .Chart.Name }}-pvc
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 10Mi
  storageClassName: {{ .Chart.Name }}-{{ .Values.global.commit_ref_slug }}
  volumeName: {{ .Values.global.commit_ref_slug }}

---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: {{ .Values.global.commit_ref_slug }}
spec:
  accessModes:
  - ReadWriteOnce
  capacity:
    storage: 10Mi
  local:
    path: /mnt/tests/
  nodeAffinity:
   required:
     nodeSelectorTerms:
     - matchExpressions:
       - key: kubernetes.io/hostname
         operator: In
         values:
         - kube-master
  persistentVolumeReclaimPolicy: Delete
  storageClassName: {{ .Chart.Name }}-{{ .Values.global.commit_ref_slug }}
{{- end }}

Какъв вид ресурси описани в тази конфигурация? При внедряването ние създаваме уникално пространство от имена за проекта (това е посочено в .gitlab-ci.yaml - tests-${CI_COMMIT_REF_SLUG}) и го разточете:

  1. ConfigMap с тестов скрипт;
  2. Работа с описание на подс и посочената директива command, който само изпълнява тестовете;
  3. PV и PVC, които ви позволяват да съхранявате тестови данни.

Обърнете внимание на уводното условие с if в началото на манифеста - съответно другите YAML файлове на диаграмата на Helm с приложението трябва да бъдат обвити в обратен проектирайте така, че да не се разгръщат по време на тестване. Това е:

{{- if ne .Values.global.run_tests "yes" }}
---
я другой ямлик
{{- end }}

Въпреки това, ако тестовете изискват известна инфраструктура (например Redis, RabbitMQ, Mongo, PostgreSQL...) - техните YAML могат да бъдат не изключи. Разположете ги и в тестова среда... като ги коригирате както сметнете за добре, разбира се.

Последно докосване

защото асемблирането и внедряването с помощта на werf работи засега само на сървъра за изграждане (с gitlab-runner) и подът с тестове се стартира на главния, ще трябва да създадете директория /mnt/tests върху майстора и го дай на бегача, например чрез NFS. Подробен пример с обяснения можете да намерите в K8s документация.

Резултатът ще бъде:

user@kube-master:~$ cat /etc/exports | grep tests
/mnt/tests    IP_gitlab-builder/32(rw,nohide,insecure,no_subtree_check,sync,all_squash,anonuid=999,anongid=998)

user@gitlab-runner:~$ cat /etc/fstab | grep tests
IP_kube-master:/mnt/tests    /mnt/tests   nfs4    _netdev,auto  0       0

Никой не забранява да правите споделяне на NFS директно на gitlab-runner и след това да го монтирате в pods.

Внимание

Може би се питате защо да усложнявате всичко, като създавате Job, ако можете просто да стартирате скрипт с тестове директно в shell runner-а? Отговорът е доста тривиален...

Някои тестове изискват достъп до инфраструктурата (MongoDB, RabbitMQ, PostgreSQL и др.), за да се провери дали работят правилно. Ние правим тестването унифицирано - с този подход става лесно включването на такива допълнителни обекти. В допълнение към това получаваме стандарт подход за внедряване (дори ако използвате NFS, допълнително монтиране на директории).

Резултат

Какво ще видим, когато приложим подготвената конфигурация?

Заявката за сливане ще покаже обобщена статистика за тестове, изпълнявани в последния й конвейер:

JUnit в GitLab CI с Kubernetes

Всяка грешка може да бъде кликната тук за подробности:

JUnit в GitLab CI с Kubernetes

NB: Внимателният читател ще забележи, че тестваме приложение NodeJS, а на екранните снимки - .NET... Не се изненадвайте: просто при подготовката на статията не бяха открити грешки при тестването на първото приложение, но те са намерени в друг.

Заключение

Както можете да видите, нищо сложно!

По принцип, ако вече имате колектор на обвивки и той работи, но не се нуждаете от Kubernetes, прикачването на тестване към него ще бъде дори по-проста задача от описаната тук. И в GitLab CI документация ще намерите примери за Ruby, Go, Gradle, Maven и някои други.

PS

Прочетете също в нашия блог:

Източник: www.habr.com

Добавяне на нов коментар