JUnit în GitLab CI cu Kubernetes

Deși toată lumea știe că este important și necesar să ne testăm software-ul, iar mulți fac acest lucru automat de ceva vreme, pe Habr nu am găsit nicio rețetă pentru configurarea unei combinări dintre produsele populare din această nișă, cum ar fi (preferatul nostru) GitLab și JUnit. Să umplem această lacună!

JUnit în GitLab CI cu Kubernetes

Introducere

Pentru început, voi delimita contextul:

  • Având în vedere că toate aplicațiile noastre funcționează în Kubernetes, vom discuta despre rularea testelor în infrastructura corespunzătoare.
  • Pentru construirea și implementarea, folosim werf (în sensul componentelor de infrastructură, aceasta înseamnă de asemenea că este folosit Helm).
  • Nu voi intra în detalii despre crearea efectivă a testelor: în cazul nostru, clientul scrie testele el însuși, iar noi ne ocupăm doar de rularea lor (și de generarea raportului corespunzător în merge request).


Care va fi secvența generală a pașilor?

  1. Construirea aplicației — vom omite descrierea acestui pas.
  2. Implementarea aplicației într-un namespace separat al clusterei Kubernetes și rularea testării.
  3. Căutarea artefactelor și parsarea raportului JUnit de către GitLab.
  4. Ștergerea namespace-ului creat anterior.

Acum — să trecem la implementare!

Configurare

GitLab CI

Să începem cu fragmentul .gitlab-ci.yaml, care descrie implementarea aplicației și rularea testelor. Listingul a fost destul de voluminos, așa că a fost completat cu comentarii suplimentare:

variabile:
# declarăm versiunea werf pe care intenționăm să o utilizăm
  WERF_VERSION: "1.0 beta"

.base_deploy: &base_deploy
  script:
# creăm namespace în K8s, dacă nu există
    - kubectl --context="${WERF_KUBE_CONTEXT}" get ns ${CI_ENVIRONMENT_SLUG} || kubectl create ns ${CI_ENVIRONMENT_SLUG}
# încărcăm werf și implementăm - mai multe detalii puteți găsi în documentație
# (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:-''}"
# transmitem variabila `run_tests`
# aceasta va fi utilizată în renderizarea Helm-ului
      --set "global.run_tests=${RUN_TESTS:-no}"
      --set "global.env=${CI_ENVIRONMENT_SLUG}"
# modificăm timeout-ul (unele teste pot dura mult) și îl transmitem în release
      --set "global.ci_timeout=${CI_TIMEOUT:-900}"
     --timeout ${CI_TIMEOUT:-900}
  dependencies:
    - Build

.test-base: &test-base
  extends: .base_deploy
  before_script:
# creăm un director pentru viitorul raport, bazat pe $CI_COMMIT_REF_SLUG
    - mkdir /mnt/tests/${CI_COMMIT_REF_SLUG} || true
# un workaround necesar, deoarece GitLab vrea să primească artefactele în build-dir-ul său
    - mkdir ./tests || true
    - ln -s /mnt/tests/${CI_COMMIT_REF_SLUG} ./tests/${CI_COMMIT_REF_SLUG}
  after_script:
# după terminarea testelor, ștergem release-ul împreună cu Job-ul
# (și, poate, infrastructura acestuia)
    - 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
# permitem eșecurile, dar puteți proceda altfel
  allow_failure: true
  variables:
    RUN_TESTS: 'yes'
# stabilim contextul în werf
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#infrastructure)
    WERF_KUBE_CONTEXT: 'admin@stage-cluster'
  tags:
# folosim un runner cu eticheta `werf-runner`
    - werf-runner
  artifacts:
# este necesar să generăm un artefact pentru a putea fi vizibil
# în pipeline și descărcat - de exemplu, pentru o analiză mai amănunțită
    paths:
      - ./tests/${CI_COMMIT_REF_SLUG}/*
# artefactele mai vechi de o săptămână vor fi șterse
    expire_in: 7 zile
# important: aceste linii sunt responsabile pentru parsarea raportului de către GitLab
    reports:
      junit: ./tests/${CI_COMMIT_REF_SLUG}/report.xml

# pentru simplificare, aici sunt prezentate doar două etape
# în realitate, veți avea mai multe - cel puțin din cauza implementării
stages:
  - build
  - tests

build:
  stage: build
  script:
# construcția - iarăși conform documentației 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:
# "sarea esențială" a denumirii namespace-ului
# (https://docs.gitlab.com/ce/ci/variables/predefined_variables.html)
    name: tests-${CI_COMMIT_REF_SLUG}
  stage: tests
  except:
    - schedules

Kubernetes

Acum în directorul .helm/templates vom crea un YAML cu Job-ul — tests-job.yaml — pentru a rula testele și resursele necesare Kubernetes. Explicațiile sunt după listare:

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

Ce resurse sunt descrise în această configurație? La desfășurare, creăm un namespace unic pentru proiect (acesta este specificat încă în .gitlab-ci.yaml — tests-${CI_COMMIT_REF_SLUG}) și îl implementăm în:

  1. ConfigMap cu scriptul testului;
  2. Job cu descrierea pod-ului și directiva specificată command, care lansează testele;
  3. PV și PVC, care permit stocarea datelor testelor.

Atenție la condiția de introducere de la if în partea de sus a manifestului — prin urmare, celelalte fișiere YAML ale chart-ului Helm cu aplicația trebuie să fie învelite în o structură inversă , astfel încât să nu fie desfășurate în timpul testării. Așadar:

{{- if ne .Values.global.run_tests "yes" }}
---
un alt yaml
{{- end }}

Totuși, dacă testele cer o anumită infrastructură (de exemplu, Redis, RabbitMQ, Mongo, PostgreSQL…) — fișierele lor YAML pot nu fi dezactivate. Extindeți-le și în mediu de testare… desigur, modificându-le după bunul plac.

Ultimul retuș

Deoarece compilarea și implementarea folosind werf funcționează deocamdată doar pe serverul de compilare (cu gitlab-runner), iar podul cu teste se lansează pe master, va fi necesar să creați un director /mnt/tests pe master și să-l oferiți runner-ului, de exemplu, prin NFS. Un exemplu detaliat cu explicații poate fi găsit în documentația K8s.

Rezultatul va fi:

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

Nimeni nu interzice să faceți un share NFS direct pe gitlab-runner, după care să-l montați în poduri.

Notă

Probabil vă întrebați de ce complicăm totul creând un Job, când putem pur și simplu să lansăm scriptul cu teste direct pe shell-runner? Răspunsul este destul de trivial…

Unele teste necesită acces la infrastructură (MongoDB, RabbitMQ, PostgreSQL etc.) pentru a verifica corectitudinea operațiunilor cu acestea. Facem testarea unificată — cu această abordare, includerea entităților suplimentare devine ușoară. În plus, obținem standard o abordare în implementare (chiar dacă folosim NFS, montând directoare suplimentare).

Rezultatul

Ce vom vedea când aplicăm configurația pregătită?

În merge request va fi afișată o statistică sumară pentru testele rulate în ultimul său pipeline:

JUnit în GitLab CI cu Kubernetes

Pentru fiecare eroare, aici se poate da clic pentru a obține detalii:

JUnit în GitLab CI cu Kubernetes

NB: Cititorul atent va observa că testăm aplicația NodeJS, iar în capturile de ecran — .NET… Nu vă mirați: doar în cadrul pregătirii articolului nu au fost găsite erori în testarea primei aplicații, dar au fost găsite în cealaltă.

Concluzie

După cum se vede, nimic complicat!

În principiu, dacă aveți deja un builder shell și funcționează, iar Kubernetes nu vă este necesar — atașarea testării la acesta va fi o sarcină și mai simplă decât cea descrisă aici. Și în documentația GitLab CI veți găsi exemple pentru Ruby, Go, Gradle, Maven și câteva altele.

P.S.

Citiți și în blogul nostru:

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster