JUnit in GitLab CI mit Kubernetes

Obwohl jeder genau weiß, dass das Testen seiner Software wichtig und notwendig ist und viele dies schon seit langem automatisch tun, gab es in den Weiten von Habr kein einziges Rezept für die Einrichtung einer Kombination solch beliebter Produkte diese Nische als (unser Favorit) GitLab und JUnit. Lasst uns diese Lücke füllen!

JUnit in GitLab CI mit Kubernetes

einleitend

Lassen Sie mich zunächst einen Kontext geben:

  • Da alle unsere Anwendungen auf Kubernetes laufen, ziehen wir in Betracht, Tests auf der entsprechenden Infrastruktur durchzuführen.
  • Zur Montage und Bereitstellung nutzen wir Hof (Bezogen auf Infrastrukturkomponenten bedeutet dies automatisch auch, dass Helm beteiligt ist).
  • Ich werde nicht auf die Details der tatsächlichen Erstellung von Tests eingehen: In unserem Fall schreibt der Kunde die Tests selbst und wir stellen nur sicher, dass sie gestartet werden (und das Vorhandensein eines entsprechenden Berichts in der Zusammenführungsanforderung).


Wie wird der allgemeine Handlungsablauf aussehen?

  1. Erstellen der Anwendung – auf die Beschreibung dieser Phase verzichten wir.
  2. Stellen Sie die Anwendung in einem separaten Namespace des Kubernetes-Clusters bereit und beginnen Sie mit dem Testen.
  3. Suchen nach Artefakten und Parsen von JUnit-Berichten mit GitLab.
  4. Löschen eines zuvor erstellten Namespace.

Nun zur Umsetzung!

Einstellung

GitLab-CI

Beginnen wir mit einem Fragment .gitlab-ci.yaml, in dem die Bereitstellung der Anwendung und das Ausführen von Tests beschrieben werden. Die Auflistung erwies sich als recht umfangreich und wurde daher gründlich mit Kommentaren ergänzt:

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

Jetzt im Verzeichnis .helm/templates Lasst uns YAML mit Job erstellen - tests-job.yaml – um Tests und die benötigten Kubernetes-Ressourcen auszuführen. Siehe Erläuterungen 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 }}

Was für Ressourcen in dieser Konfiguration beschrieben? Bei der Bereitstellung erstellen wir einen eindeutigen Namensraum für das Projekt (dies ist in angegeben). .gitlab-ci.yaml - tests-${CI_COMMIT_REF_SLUG}) und rollen Sie es aus:

  1. Konfigurationskarte mit Testskript;
  2. Job mit einer Beschreibung des Pods und der angegebenen Direktive command, das nur die Tests ausführt;
  3. PV und PVC, mit denen Sie Testdaten speichern können.

Beachten Sie die Einführungsbedingung mit if am Anfang des Manifests - entsprechend müssen weitere YAML-Dateien des Helm-Charts mit der Anwendung eingebunden werden invers Entwerfen Sie so, dass sie während des Tests nicht bereitgestellt werden. Also:

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

Allerdings, wenn die Tests erfordern eine gewisse Infrastruktur (zum Beispiel Redis, RabbitMQ, Mongo, PostgreSQL...) – ihre YAMLs können sein nicht abschalten. Stellen Sie sie auch in einer Testumgebung bereit und passen Sie sie natürlich nach Ihren Wünschen an.

Letzte Berührung

Weil Die Montage und Bereitstellung mit werf funktioniert vorerst nur Auf dem Build-Server (mit Gitlab-Runner) und der Pod mit Tests auf dem Master gestartet wird, müssen Sie ein Verzeichnis erstellen /mnt/tests auf den Meister und gib es dem Läufer, zum Beispiel über NFS. Ein ausführliches Beispiel mit Erläuterungen finden Sie in 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 verbietet das Erstellen einer NFS-Freigabe direkt auf Gitlab-Runner und das anschließende Mounten in Pods.

Beachten

Sie fragen sich vielleicht, warum die Erstellung eines Jobs alles komplizierter machen soll, wenn Sie einfach ein Skript mit Tests direkt auf dem Shell-Runner ausführen können? Die Antwort ist ziemlich trivial...

Einige Tests erfordern Zugriff auf die Infrastruktur (MongoDB, RabbitMQ, PostgreSQL usw.), um zu überprüfen, ob sie ordnungsgemäß funktionieren. Wir vereinheitlichen das Testen – mit diesem Ansatz wird es einfach, solche zusätzlichen Einheiten einzubeziehen. Darüber hinaus erhalten wir Standard Bereitstellungsansatz (auch bei Verwendung von NFS, zusätzliches Mounten von Verzeichnissen).

Erlebe die Kraft effektiver Ergebnisse

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

Die Zusammenführungsanforderung zeigt zusammenfassende Statistiken für Tests an, die in der neuesten Pipeline ausgeführt werden:

JUnit in GitLab CI mit Kubernetes

Für Einzelheiten zu jedem Fehler kann hier geklickt werden:

JUnit in GitLab CI mit Kubernetes

NB: Der aufmerksame Leser wird bemerken, dass wir eine NodeJS-Anwendung testen und in den Screenshots - .NET... Seien Sie nicht überrascht: Bei der Vorbereitung des Artikels wurden beim Testen der ersten Anwendung nur keine Fehler gefunden, aber sie wurden in einem anderen gefunden.

Abschluss

Wie Sie sehen, nichts Kompliziertes!

Wenn Sie bereits einen Shell-Kollektor haben und dieser funktioniert, Sie aber kein Kubernetes benötigen, ist das Anhängen von Tests im Prinzip eine noch einfachere Aufgabe als hier beschrieben. Und in GitLab CI-Dokumentation Sie finden Beispiele für Ruby, Go, Gradle, Maven und einige andere.

PS

Lesen Sie auch auf unserem Blog:

Source: habr.com

Kommentar hinzufügen