JUnit dans GitLab CI avec Kubernetes

Malgré le fait que tout le monde sait parfaitement que tester votre logiciel est important et nécessaire, et que beaucoup le font automatiquement depuis longtemps, dans l'immensité de Habr, il n'y avait pas de recette unique pour mettre en place une combinaison de produits aussi populaires dans ce créneau comme (notre préféré) GitLab et JUnit . Comblons cette lacune !

JUnit dans GitLab CI avec Kubernetes

Introduction

Tout d'abord, permettez-moi de donner un peu de contexte :

  • Puisque toutes nos applications fonctionnent sur Kubernetes, nous envisagerons d'effectuer des tests sur l'infrastructure appropriée.
  • Pour l'assemblage et le déploiement, nous utilisons cour (en termes de composants d'infrastructure, cela signifie aussi automatiquement que Helm est impliqué).
  • Je n'entrerai pas dans les détails de la création proprement dite des tests : dans notre cas, le client écrit lui-même les tests, et nous assurons uniquement leur lancement (et la présence d'un rapport correspondant dans la demande de fusion).


À quoi ressemblera la séquence générale des actions ?

  1. Construire l'application - nous omettrons la description de cette étape.
  2. Déployez l'application sur un espace de noms distinct du cluster Kubernetes et démarrez les tests.
  3. Recherche d'artefacts et analyse des rapports JUnit avec GitLab.
  4. Suppression d'un espace de noms précédemment créé.

Maintenant, place à la mise en œuvre !

réglage

CI GitLab

Commençons par un fragment .gitlab-ci.yaml, qui décrit le déploiement de l'application et l'exécution des tests. La liste s'est avérée assez volumineuse, elle a donc été soigneusement complétée par des commentaires :

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

Maintenant dans le répertoire .helm/templates créons YAML avec Job - tests-job.yaml — pour exécuter les tests et les ressources Kubernetes dont il a besoin. Voir les explications après l'inscription :

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

Quel genre de ressources décrit dans cette configuration ? Lors du déploiement, nous créons un espace de noms unique pour le projet (ceci est indiqué dans .gitlab-ci.yaml - tests-${CI_COMMIT_REF_SLUG}) et déployez-le :

  1. Carte de configuration avec script de test ;
  2. Emploi avec une description du pod et la directive spécifiée command, qui exécute simplement les tests ;
  3. PV et PVC, qui vous permettent de stocker les données de test.

Faites attention à la condition d'introduction avec if au début du manifeste - par conséquent, les autres fichiers YAML de la charte Helm avec l'application doivent être enveloppés dans sens inverse concevoir de manière à ce qu’ils ne soient pas déployés pendant les tests. C'est-à-dire:

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

Cependant, si les tests nécessitent une certaine infrastructure (par exemple, Redis, RabbitMQ, Mongo, PostgreSQL...) - leurs YAML peuvent être aucun éteindre. Déployez-les également dans un environnement de test... en les ajustant comme bon vous semble, bien sûr.

Touche finale

Parce que l'assemblage et le déploiement à l'aide de Werf fonctionnent pour l'instant seulement sur le serveur de build (avec gitlab-runner), et que le pod avec les tests est lancé sur le master, vous devrez créer un répertoire /mnt/tests sur le maître et donnez-le au coureur, par exemple, via NFS. Un exemple détaillé avec des explications peut être trouvé dans Documentation K8.

Le résultat sera :

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

Personne n'interdit de créer un partage NFS directement sur gitlab-runner, puis de le monter dans des pods.

Noter

Vous vous demandez peut-être pourquoi tout compliquer en créant un Job si vous pouvez simplement exécuter un script avec des tests directement sur le shell runner ? La réponse est assez triviale...

Certains tests nécessitent un accès à l'infrastructure (MongoDB, RabbitMQ, PostgreSQL, etc.) pour vérifier leur bon fonctionnement. Nous rendons les tests unifiés - avec cette approche, il devient facile d'inclure de telles entités supplémentaires. En plus de cela, nous obtenons standard approche de déploiement (même si vous utilisez NFS, montage supplémentaire de répertoires).

Résultat

Que verrons-nous lorsque nous appliquerons la configuration préparée ?

La demande de fusion affichera des statistiques récapitulatives pour les tests exécutés dans son dernier pipeline :

JUnit dans GitLab CI avec Kubernetes

Chaque erreur peut être cliqué ici pour plus de détails :

JUnit dans GitLab CI avec Kubernetes

NB: Le lecteur attentif remarquera que nous testons une application NodeJS, et dans les captures d'écran - .NET... Ne soyez pas surpris : c'est juste qu'en préparant l'article, aucune erreur n'a été trouvée lors du test de la première application, mais elles ont été retrouvés dans un autre.

Conclusion

Comme vous pouvez le constater, rien de compliqué !

En principe, si vous disposez déjà d'un collecteur de shell et qu'il fonctionne, mais que vous n'avez pas besoin de Kubernetes, y attacher des tests sera une tâche encore plus simple que celle décrite ici. Et en Documentation GitLabCI vous trouverez des exemples pour Ruby, Go, Gradle, Maven et quelques autres.

PS

A lire aussi sur notre blog :

Source: habr.com

Ajouter un commentaire