JUnit in GitLab CI mit Kubernetes

Trotz der allgemeinen Erkenntnis, dass es wichtig ist, seine Software zu testen, und dass viele dies bereits automatisiert tun, findet sich auf den Weiten von Habr kein Rezept zur Konfiguration einer Verbindung dieser beliebten Produkte in diesem Bereich, wie (unsere Favoriten) GitLab und JUnit. Lassen Sie uns diese Lücke schließen!

JUnit in GitLab CI mit Kubernetes

Einleitung

Zunächst möchte ich den Kontext festlegen:

  • Da alle unsere Anwendungen in Kubernetes laufen, werden wir den Testlauf in der entsprechenden Infrastruktur betrachten.
  • Für den Aufbau und das Deployment verwenden wir werf (was im Sinne von Infrastrukturkomponenten auch automatisch bedeutet, dass Helm verwendet wird).
  • Ich werde nicht ins Detail der Testkreation eingehen: In unserem Fall schreibt der Kunde die Tests selbst, und wir gewährleisten nur deren Ausführung (und das Vorhandensein der entsprechenden Berichte im Merge-Request).


Wie wird die allgemeine Abfolge der Schritte aussehen?

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

Nun — zur Umsetzung!

Konfiguration

GitLab CI

Lassen Sie uns mit dem Fragment beginnen .gitlab-ci.yaml, der das Deployment der Anwendung und das Starten von Tests beschreibt. Die Auflistung ist recht umfangreich, daher wurde sie umfangreich mit Kommentaren ergänzt:

Variablen:
# Wir deklarieren die Version von werf, die wir verwenden möchten
  WERF_VERSION: "1.0 beta"

.base_deploy: &base_deploy
  script:
# Namespace in K8s erstellen, falls nicht vorhanden
    - kubectl --context="${WERF_KUBE_CONTEXT}" get ns ${CI_ENVIRONMENT_SLUG} || kubectl create ns ${CI_ENVIRONMENT_SLUG}
# wir laden werf und deployen – weitere Informationen 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:-''}"
# Übergabe der Variable `run_tests`
# sie wird im Rendern des Helm-Releases verwendet
      --set "global.run_tests=${RUN_TESTS:-no}"
      --set "global.env=${CI_ENVIRONMENT_SLUG}"
# Timeout ändern (es gibt lange Tests) und übergeben im Release
      --set "global.ci_timeout=${CI_TIMEOUT:-900}"
     --timeout ${CI_TIMEOUT:-900}
  dependencies:
    - Build

.test-base: &test-base
  extends: .base_deploy
  before_script:
# Erstellen eines Verzeichnisses für den zukünftigen Bericht, basierend auf $CI_COMMIT_REF_SLUG
    - mkdir /mnt/tests/${CI_COMMIT_REF_SLUG} || true
# Notlösung, 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 entfernen wir das Release zusammen mit der Jobbeschreibung
# (und möglicherweise 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 Fehler, aber Sie können das anders gestalten
  allow_failure: true
  variables:
    RUN_TESTS: 'yes'
# 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
  artifacts:
# Artefakte müssen gesammelt werden, um im Pipeline sichtbar zu sein
# und heruntergeladen werden zu können – z.B. für eine gründliche Analyse
    paths:
      - ./tests/${CI_COMMIT_REF_SLUG}/*
# Artefakte, die älter als eine Woche sind, werden gelöscht
    expire_in: 7 days
# wichtig: diese Zeilen steuern das Parsen des Berichts durch GitLab
    reports:
      junit: ./tests/${CI_COMMIT_REF_SLUG}/report.xml

# Zur Vereinfachung sind hier nur zwei Phasen dargestellt
# in der Realität gibt es mehr – mindestens wegen des Deployments
stages:
  - build
  - tests

build:
  stage: build
  script:
# Build – wieder nach der werf-Dokumentation
# (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:
# "Salz der Benennung" des Namespace
# (https://docs.gitlab.com/ce/ci/variables/predefined_variables.html)
    name: tests-${CI_COMMIT_REF_SLUG}
  stage: tests
  except:
    - schedules

Kubernetes

Jetzt im Verzeichnis .helm/templates erstellen wir eine YAML mit einem Job — tests-job.yaml — für die Ausführung von Tests und den erforderlichen Kubernetes-Ressourcen. Erläuterungen siehe nach dem Listing:

{{- 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 ist auch in .gitlab-ci.yamltests-${CI_COMMIT_REF_SLUG}) und darin werden bereitgestellt:

  1. ConfigMap mit dem Testskript;
  2. Auftrag mit der Beschreibung des Pods und der angegebenen Direktive command, die die Tests tatsächlich startet;
  3. PV und PVC, die es ermöglichen, Testdaten zu speichern.

Beachten Sie die Eingangsbedingung mit wenn zu Beginn des Manifests — entsprechend müssen andere YAML-Dateien des Helm-Charts mit der Anwendung in eine umgekehrte Konstruktion gewickelt werden, damit sie bei Tests nicht bereitgestellt werden. Das heißt:

{{- if ne .Values.global.run_tests "yes" }}
---
ich andererseits yaml
{{- end }}

Wenn die Tests jedoch eine bestimmte Infrastruktur benötigen (zum Beispiel Redis, RabbitMQ, Mongo, PostgreSQL…) — ihre YAML-Dateien können nicht deaktiviert werden. Stellen Sie sie auch in der Testumgebung bereit… natürlich angepasst nach Ihrem Ermessen.

Letzter Schliff

Da der Build und die Bereitstellung derzeit mit werf auf der Build-Server (mit gitlab-runner) stattfinden, und der Pod mit den Tests auf dem Master gestartet wird, muss ein Verzeichnis nur auf dem Master erstellt und an den Runner übergeben werden, /mnt/tests zum Beispiel über NFS. Ein ausgearbeitetes Beispiel mit Erklärungen finden Sie in derK8s-Dokumentation. Das Ergebnis wird sein:.

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 daran, eine NFS-Freigabe direkt auf dem gitlab-runner zu erstellen und sie dann in die Pods zu mounten.

Hinweis

Vielleicht fragen Sie sich, warum man überhaupt die Erstellung eines Jobs komplizieren sollte, wenn man das Skript mit den Tests einfach direkt auf dem Shell-Runner ausführen kann? Die Antwort ist recht einfach...

Einige Tests erfordern den Zugriff auf die Infrastruktur (MongoDB, RabbitMQ, PostgreSQL usw.), um die korrekte Funktionsweise zu überprüfen. Wir machen die Tests einheitlich – mit diesem Ansatz wird es einfach, solche zusätzlichen Entitäten einzubeziehen. Darüber hinaus erhalten wir standardmäßig eine einheitliche Vorgehensweise beim Deployment (auch wenn es NFS und zusätzliches Mounten von Verzeichnissen verwendet).

Ergebnis

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

Im Merge-Request wird eine zusammenfassende Statistik der Tests angezeigt, die in seinem letzten Pipeline ausgeführt wurden:

JUnit in GitLab CI mit Kubernetes

Für jeden Fehler hier kann man klicken, um Details zu erhalten:

JUnit in GitLab CI mit Kubernetes

NB: Der aufmerksame Leser wird bemerken, dass wir eine NodeJS-Anwendung testen, während auf den Screenshots .NET zu sehen ist... Sehen Sie das nicht als Überraschung: Im Rahmen der Artikelvorbereitung gab es einfach keine Fehler beim Testen der ersten Anwendung, sondern wir haben sie in einer anderen gefunden.

Fazit

Wie man sieht, nichts kompliziertes!

Im Prinzip, wenn Sie bereits einen Shell-Bauer haben, der funktioniert, und Kubernetes nicht benötigen — wird es noch einfacher sein, das Testing daran anzuschließen, als hier beschrieben. 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

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Servern 🔥 Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Servern | ProHoster