JUnit w GitLab CI z Kubernetes

Chociaż wszyscy doskonale wiedzą, że testowanie swojego oprogramowania jest ważne i konieczne, a wielu robi to automatycznie, na przestrzeni Habr nie znaleziono ani jednego przepisu dotyczącego konfiguracji połączenia takich popularnych w tej niszy produktów, jak (nasz ulubiony) GitLab i JUnit. Uzupełnijmy tę lukę!

JUnit w GitLab CI z Kubernetes

Wprowadzenie

Na początek określę kontekst:

  • Ponieważ wszystkie nasze aplikacje działają w Kubernetes, rozważymy uruchomienie testów w odpowiedniej infrastrukturze.
  • Do budowy i wdrożenia używamy werf (co w kontekście komponentów infrastrukturalnych automatycznie oznacza, że używamy Helm).
  • Nie będę zagłębiać się w szczegóły samego tworzenia testów: w naszym przypadku klient pisze testy sam, a my jedynie zapewniamy ich uruchomienie (i obecność odpowiedniego raportu w merge requeście).


Jak będzie wyglądać ogólna sekwencja działań?

  1. Budowa aplikacji — opis tego etapu pominiemy.
  2. Wdrożenie aplikacji do oddzielnej przestrzeni nazw klastra Kubernetes i rozpoczęcie testowania.
  3. Wyszukiwanie artefaktów i parsowanie raportu JUnit przez GitLab.
  4. Usunięcie wcześniej utworzonej przestrzeni nazw.

Teraz — do realizacji!

Konfiguracja

GitLab CI

Zacznijmy od fragmentu .gitlab-ci.yaml, opisującego wdrożenie aplikacji i uruchomienie testów. Listing jest dość obszerny, dlatego został dokładnie uzupełniony komentarzami:

zmienne:
# ogłaszamy wersję werf, którą zamierzamy używać
  WERF_VERSION: "1.0 beta"

.base_deploy: &base_deploy
  skrypt:
# tworzymy namespace w K8s, jeśli go nie ma
    - kubectl --context="${WERF_KUBE_CONTEXT}" get ns ${CI_ENVIRONMENT_SLUG} || kubectl create ns ${CI_ENVIRONMENT_SLUG}
# ładujemy werf i wdrażamy — więcej na ten temat w dokumentacji
# (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:-''}"
# przekazujemy zmienną `run_tests`
# będzie używana w renderowaniu Helm-release
      --set "global.run_tests=${RUN_TESTS:-no}"
      --set "global.env=${CI_ENVIRONMENT_SLUG}"
# zmieniamy timeout (mogą być długie testy) i przekazujemy go do release
      --set "global.ci_timeout=${CI_TIMEOUT:-900}"
     --timeout ${CI_TIMEOUT:-900}
  zależności:
    - Build

.test-base: &test-base
  rozszerza: .base_deploy
  before_script:
# tworzymy katalog dla przyszłego raportu, opierając się na $CI_COMMIT_REF_SLUG
    - mkdir /mnt/tests/${CI_COMMIT_REF_SLUG} || true
# przymusowa sztuczka, ponieważ GitLab chce uzyskać artefakty w swoim build-dirze
    - mkdir ./tests || true
    - ln -s /mnt/tests/${CI_COMMIT_REF_SLUG} ./tests/${CI_COMMIT_REF_SLUG}
  after_script:
# po zakończeniu testów usuwamy release razem z Jobem
# (i, być może, jego infrastrukturą)
    - 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
# zezwalamy na błędy, ale można to zrobić inaczej
  allow_failure: true
  zmienne:
    RUN_TESTS: 'yes'
# ustawiamy kontekst w werf
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#infrastructure)
    WERF_KUBE_CONTEXT: 'admin@stage-cluster'
  tagi:
# używamy runnera z tagiem `werf-runner`
    - werf-runner
  artefakty:
# konieczne do zbudowania artefaktu, aby można go było zobaczyć
# w pipeline i pobrać — na przykład do bardziej wnikliwego zbadania
    ścieżki:
      - ./tests/${CI_COMMIT_REF_SLUG}/*
# artefakty starsze niż tydzień zostaną usunięte
    expire_in: 7 dni
# ważne: te linie odpowiadają za parsowanie raportu przez GitLaba
    raporty:
      junit: ./tests/${CI_COMMIT_REF_SLUG}/report.xml

# dla uproszczenia pokazano tylko dwie etapy
# w rzeczywistości będziesz miał ich więcej — przynajmniej z powodu wdrożenia
etapy:
  - build
  - tests

build:
  etap: build
  skrypt:
# budowanie — ponownie według dokumentacji do 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
  tagi:
    - werf-runner
  z wyjątkiem:
    - schedules

uruchom testy:
  <<: *test-base
  środowisko:
# "sól" nazewnictwa namespace'a
# (https://docs.gitlab.com/ce/ci/variables/predefined_variables.html)
    name: tests-${CI_COMMIT_REF_SLUG}
  etap: tests
  z wyjątkiem:
    - schedules

Kubernetes

Teraz w katalogu .helm/templates stworzymy YAML z Jobem — tests-job.yaml — do uruchomienia testów oraz niezbędnymi dla niego zasobami Kubernetes. Wyjaśnienia znajdują się po liście:

{{- if eq .Values.global.run_tests "yes" }}
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: tests-script
data:
  tests.sh: |
    echo "======================"
    echo "${APP_NAME} TESTY"
    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 }}

Jakie zasoby są opisane w tej konfiguracji? Podczas wdrażania tworzymy unikalną dla projektu przestrzeń nazw (jest to wskazane również w .gitlab-ci.yamltests-${CI_COMMIT_REF_SLUG}) i wdrażamy do niej:

  1. ConfigMap ze skryptem testu;
  2. Zadanie z opisem podu oraz wskazaną dyrektywą komenda, która właśnie uruchamia testy;
  3. PV i PVC, które pozwalają przechowywać dane testów.

Zwróć uwagę na warunek wprowadzający z if na początku manifestu — odpowiednio, inne pliki YAML Helm-charta z aplikacją należy owinąć w odwrotną strukturę, aby one nie były wdrażane podczas testowania. Czyli:

{{- if ne .Values.global.run_tests "yes" }}
---
ja inny yaml
{{- end }}

Jednak, jeśli testy wymagają pewnej infrastruktury (na przykład, Redis, RabbitMQ, Mongo, PostgreSQL…) — ich YAML można nie wyłączanie. Rozwiń je i w środowisku testowym… oczywiście dostosowując do własnych potrzeb.

Ostateczny szlif

Ponieważ budowanie i wdrażanie za pomocą werf na razie działa tylko na serwerze build (z gitlab-runner), a pod z testami uruchamia się na masterze, konieczne będzie utworzenie katalogu /mnt/tests na masterze i przekazanie go do runnera, na przykład przez NFS. Rozwinięty przykład z objaśnieniami można znaleźć w dokumentacji K8s.

Wynikiem będzie:

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

Nikt nie zabrania stworzenia NFS-share bezpośrednio na gitlab-runnerze, a następnie zamontowania jej w podach.

Uwaga

Możesz zapytać, po co w ogóle komplikować tworząc Job’a, skoro można po prostu uruchomić skrypt z testami bezpośrednio na shell-runnerze? Odpowiedź jest dość trywialna…

Niektóre testy wymagają dostępu do infrastruktury (MongoDB, RabbitMQ, PostgreSQL itp.), aby sprawdzić poprawność działania z nimi. Używamy ujednoliconego podejścia do testowania — w takim podejściu łatwo jest dodać takie dodatkowe encje. Dodatkowo otrzymujemy standardowe podejście do wdrażania (nawet z wykorzystaniem NFS, dodatkowym montowaniem katalogów).

Wynik

Co zobaczymy, gdy zastosujemy przygotowaną konfigurację?

W merge request’cie będzie pokazana zbiorcza statystyka testów uruchomionych w jego ostatnim pipeline:

JUnit w GitLab CI z Kubernetes

Na każdy błąd można kliknąć, aby uzyskać szczegóły:

JUnit w GitLab CI z Kubernetes

NB: Uważny czytelnik zauważy, że testujemy aplikację NodeJS, a na zrzutach ekranu jest .NET… Nie dziw się: po prostu w ramach przygotowywania artykułu nie znaleziono błędów w testowaniu pierwszej aplikacji, ale znaleziono je w drugiej.

Podsumowanie

Jak widać, nic trudnego!

Zasadniczo, jeśli już masz shell-buildera i działa, a Kubernetes nie jest potrzebny — przymocowanie testowania do niego będzie jeszcze prostszym zadaniem niż opisane tutaj. A w dokumentacji GitLab CI znajdziesz przykłady dla Ruby, Go, Gradle, Maven i innych.

P.S.

Przeczytaj także na naszym blogu:

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster