JUnit v GitLab CI s Kubernetes

I když všichni dobře ví, jak důležité je testovat svůj software a mnozí to dělají automaticky, na Habr nebyl nalezen žádný recept na nastavení kombinace tak populárních produktů v tomto oboru, jako jsou (nám milý) GitLab a JUnit. Zaplníme tuto mezeru!

JUnit v GitLab CI s Kubernetes

Úvod

Nejprve nastíním kontext:

  • Protože všechna naše aplikace běží v Kubernetes, budeme zvažovat spuštění testů v odpovídající infrastruktuře.
  • Pro sestavení a nasazení používáme werf (což v případě infrastrukturních komponentů také automaticky znamená, že je zapojen Helm).
  • Neproniknu do detailů samotného vytváření testů: v našem případě klient píše testy sám, my jen zajišťujeme jejich spuštění (a přítomnost odpovídající zprávy ve merge requestu).


Jak bude vypadat celková sekvence akcí?

  1. Sestavení aplikace — popis této fáze vynecháme.
  2. Nasazení aplikace do samostatného namespace clusteru Kubernetes a spuštění testování.
  3. Vyhledání artefaktů a parsování JUnit zprávy GitLabem.
  4. Odstranění dříve vytvořeného namespace.

Teď — k realizaci!

Nastavení

GitLab CI

Začneme úryvkem .gitlab-ci.yaml, který popisuje nasazení aplikace a spuštění testů. Výpis se ukázal jako poměrně objemný, a proto byl důkladně doplněn komentáři:

proměnné:
# deklarujeme verzi werf, kterou budeme používat
  WERF_VERZE: "1.0 beta"

.base_deploy: &base_deploy
  skript:
# vytváříme namespace v K8s, pokud neexistuje
    - kubectl --context="${WERF_KUBE_CONTEXT}" get ns ${CI_ENVIRONMENT_SLUG} || kubectl create ns ${CI_ENVIRONMENT_SLUG}
# načítáme werf a nasazujeme — podrobnosti naleznete v dokumentaci
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#deploy-stage)
    - type multiwerf && source <(multiwerf use ${WERF_VERSION})
    - werf verze
    - type werf && source <(werf ci-env gitlab --tagging-strategy tag-or-branch --verbose)
    - werf nasadit --stages-storage :local
      --namespace ${CI_ENVIRONMENT_SLUG}
      --set "global.commit_ref_slug=${CI_COMMIT_REF_SLUG:-''}"
# předáváme proměnnou `run_tests`
# bude se používat při renderování Helm-releasu
      --set "global.run_tests=${RUN_TESTS:-no}"
      --set "global.env=${CI_ENVIRONMENT_SLUG}"
# měníme timeout (mohou být dlouhé testy) a předáváme ho do releasu
      --set "global.ci_timeout=${CI_TIMEOUT:-900}"
     --timeout ${CI_TIMEOUT:-900}
  závislosti:
    - Build

.test-base: &test-base
  rozšiřuje: .base_deploy
  before_script:
# vytváříme adresář pro budoucí zprávu, vycházející z $CI_COMMIT_REF_SLUG
    - mkdir /mnt/tests/${CI_COMMIT_REF_SLUG} || true
# nedobrovolný workaround, protože GitLab chce dostat artefakty ve svém build-dir’ě
    - mkdir ./tests || true
    - ln -s /mnt/tests/${CI_COMMIT_REF_SLUG} ./tests/${CI_COMMIT_REF_SLUG}
  after_script:
# po skončení testů odstraníme releas spolu s Job’em
# (a možná i jeho infrastrukturou)
    - type multiwerf && source <(multiwerf use ${WERF_VERSION})
    - werf verze
    - type werf && source <(werf ci-env gitlab --tagging-strategy tag-or-branch --verbose)
    - werf zrušit --namespace ${CI_ENVIRONMENT_SLUG} --with-namespace
# dovolujeme selhání, ale můžete to udělat jinak
  allow_failure: true
  proměnné:
    RUN_TESTS: 'yes'
# nastavujeme kontext v werf
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#infrastructure)
    WERF_KUBE_CONTEXT: 'admin@stage-cluster'
  tagy:
# používáme runner s tagem `werf-runner`
    - werf-runner
  artefakty:
# je třeba sestavit artefakt, aby ho bylo možné vidět
# v pipeline a stáhnout — například pro hlubší zkoumání
    cesty:
      - ./tests/${CI_COMMIT_REF_SLUG}/*
# artefakty starší než týden budou odstraněny
    expire_in: 7 dnů
# důležité: tyto řádky odpovídají za parsování zprávy GitLab’em
    zprávy:
      junit: ./tests/${CI_COMMIT_REF_SLUG}/report.xml

# pro zjednodušení jsou zde zobrazeny pouze dvě fáze
# ve skutečnosti jich budete mít více — minimálně kvůli nasazení
fáze:
  - build
  - tests

build:
  fáze: build
  skript:
# sestavení — znovu podle dokumentace k werf
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#build-stage)
    - type multiwerf && source <(multiwerf use ${WERF_VERSION})
    - werf verze
    - type werf && source <(werf ci-env gitlab --tagging-strategy tag-or-branch --verbose)
    - werf sestavit-a-publikovat --stages-storage :local
  tagy:
    - werf-runner
  kromě:
    - schedules

spustit testy:
  <<: *test-base
  prostředí:
# "sama sůl" pojmenování namespace’
# (https://docs.gitlab.com/ce/ci/variables/predefined_variables.html)
    jméno: tests-${CI_COMMIT_REF_SLUG}
  fáze: tests
  kromě:
    - schedules

Kubernetes

Nyní v adresáři .helm/templates vytvoříme YAML s Jobem — tests-job.yaml — pro spuštění testů a potřebnými Kubernetes zdroji. Vysvětlení viz po výpisu:

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

Jaké zdroje jsou popsány v této konfiguraci? Při nasazení vytváříme unikátní namespace pro projekt (to je uvedeno již v .gitlab-ci.yaml — tests-${CI_COMMIT_REF_SLUG}) a do něj nasazujeme:

  1. ConfigMap se skriptem testu;
  2. Úloha s popisem podu a uvedenou direktivou command, která právě spouští testy;
  3. PV a PVC, které umožňují ukládat data testů.

Věnujte pozornost podmínce na začátku manifestu — ostatní YAML soubory Helm chartu s aplikací by měly být obaleny if konstrukcí, aby se nen nasazovaly při testování. To znamená: {{- if ne .Values.global.run_tests "yes" }} --- jiný yaml soubor {{- end }} Nicméně, pokud testy

vyžadují určitou infrastrukturu

Nicméně, pokud testy vyžadují nějakou infrastrukturu (například, Redis, RabbitMQ, Mongo, PostgreSQL…) — jejich YAML soubory můžete ne vypnout. Rozviňte je a v testovací prostředí… samozřejmě, přizpůsobte si je podle svého uvážení.

Závěrečná úprava

Jelikož sestavení a nasazení pomocí werf zatím funguje pouze na build serveru (s gitlab-runner), a pod s testy se spouští na masteru, bude potřeba vytvořit adresář /mnt/tests na masteru a předat ho runneru, například, přes NFS. Rozšířený příklad s vysvětlením naleznete v dokumentaci K8s.

Výsledkem bude:

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

Nikdo vám nebrání vytvořit NFS sdílení přímo na gitlab-runneru a poté je připojit do podů.

Poznámka

Možná se ptáte, proč vůbec komplikovat vznik Jobu, když můžete jednoduše spustit skript s testy přímo na shell-runneru? Odpověď je dost triviální…

Některé testy vyžadují přístup k infrastruktuře (MongoDB, RabbitMQ, PostgreSQL atd.) pro ověření správnosti práce s nimi. Děláme testování standardizovaným způsobem — díky tomuto přístupu je snadné zahrnout podobné dodatečné entity. Kromě toho získáváme standardní přístup k nasazení (i když s využitím NFS a dodatečným připojením adresářů).

Výsledek

Co uvidíme, když aplikujeme připravenou konfiguraci?

V merge requestu bude zobrazena souhrnná statistika testů spuštěných v jeho posledním pipeline:

JUnit v GitLab CI s Kubernetes

Na každou chybu zde můžete kliknout pro podrobnosti:

JUnit v GitLab CI s Kubernetes

NB: Pozorný čtenář si všimne, že testujeme aplikaci NodeJS, zatímco na screenshotu je .NET… Nenechte se překvapit: během přípravy článku se nenašly žádné chyby v testování první aplikace, ale v jiné byly nalezeny.

Závěr

Jak je vidět, není to nic složitého!

V zásadě, pokud již máte shell builder a funguje, a Kubernetes nepotřebujete — připojit k němu testování bude ještě jednodušší úkol než je zde popsáno. A v dokumentaci GitLab CI najdete příklady pro Ruby, Go, Gradle, Maven a některé další.

P.S.

Přečtěte si také náš blog:

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster