JUnit am GitLab CI mat Kubernetes

Trotz der Tatsaach, datt jidderee ganz gutt weess datt d'Tester vun Ärer Software wichteg an néideg ass, a vill hunn et automatesch fir eng laang Zäit gemaach, an der Wäisheet vun Habr gouf et keen eenzegt Rezept fir eng Kombinatioun vun esou populäre Produkter opzestellen. dës Nisch als (eise Liiblings) GitLab an JUnit. Loosst eis dës Lück ausfëllen!

JUnit am GitLab CI mat Kubernetes

Aféierung

Als éischt, loosst mech e puer Kontext ginn:

  • Well all eis Uwendungen op Kubernetes lafen, wäerte mir iwwerleeën Tester op déi entspriechend Infrastruktur ze lafen.
  • Fir Assemblée an Deployment benotze mir werf (wat d'Infrastrukturkomponenten ugeet, heescht dat och automatesch datt Helm involvéiert ass).
  • Ech wäert net an d'Detailer vun der aktueller Schafung vun Tester goen: an eisem Fall schreift de Client d'Tester selwer, a mir garantéieren nëmmen hir Start (an d'Präsenz vun engem entspriechende Bericht an der Fusiounsufro).


Wéi gesäit déi allgemeng Sequenz vun Aktiounen aus?

  1. D'Applikatioun bauen - mir wäerten d'Beschreiwung vun dëser Etapp ausgoen.
  2. Deploy d'Applikatioun op e separaten Nummraum vum Kubernetes Cluster a fänkt un ze testen.
  3. Sich no Artefakte a parsing JUnit Berichter mat GitLab.
  4. E virdrun erstallt Nummraum läschen.

Elo - zur Ëmsetzung!

Upassung

GitLab CI

Loosst eis mat engem Fragment ufänken .gitlab-ci.yaml, déi beschreift d'Deployment vun der Applikatioun an d'Lafen Tester. D'Oplëschtung huet sech als zimlech voluminös erausgestallt, sou datt et grëndlech mat Kommentaren ergänzt gouf:

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

Elo am Dossier .helm/templates loosst eis YAML mam Job erstellen - tests-job.yaml - Tester auszeféieren an d'Kubernetes Ressourcen déi et brauch. Kuckt Erklärungen no der Oplëschtung:

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

Wéi eng Ressourcen an dëser Configuratioun beschriwwen? Beim Ofbau kreéiere mir en eenzegaartegen Nummraum fir de Projet (dëst gëtt an .gitlab-ci.yaml - tests-${CI_COMMIT_REF_SLUG}) a rullt et aus:

  1. ConfigMap mat Test Schrëft;
  2. Aarbecht mat enger Beschreiwung vum Pod an der spezifizéierter Direktiv command, déi just d'Tester leeft;
  3. PV an PVC, déi Iech erlaabt Testdaten ze späicheren.

Opgepasst op d'Aféierungskonditioun mat if um Ufank vum Manifest - deementspriechend mussen aner YAML Dateien vum Helm Chart mat der Applikatioun agewéckelt ginn ëmgedréint Design sou datt se net während dem Test ofgesat ginn. Dat ass:

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

Allerdéngs, wann d'Tester verlaangen puer Infrastruktur (zum Beispill Redis, RabbitMQ, Mongo, PostgreSQL ...) - hir YAMLs kënne sinn Net ausmaachen. Deploy se och an en Testëmfeld ... ajustéiert se wéi Dir passt, natierlech.

Finale Touch

Well Assemblée an Deployment mat werf Wierker fir de Moment nëmmen um Build Server (mat gitlab-runner), an de Pod mat Tester gëtt um Master gestart, musst Dir e Verzeechnes erstellen /mnt/tests op de Meeschter a gitt et dem Leefer, zum Beispill, iwwer NFS. En detailléiert Beispill mat Erklärungen fannt Dir an K8s Dokumentatioun.

D'Resultat wäert sinn:

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

Keen verbitt en NFS Deelen direkt op gitlab-runner ze maachen, an dann an Pods montéieren.

Remarque

Dir frot Iech vläicht firwat alles komplizéiert gëtt andeems Dir en Job erstellt, wann Dir einfach e Skript mat Tester direkt op de Shell Runner ausféiere kënnt? D'Äntwert ass ganz trivial ...

E puer Tester erfuerderen Zougang zu der Infrastruktur (MongoDB, RabbitMQ, PostgreSQL, etc.) fir z'iwwerpréiwen datt se richteg funktionnéieren. Mir maachen Testen vereenegt - mat dëser Approche gëtt et einfach sou zousätzlech Entitéiten ze enthalen. Zousätzlech zu dëser, mir kréien Standard Deployment Approche (och wann Dir NFS benotzt, zousätzlech Montage vun Verzeichnungen).

Resultat

Wat wäerte mir gesinn wa mir déi preparéiert Konfiguratioun uwenden?

D'Fusiounsufro weist Resuméstatistike fir Tester déi a senger leschter Pipeline lafen:

JUnit am GitLab CI mat Kubernetes

All Feeler kann hei geklickt ginn fir Detailer:

JUnit am GitLab CI mat Kubernetes

NB: Den opmierksamen Lieser mierkt datt mir eng NodeJS Applikatioun testen, an an de Screenshots - .NET ... Sidd net iwwerrascht: et ass just datt beim Virbereedung vum Artikel keng Feeler fonnt goufen beim Testen vun der éischter Applikatioun, awer si goufen an engem aneren fonnt.

Konklusioun

Wéi Dir gesitt, näischt komplizéiert!

Am Prinzip, wann Dir schonn e Shell Sammler hutt an et funktionnéiert, awer Dir braucht keng Kubernetes, d'Tester befestegt ass eng nach méi einfach Aufgab wéi hei beschriwwen. An an GitLab CI Dokumentatioun Dir fannt Beispiller fir Ruby, Go, Gradle, Maven an e puer anerer.

PS

Liest och op eisem Blog:

Source: will.com

Setzt e Commentaire