JUnit i GitLab CI med Kubernetes

Trots det faktum att alla mycket väl vet att det är viktigt och nödvändigt att testa din mjukvara, och många har gjort det automatiskt under lång tid, fanns det inte ett enda recept för att sätta upp en kombination av så populära produkter i Habr. denna nisch som (vår favorit) GitLab och JUnit . Låt oss fylla denna lucka!

JUnit i GitLab CI med Kubernetes

inledande

Låt mig först ge lite sammanhang:

  • Eftersom alla våra applikationer körs på Kubernetes kommer vi att överväga att köra tester på lämplig infrastruktur.
  • För montering och driftsättning använder vi werf (när det gäller infrastrukturkomponenter betyder detta också automatiskt att Helm är inblandat).
  • Jag kommer inte att gå in på detaljerna för det faktiska skapandet av tester: i vårt fall skriver kunden testerna själv, och vi säkerställer bara att de lanseras (och närvaron av en motsvarande rapport i sammanslagningsbegäran).


Hur kommer den allmänna handlingsföljden att se ut?

  1. Bygga applikationen - vi kommer att utelämna beskrivningen av detta steg.
  2. Distribuera programmet till ett separat namnområde i Kubernetes-klustret och börja testa.
  3. Söker efter artefakter och analyserar JUnit-rapporter med GitLab.
  4. Ta bort ett tidigare skapat namnområde.

Nu - till genomförandet!

justering

GitLab CI

Låt oss börja med ett fragment .gitlab-ci.yaml, som beskriver driftsättning av programmet och körning av tester. Listan visade sig vara ganska omfattande, så den kompletterades grundligt 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

Nu i katalogen .helm/templates låt oss skapa YAML med Job - tests-job.yaml — att köra tester och de Kubernetes-resurser som behövs. Se förklaringar efter listning:

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

Vilken typ av resurser beskrivs i den här konfigurationen? Vid implementering skapar vi ett unikt namnområde för projektet (detta anges i .gitlab-ci.yaml - tests-${CI_COMMIT_REF_SLUG}) och rulla ut den:

  1. ConfigMap med testskript;
  2. Jobb med en beskrivning av podden och det specificerade direktivet command, som bara kör testerna;
  3. PV och PVC, som låter dig lagra testdata.

Var uppmärksam på det inledande villkoret med if i början av manifestet - följaktligen måste andra YAML-filer i Helm-diagrammet med applikationen vara inslagna i omvänd designa så att de inte distribueras under testning. Det är:

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

Men om testerna kräver viss infrastruktur (till exempel Redis, RabbitMQ, Mongo, PostgreSQL...) - deras YAML kan vara ingen Stäng av. Distribuera dem i en testmiljö också... justera dem som du tycker är lämpligt, förstås.

Slutlig touch

Därför att montering och driftsättning med werf fungerar för närvarande endast på byggservern (med gitlab-runner), och podden med tester startas på mastern, måste du skapa en katalog /mnt/tests på mästaren och ge den till löparen, till exempel via NFS. Ett detaljerat exempel med förklaringar finns i K8s dokumentation.

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 förbjuder att göra en NFS-share direkt på gitlab-runner och sedan montera den i pods.

Notera

Du kanske frågar varför komplicera allt genom att skapa ett jobb om du helt enkelt kan köra ett skript med tester direkt på skallöparen? Svaret är ganska trivialt...

Vissa tester kräver åtkomst till infrastrukturen (MongoDB, RabbitMQ, PostgreSQL, etc.) för att verifiera att de fungerar korrekt. Vi gör testning enhetlig - med detta tillvägagångssätt blir det lätt att inkludera sådana ytterligare enheter. Utöver detta får vi standard distributionsmetod (även om du använder NFS, ytterligare montering av kataloger).

Resultat

Vad kommer vi att se när vi tillämpar den förberedda konfigurationen?

Sammanslagningsbegäran kommer att visa sammanfattande statistik för tester som körs i dess senaste pipeline:

JUnit i GitLab CI med Kubernetes

Varje fel kan klickas här för detaljer:

JUnit i GitLab CI med Kubernetes

NB: Den uppmärksamma läsaren kommer att märka att vi testar en NodeJS-applikation, och i skärmdumparna - .NET... Bli inte förvånad: precis som en del av förberedelsen av artikeln hittades inga fel vid testning av den första applikationen, men de hittades i en annan.

Slutsats

Som du kan se, inget komplicerat!

I princip, om du redan har en skalsamlare och den fungerar, men du inte behöver Kubernetes, blir det en ännu enklare uppgift att koppla testning till den än vad som beskrivs här. Och i GitLab CI-dokumentation du hittar exempel för Ruby, Go, Gradle, Maven och några andra.

PS

Läs även på vår blogg:

Källa: will.com

Lägg en kommentar