JUnit in GitLab CI mit Kubernetes

Obwohl alle genau wissen, wie wichtig es ist, ihre Software zu testen, und viele dies bereits automatisiert tun, fand sich in den Weiten von Habr kein einziges Rezept zur Einrichtung der Verbindung zwischen so populĂ€ren Produkten in dieser Nische wie (unserem Favoriten) GitLab und JUnit. Lassen Sie uns diese LĂŒcke schließen!

JUnit in GitLab CI mit Kubernetes

EinfĂŒhrung

ZunÀchst möchte ich den Kontext festlegen:

  • Da alle unsere Anwendungen in Kubernetes laufen, werden wir die AusfĂŒhrung von Tests in der entsprechenden Infrastruktur behandeln.
  • FĂŒr den Bau und das Deployment verwenden wir werf (im Hinblick auf infrastrukturelle Komponenten bedeutet dies auch automatisch, dass Helm eingesetzt wird).
  • Auf die Einzelheiten der Erstellung von Tests werde ich nicht eingehen: In unserem Fall schreibt der Kunde die Tests selbst, und wir sorgen lediglich fĂŒr deren AusfĂŒhrung (und fĂŒr das Vorhandensein des entsprechenden Berichts im Merge Request).


Wie wird der gesamte Ablauf aussehen?

  1. Der Aufbau der Anwendung – die Beschreibung dieses Schrittes lassen wir weg.
  2. Deployment der Anwendung in einen separaten Namespace des Kubernetes-Clusters und DurchfĂŒhrung der Tests.
  3. Suche nach Artefakten und Parsing des JUnit-Berichts durch GitLab.
  4. Löschen des zuvor erstellten Namespaces.

Jetzt – zur Umsetzung!

Einstellungen

GitLab CI

Beginnen wir mit dem Abschnitt .gitlab-ci.yaml, der das Deployment der Anwendung und die AusfĂŒhrung der Tests beschreibt. Das Listing ist ziemlich umfangreich geworden und wurde daher mit Kommentaren ausfĂŒhrlich ergĂ€nzt:

variablen:
# wir erklÀren die werf-Version, die wir verwenden möchten
  WERF_VERSION: "1.0 beta"

.base_deploy: &base_deploy
  Skript:
# wir erstellen einen Namespace in K8s, falls dieser nicht vorhanden ist
    - kubectl --context="${WERF_KUBE_CONTEXT}" get ns ${CI_ENVIRONMENT_SLUG} || kubectl create ns ${CI_ENVIRONMENT_SLUG}
# wir laden werf und deployen – nĂ€here Informationen dazu finden Sie in der Dokumentation
# (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:-''}"
# wir ĂŒbergeben die Variable `run_tests`
# sie wird im Rendern des Helm-Releases verwendet
      --set "global.run_tests=${RUN_TESTS:-no}"
      --set "global.env=${CI_ENVIRONMENT_SLUG}"
# wir Ă€ndern das Timeout (es gibt lange Tests) und ĂŒbergeben es an das Release
      --set "global.ci_timeout=${CI_TIMEOUT:-900}"
     --timeout ${CI_TIMEOUT:-900}
  AbhÀngigkeiten:
    - Build

.test-base: &test-base
  extends: .base_deploy
  before_script:
# wir erstellen ein Verzeichnis fĂŒr den zukĂŒnftigen Bericht, basierend auf $CI_COMMIT_REF_SLUG
    - mkdir /mnt/tests/${CI_COMMIT_REF_SLUG} || true
# ein notwendiger Workaround, da GitLab Artefakte in seinem build-dir haben möchte
    - mkdir ./tests || true
    - ln -s /mnt/tests/${CI_COMMIT_REF_SLUG} ./tests/${CI_COMMIT_REF_SLUG}
  after_script:
# nach Abschluss der Tests löschen wir das Release zusammen mit dem Job
# (und vielleicht seiner Infrastruktur)
    - 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
# wir erlauben FehlschlÀge, aber Sie können es anders machen
  allow_failure: true
  variablen:
    RUN_TESTS: 'ja'
# wir setzen den Kontext in werf
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#infrastructure)
    WERF_KUBE_CONTEXT: 'admin@stage-cluster'
  tags:
# wir verwenden einen Runner mit dem Tag `werf-runner`
    - werf-runner
  artefakte:
# es ist erforderlich, ein Artefakt zu erstellen, damit es im Pipeline sichtbar ist
# und heruntergeladen werden kann – z.B. fĂŒr eine detailliertere Untersuchung
    Pfade:
      - ./tests/${CI_COMMIT_REF_SLUG}/*
# Artefakte, die Àlter als eine Woche sind, werden gelöscht
    expire_in: 7 Tage
# wichtig: diese Zeilen sind fĂŒr das Parsen des Berichts durch GitLab verantwortlich
    Berichte:
      junit: ./tests/${CI_COMMIT_REF_SLUG}/report.xml

# zur Vereinfachung sind hier nur zwei Phasen dargestellt
# in der RealitĂ€t werden es mehr sein – mindestens wegen des Deployments
stufen:
  - build
  - tests

build:
  phase: build
  Skript:
# der Build – wiederum gemĂ€ĂŸ der Dokumentation zu 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
  außer:
    - ZeitplÀne

testen:
  <<: *test-base
  Umgebung:
# "der eigentliche Kern" der Namensgebung des Namespaces
# (https://docs.gitlab.com/ce/ci/variables/predefined_variables.html)
    Name: tests-${CI_COMMIT_REF_SLUG}
  Phase: tests
  außer:
    - ZeitplÀne

Kubernetes

Jetzt im Verzeichnis .helm/templates erstellen wir eine YAML mit einem Job — tests-job.yaml — um Tests zu starten und die dafĂŒr benötigten Ressourcen von Kubernetes bereitzustellen. Hinweise siehe nach der Auflistung:

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

Welche Ressourcen sind in dieser Konfiguration beschrieben? Bei der Bereitstellung erstellen wir einen einzigartigen Namespace fĂŒr das Projekt (dies wird auch noch angegeben in .gitlab-ci.yaml — tests-${CI_COMMIT_REF_SLUG}) und versehen diesen mit:

  1. ConfigMap einem Tests-Skript;
  2. Job einer Beschreibung des Pods und der spezifischen Direktive command, die die Tests tatsÀchlich startet;
  3. PV und PVC, die das Speichern von Testdaten ermöglichen.

Bitte beachten Sie die Bedingung am Anfang des Manifests — entsprechend mĂŒssen andere YAML-Dateien des Helm-Charts mit der Anwendung in if eine umgekehrte Struktur eingeschlossen werden, damit sie beim Testen nicht bereitgestellt werden. Das bedeutet: {{- if ne .Values.global.run_tests "yes" }} --- ich eine andere yaml {{- end }}

Wenn die Tests

eine bestimmte Infrastruktur erfordern zum Beispiel Redis, RabbitMQ, Mongo, PostgreSQL
) – deren YAML-Dateien können (zum Beispiel Redis, RabbitMQ, Mongo, PostgreSQL
) — deren YAMLs können nicht Ausschalten. Erweitern Sie sie in der Testumgebung
 natĂŒrlich, indem Sie sie nach Belieben anpassen.

Der letzte Schliff

Da das Bauen und Bereitstellen mit werf bisher funktioniert nur auf dem Build-Server (mit gitlab-runner), wĂ€hrend das Pod mit den Tests auf dem Master gestartet wird, mĂŒssen Sie ein Verzeichnis erstellen /mnt/tests auf dem Master und es dem Runner zur VerfĂŒgung stellen, zum Beispiel ĂŒber NFS. Ein erweitertes Beispiel mit ErklĂ€rungen finden Sie in der K8s-Dokumentation.

Das Ergebnis wird sein:

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

Niemand hindert Sie daran, auch einen NFS-Share direkt auf dem gitlab-runner zu erstellen und ihn in die Pods einzuhÀngen.

Hinweis

Vielleicht fragen Sie sich, warum man alles durch die Erstellung eines Jobs komplizieren sollte, wenn man einfach das Skript mit den Tests direkt auf dem Shell-Runner ausfĂŒhren kann? Die Antwort ist ziemlich trivial...

Einige Tests erfordern den Zugriff auf die Infrastruktur (MongoDB, RabbitMQ, PostgreSQL usw.), um die ordnungsgemĂ€ĂŸe Funktionsweise zu ĂŒberprĂŒfen. Wir machen das Testen einheitlich – mit diesem Ansatz wird es einfach, solche zusĂ€tzlichen EntitĂ€ten einzubeziehen. DarĂŒber hinaus erhalten wir den Standard- eine Vorgehensweise beim Deployment (auch wenn es mit NFS und zusĂ€tzlichem Mounten von Verzeichnissen erfolgt).

Ergebnis

Was werden wir sehen, wenn wir die vorbereitete Konfiguration anwenden?

Im Merge-Request wird eine Zusammenfassungsstatistik zu den Tests angezeigt, die in der letzten Pipeline ausgefĂŒhrt wurden:

JUnit in GitLab CI mit Kubernetes

Auf jeden Fehler kann hier geklickt werden, um Details zu erhalten:

JUnit in GitLab CI mit Kubernetes

NB: Aufmerksame Leser werden bemerken, dass wir eine NodeJS-Anwendung testen, und auf den Screenshots ist .NET zu sehen
 Lassen Sie sich nicht ĂŒberraschen: Im Rahmen der Vorbereitung des Artikels gab es keine Fehler beim Testen der ersten Anwendung, jedoch wurden in einer anderen gefunden.

Fazit

Wie man sieht, nichts Kompliziertes!

Im Grunde genommen, wenn Sie bereits einen Shell-Bausatz haben und er funktioniert, wĂ€hrend Sie Kubernetes nicht benötigen – wird das Testen daran eine noch einfachere Aufgabe sein als die hier beschriebene. Und in der GitLab CI-Dokumentation finden Sie Beispiele fĂŒr Ruby, Go, Gradle, Maven und einige andere.

P.S.

Lesen Sie auch in unserem Blog:

Quelle: habr.com

60GB SSD 8Gb DDR4