JUnit in GitLab CI met Kubernetes

Hoewel iedereen goed weet dat het belangrijk en noodzakelijk is om software te testen en velen dit al automatisch doen, is er op de ruimtes van Habr geen recept te vinden voor het instellen van de combinatie van populaire producten in deze niche, zoals (ons favoriete) GitLab en JUnit. We vullen deze leemte aan!

JUnit in GitLab CI met Kubernetes

Inleiding

Om te beginnen zal ik de context schetsen:

  • Aangezien al onze applicaties in Kubernetes draaien, zullen we de uitvoering van tests in de bijbehorende infrastructuur bekijken.
  • Voor de bouw en implementatie gebruiken we werf (wat in termen van infrastructuurcomponenten ook automatisch betekent dat Helm wordt gebruikt).
  • Ik zal niet in detail treden over het maken van tests: in ons geval schrijft de klant de tests zelf, en zorgen wij alleen voor de uitvoering ervan (en de aanwezigheid van een bijbehorende rapportage in de merge request).


Hoe ziet de algemene volgorde van handelingen eruit?

  1. De bouw van de applicatie — een beschrijving van deze fase laten we achterwege.
  2. Implementatie van de applicatie in een aparte namespace van het Kubernetes-cluster en uitvoering van de tests.
  3. Zoeken naar artefacten en parseren van het JUnit-rapport door GitLab.
  4. Verwijderen van de eerder aangemaakte namespace.

Nu — naar de implementatie!

Instellingen

GitLab CI

Laten we beginnen met het fragment .gitlab-ci.yaml, waarin de implementatie van de applicatie en het uitvoeren van de tests worden beschreven. De listing is vrij uitgebreid, daarom is deze grondig aangevuld met opmerkingen:

variabelen:
# geef de versie van werf aan die we gaan gebruiken
  WERF_VERSION: "1.0 beta"

.base_deploy: &base_deploy
  script:
# maak een namespace in K8s aan als deze nog niet bestaat
    - kubectl --context="${WERF_KUBE_CONTEXT}" get ns ${CI_ENVIRONMENT_SLUG} || kubectl create ns ${CI_ENVIRONMENT_SLUG}
# laad werf en implementeer — voor meer informatie hierover zie de documentatie
# (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:-''}"
# geef de variabele `run_tests`
# deze zal worden gebruikt bij het renderen van de Helm-release
      --set "global.run_tests=${RUN_TESTS:-nee}"
      --set "global.env=${CI_ENVIRONMENT_SLUG}"
# wijzig de timeout (er kunnen lange tests zijn) en geef deze door aan de release
      --set "global.ci_timeout=${CI_TIMEOUT:-900}"
     --timeout ${CI_TIMEOUT:-900}
  dependencies:
    - Build

.test-base: &test-base
  extends: .base_deploy
  before_script:
# maak een directory aan voor het toekomstige rapport, gebaseerd op $CI_COMMIT_REF_SLUG
    - mkdir /mnt/tests/${CI_COMMIT_REF_SLUG} || true
# dwingde workaround, omdat GitLab de artefacten in zijn build-directory wil ontvangen
    - mkdir ./tests || true
    - ln -s /mnt/tests/${CI_COMMIT_REF_SLUG} ./tests/${CI_COMMIT_REF_SLUG}
  after_script:
# verwijder de release samen met de Job na het einde van de tests
# (en mogelijk ook zijn infrastructuur)
    - 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
# we staan falen toe, maar je kunt het ook anders doen
  allow_failure: true
  variables:
    RUN_TESTS: 'ja'
# stel de context in voor werf
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#infrastructure)
    WERF_KUBE_CONTEXT: 'admin@stage-cluster'
  tags:
# gebruik een runner met de tag `werf-runner`
    - werf-runner
  artifacts:
# het is vereist om een artefact te bouwen zodat deze zichtbaar en downloadbaar is
# in de pipeline — bijvoorbeeld voor een grondigere inspectie
    paths:
      - ./tests/${CI_COMMIT_REF_SLUG}/*
# artefacten ouder dan een week worden verwijderd
    expire_in: 7 dagen
# belangrijk: deze regels zijn verantwoordelijk voor de parsing van het rapport door GitLab
    reports:
      junit: ./tests/${CI_COMMIT_REF_SLUG}/report.xml

# ter vereenvoudiging zijn hier slechts twee fasen getoond
# in werkelijkheid heb je er meer — tenminste vanwege de implementatie
stages:
  - build
  - tests

build:
  stage: build
  script:
# build — opnieuw volgens de documentatie over 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:
# "de kern" van de naamgeving van de namespace
# (https://docs.gitlab.com/ce/ci/variables/predefined_variables.html)
    name: tests-${CI_COMMIT_REF_SLUG}
  stage: tests
  except:
    - schedules

Kubernetes

Nu in de directory .helm/templates creëren we een YAML met een Job — tests-job.yaml — voor het uitvoeren van tests en de benodigde Kubernetes-resources. Verklaringen zie na de 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 }}

Welke resources worden in deze configuratie beschreven? Bij de deployment creëren we een unieke namespace voor het project (dit is al aangegeven in .gitlab-ci.yaml — tests-${CI_COMMIT_REF_SLUG}) en hierin implementeren we:

  1. ConfigMap met het testscrip;
  2. Job met de beschrijving van de pod en de opgegeven directieve command, die juist de tests uitvoert;
  3. PV en PVC, die het opslaan van testdata mogelijk maken.

Let op de invoervoorwaarde met als aan het begin van de manifest — dienovereenkomstig moeten andere YAML-bestanden van de Helm-chart met de applicatie worden ingepakt in de omgekeerde constructie, zodat ze niet worden gedeployed tijdens het testen. Dat wil zeggen:

{{- if ne .Values.global.run_tests "yes" }}
---
ik een andere yaml
{{- end }}

Trouwens, als de tests bepaalde infrastructuur vereisen (bijv. Redis, RabbitMQ, Mongo, PostgreSQL…) — hun YAML's kunnen niet Uitschakelen. Breid ze uit en voer ze in een testomgeving uit… uiteraard, aangepast naar eigen inzicht.

Laatste details

Aangezien de build en deployment met behulp van werf tot nu toe werkt van op de build-server (met gitlab-runner), en de pod met tests start op de master, is het noodzakelijk om een directory te creëren /mnt/tests op de master en deze door te geven aan de runner, bijvoorbeeld via NFS. Een uitgebreid voorbeeld met uitleg is te vinden in de K8s-documentatie.

Het resultaat zal zijn:

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 staat het ook in de weg om een NFS-share op de gitlab-runner zelf te creëren en deze vervolgens in de pods te monteren.

Opmerking

Misschien vraag je je af, waarom al deze complicaties door het creëren van een Job, als je het script met tests gewoon op de shell-runner kunt uitvoeren? Het antwoord is vrij triviaal…

Sommige tests vereisen toegang tot de infrastructuur (MongoDB, RabbitMQ, PostgreSQL, enz.) om de juistheid van het werk ermee te controleren. We maken het testen uniform — met deze aanpak wordt het eenvoudig om dergelijke extra entiteiten erbij te betrekken. Bovendien krijgen we standaard een aanpak bij de deployment (zelfs al met gebruik van NFS, extra mappen montages).

Resultaat

Wat zullen we zien wanneer we de voorbereide configuratie toepassen?

In de merge request zal een samenvattende statistiek van de tests verschijnen die in de laatste pipeline zijn uitgevoerd:

JUnit in GitLab CI met Kubernetes

Op elke fout kan hierop geklikt worden om details te ontvangen:

JUnit in GitLab CI met Kubernetes

NB: Een oplettende lezer zal opmerken dat we een NodeJS-applicatie testen, terwijl op de screenshots .NET staat… Verwonder je niet: gewoon in het kader van het voorbereiden van het artikel waren er geen fouten in de tests van de eerste applicatie, maar er werden fouten in de andere gevonden.

Conclusie

Zoals je ziet, niets moeilijks!

In principe, als je al een shell-builder hebt die werkt, en Kubernetes heb je niet nodig — het integreren van testen zal een nog eenvoudigere taak zijn dan hier beschreven. En in de GitLab CI-documentatie vind je voorbeelden voor Ruby, Go, Gradle, Maven, en enkele anderen.

P.S.

Lees ook op onze blog:

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers 🔥 Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster