JUnit i GitLab CI med Kubernetes

Til tross for at alle godt vet at det å teste programvaren din er viktig og nødvendig, og mange har gjort det automatisk i lang tid, var det ikke en eneste oppskrift for å sette opp en kombinasjon av slike populære produkter i Habrs vidder. denne nisjen som (vår favoritt) GitLab og JUnit . La oss fylle dette gapet!

JUnit i GitLab CI med Kubernetes

Innledende

Først, la meg gi litt kontekst:

  • Siden alle applikasjonene våre kjører på Kubernetes, vil vi vurdere å kjøre tester på riktig infrastruktur.
  • For montering og distribusjon bruker vi werf (i forhold til infrastrukturkomponenter betyr dette også automatisk at Helm er involvert).
  • Jeg vil ikke gå inn på detaljene i den faktiske opprettelsen av tester: i vårt tilfelle skriver klienten testene selv, og vi sikrer bare lanseringen av dem (og tilstedeværelsen av en tilsvarende rapport i sammenslåingsforespørselen).


Hvordan vil den generelle handlingsrekkefølgen se ut?

  1. Bygge applikasjonen - vi vil utelate beskrivelsen av dette stadiet.
  2. Distribuer applikasjonen til et eget navneområde i Kubernetes-klyngen og begynn å teste.
  3. Søker etter artefakter og analyserer JUnit-rapporter med GitLab.
  4. Sletter et tidligere opprettet navneområde.

Nå - til implementering!

justering

GitLab CI

La oss starte med et fragment .gitlab-ci.yaml, som beskriver distribusjon av applikasjonen og kjøring av tester. Oppføringen viste seg å være ganske omfangsrik, så den ble grundig supplert med kommentarer:

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

Nå i katalogen .helm/templates la oss lage YAML med Job - tests-job.yaml — å kjøre tester og Kubernetes-ressursene den trenger. Se forklaringer etter oppføringen:

{{- 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 }}

Hva slags ressurser beskrevet i denne konfigurasjonen? Ved distribusjon oppretter vi et unikt navneområde for prosjektet (dette er indikert i .gitlab-ci.yaml - tests-${CI_COMMIT_REF_SLUG}) og rull den ut:

  1. ConfigMap med testskript;
  2. Jobb med en beskrivelse av poden og det spesifiserte direktivet command, som bare kjører testene;
  3. PV og PVC, som lar deg lagre testdata.

Vær oppmerksom på den innledende betingelsen med if i begynnelsen av manifestet - følgelig må andre YAML-filer i Helm-diagrammet med applikasjonen pakkes inn omvendt design slik at de ikke blir distribuert under testing. Det er:

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

Men hvis testene krever litt infrastruktur (for eksempel Redis, RabbitMQ, Mongo, PostgreSQL...) - deres YAML-er kan være no skru av. Distribuer dem i et testmiljø også... juster dem etter eget ønske, selvfølgelig.

Endelig berøring

Fordi montering og distribusjon ved hjelp av werf fungerer foreløpig bare på byggeserveren (med gitlab-runner), og poden med tester er lansert på masteren, må du opprette en katalog /mnt/tests på mesteren og gi den til løperen, for eksempel via NFS. Et detaljert eksempel med forklaringer finner du i K8s dokumentasjon.

Resultatet blir:

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

Ingen forbyr å lage en NFS-andel direkte på gitlab-runner, og deretter montere den i pods.

Note

Du spør kanskje hvorfor komplisere alt ved å lage en jobb hvis du bare kan kjøre et skript med tester direkte på shell-løperen? Svaret er ganske trivielt...

Noen tester krever tilgang til infrastrukturen (MongoDB, RabbitMQ, PostgreSQL, etc.) for å bekrefte at de fungerer som de skal. Vi gjør testing enhetlig - med denne tilnærmingen blir det enkelt å inkludere slike tilleggsenheter. I tillegg til dette får vi standard distribusjonstilnærming (selv om du bruker NFS, ekstra montering av kataloger).

Resultat

Hva vil vi se når vi bruker den forberedte konfigurasjonen?

Sammenslåingsforespørselen vil vise sammendragsstatistikk for tester som er kjørt i sin siste pipeline:

JUnit i GitLab CI med Kubernetes

Hver feil kan klikkes her for detaljer:

JUnit i GitLab CI med Kubernetes

NB: Den oppmerksomme leseren vil legge merke til at vi tester en NodeJS-applikasjon, og i skjermbildene - .NET... Ikke bli overrasket: det er bare at under utarbeidelsen av artikkelen ble det ikke funnet noen feil ved testing av den første applikasjonen, men de ble funnet i en annen.

Konklusjon

Som du kan se, ingenting komplisert!

I prinsippet, hvis du allerede har en skallsamler og den fungerer, men du ikke trenger Kubernetes, vil det være en enda enklere oppgave å feste testing til den enn beskrevet her. Og i GitLab CI-dokumentasjon du finner eksempler for Ruby, Go, Gradle, Maven og noen andre.

PS

Les også på bloggen vår:

Kilde: www.habr.com

Legg til en kommentar