JUnit dans GitLab CI avec Kubernetes

Bien que tout le monde sache qu'il est important et nécessaire de tester son logiciel, et que beaucoup le font automatiquement depuis longtemps, il n'y avait pas de recette sur Habr pour configurer l'intégration de produits aussi populaires dans ce domaine que (notre préféré) GitLab et JUnit. Remédions à cela !

JUnit dans GitLab CI avec Kubernetes

Introduction

Pour commencer, définissons le contexte :

  • Comme toutes nos applications fonctionnent sur Kubernetes, nous examinerons le lancement des tests dans l'infrastructure correspondante.
  • Pour la construction et le dĂ©ploiement, nous utilisons werf (dans le sens des composants d'infrastructure, cela signifie Ă©galement que Helm est impliquĂ©).
  • Je ne vais pas entrer dans les dĂ©tails de la crĂ©ation des tests : dans notre cas, le client Ă©crit les tests lui-mĂȘme, et nous nous contentons d'assurer leur exĂ©cution (et de fournir le rapport correspondant dans la demande de fusion).


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

  1. Compilation de l'application — nous allons sauter cette Ă©tape.
  2. Déploiement de l'application dans un namespace séparé du cluster Kubernetes et lancement des tests.
  3. Recherche des artefacts et analyse du rapport JUnit par GitLab.
  4. Suppression du namespace créé précédemment.

Passons maintenant à la mise en Ɠuvre !

Configuration

GitLab CI

Commençons par le fragment .gitlab-ci.yaml, qui décrit le déploiement de l'application et le lancement des tests. Le listing est assez volumineux, donc considérablement enrichi de commentaires :

variables:
# déclarons la version de werf que nous allons utiliser
  WERF_VERSION: "1.0 bĂȘta"

.base_deploy: &base_deploy
  script:
# créons un namespace dans K8s, s'il n'existe pas
    - kubectl --context="${WERF_KUBE_CONTEXT}" get ns ${CI_ENVIRONMENT_SLUG} || kubectl create ns ${CI_ENVIRONMENT_SLUG}
# chargeons werf et dĂ©ployons — pour plus de dĂ©tails, voir la documentation
# (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:-''}"
# transmettons la variable `run_tests`
# elle sera utilisée dans le rendu du Helm release
      --set "global.run_tests=${RUN_TESTS:-non}"
      --set "global.env=${CI_ENVIRONMENT_SLUG}"
# modifions le timeout (certaines tests peuvent prendre longtemps) et le transmettons dans le release
      --set "global.ci_timeout=${CI_TIMEOUT:-900}"
     --timeout ${CI_TIMEOUT:-900}
  dependencies:
    - Build

.test-base: &test-base
  extends: .base_deploy
  before_script:
# créons un répertoire pour le futur rapport, basé sur $CI_COMMIT_REF_SLUG
    - mkdir /mnt/tests/${CI_COMMIT_REF_SLUG} || true
# contournement nécessaire, car GitLab souhaite recevoir des artefacts dans son build-dir
    - mkdir ./tests || true
    - ln -s /mnt/tests/${CI_COMMIT_REF_SLUG} ./tests/${CI_COMMIT_REF_SLUG}
  after_script:
# Ă  la fin des tests, supprimons le release avec le Job
# (et, peut-ĂȘtre, son infrastructure)
    - 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
# nous autorisons les échecs, mais vous pouvez faire autrement
  allow_failure: true
  variables:
    RUN_TESTS: 'oui'
# définissons le contexte dans werf
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#infrastructure)
    WERF_KUBE_CONTEXT: 'admin@stage-cluster'
  tags:
# utilisons un runner avec le tag `werf-runner`
    - werf-runner
  artifacts:
# nécessite la collecte d'artefacts pour pouvoir les voir
# dans le pipeline et tĂ©lĂ©charger — par exemple, pour une Ă©tude plus approfondie
    paths:
      - ./tests/${CI_COMMIT_REF_SLUG}/*
# les artefacts de plus d'une semaine seront supprimés
    expire_in: 7 jours
# important : ces lignes sont responsables du parsing du rapport par GitLab
    reports:
      junit: ./tests/${CI_COMMIT_REF_SLUG}/report.xml

# pour simplifier, seulement deux étapes sont montrées ici
# en rĂ©alitĂ©, il y en aura plus — au minimum Ă  cause du dĂ©ploiement
stages:
  - build
  - tests

build:
  stage: build
  script:
# construction — encore selon la documentation de 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:
# "la véritable essence" de la nomination du 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 un YAML avec un Job — tests-job.yaml — pour exĂ©cuter des tests et les ressources nĂ©cessaires Ă  Kubernetes. Les explications se trouvent aprĂšs le 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 }}

Quels sont les ressources dĂ©crites dans cette configuration ? Lors du dĂ©ploiement, nous crĂ©ons un namespace unique pour le projet (cela est indiquĂ© encore dans .gitlab-ci.yaml — tests-${CI_COMMIT_REF_SLUG}) et nous y dĂ©ployons :

  1. ConfigMap avec le script de test ;
  2. Job avec la description du pod et la directive indiquée commande, qui lance justement les tests ;
  3. PV et PVC, qui permettent de stocker les données des tests.

Notez la condition d'entrĂ©e avec if au dĂ©but du manifeste — par consĂ©quent, d'autres fichiers YAML du chart Helm avec l'application doivent ĂȘtre enveloppĂ©s dans la structure inversĂ©e , pour ne pas ĂȘtre dĂ©ployĂ©s lors des tests. En d'autres termes :

{{- if ne .Values.global.run_tests "yes" }}
---
j'ai un autre fichier yaml
{{- end }}

NĂ©anmoins, si les tests nĂ©cessitent une certaine infrastructure (par exemple, Redis, RabbitMQ, Mongo, PostgreSQL
) — leurs YAML peuvent ĂȘtre ne DĂ©sactivez-les. DĂ©ployez-les dans un environnement de test
 bien sĂ»r, en les ajustant Ă  votre convenance.

DerniĂšre touche

Étant donnĂ© que la construction et le dĂ©ploiement Ă  l'aide de werf fonctionnent encore uniquement sur le serveur de build (avec gitlab-runner), et que le pod pour les tests se lance sur le master, il sera nĂ©cessaire de crĂ©er un rĂ©pertoire /mnt/tests sur le master et de le donner au runner, par exemple, via NFS. Un exemple dĂ©ployĂ© avec des explications peut ĂȘtre trouvĂ© dans la documentation K8s.

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 le gitlab-runner, puis de le monter dans les pods.

Remarque

Vous vous demandez peut-ĂȘtre pourquoi compliquer les choses en crĂ©ant un Job, si vous pouvez simplement lancer un script avec les tests directement sur le shell-runner ? La rĂ©ponse est assez triviale


Certains tests nĂ©cessitent d'accĂ©der Ă  l'infrastructure (MongoDB, RabbitMQ, PostgreSQL, etc.) pour vĂ©rifier leur bon fonctionnement. Nous rendons les tests standardisĂ©s — avec cette approche, il devient facile d'intĂ©grer de telles entitĂ©s supplĂ©mentaires. De plus, nous obtenons niveau une approche dans le dĂ©ploiement (mĂȘme avec l'utilisation de NFS et le montage supplĂ©mentaire de rĂ©pertoires).

Résultat

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

Dans la demande de fusion, des statistiques récapitulatives des tests exécutés dans son dernier pipeline seront affichées :

JUnit dans GitLab CI avec Kubernetes

Pour chaque erreur, vous pouvez cliquer ici pour obtenir des détails :

JUnit dans GitLab CI avec Kubernetes

NB: Le lecteur attentif remarquera que nous testons une application NodeJS, alors que les captures d'écran montrent .NET
 Ne soyez pas étonnés : simplement, dans le cadre de la préparation de l'article, aucune erreur n'a été trouvée dans le test du premier application, mais des erreurs ont été détectées dans une autre.

Conclusion

Comme vous pouvez le voir, rien de compliqué !

En principe, si vous avez dĂ©jĂ  un assembleur shell et qu'il fonctionne, et que Kubernetes ne vous est pas nĂ©cessaire — l'ajout de tests sera une tĂąche encore plus simple que celle dĂ©crite ici. Et dans la documentation de GitLab CI vous trouverez des exemples pour Ruby, Go, Gradle, Maven et quelques autres.

P.S.

Lisez aussi dans notre blog :

Source : habr.com

Acheter un hĂ©bergement fiable pour les sites avec protection DDoS, serveurs VPS VDS đŸ”„ Acheter un hĂ©bergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster