JUnit în GitLab CI cu Kubernetes

În ciuda faptului că toată lumea știe perfect că testarea software-ului dvs. este importantă și necesară și mulți o fac automat de mult timp, în vastitatea Habr nu exista o singură rețetă pentru a crea o combinație de produse atât de populare în această nișă ca (preferatul nostru) GitLab și JUnit. Să umplem acest gol!

JUnit în GitLab CI cu Kubernetes

introductiv

Mai întâi, permiteți-mi să ofer un context:

  • Deoarece toate aplicațiile noastre rulează pe Kubernetes, vom lua în considerare efectuarea de teste pe infrastructura adecvată.
  • Pentru asamblare și desfășurare folosim werf (în ceea ce privește componentele infrastructurii, acest lucru înseamnă și automat că Helm este implicat).
  • Nu voi intra în detalii despre crearea efectivă a testelor: în cazul nostru, clientul scrie el însuși testele, iar noi asigurăm doar lansarea acestora (și prezența unui raport corespunzător în cererea de fuziune).


Cum va arăta secvența generală de acțiuni?

  1. Construirea aplicației - vom omite descrierea acestei etape.
  2. Implementați aplicația într-un spațiu de nume separat al clusterului Kubernetes și începeți testarea.
  3. Căutarea artefactelor și analizarea rapoartelor JUnit cu GitLab.
  4. Ștergerea unui spațiu de nume creat anterior.

Acum - la implementare!

ajustare

GitLab CI

Să începem cu un fragment .gitlab-ci.yaml, care descrie implementarea aplicației și rularea testelor. Lista sa dovedit a fi destul de voluminoasă, așa că a fost complet completată cu comentarii:

variables:
# объявляем версию werf, которую собираемся использовать
  WERF_VERSION: "1.0 beta"

.base_deploy: &base_deploy
  script:
# создаем namespace в K8s, если его нет
    - kubectl --context="${WERF_KUBE_CONTEXT}" get ns ${CI_ENVIRONMENT_SLUG} || kubectl create ns ${CI_ENVIRONMENT_SLUG}
# загружаем werf и деплоим — подробнее об этом см. в документации
# (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:-''}"
# передаем переменную `run_tests`
# она будет использоваться в рендере Helm-релиза
      --set "global.run_tests=${RUN_TESTS:-no}"
      --set "global.env=${CI_ENVIRONMENT_SLUG}"
# изменяем timeout (бывают долгие тесты) и передаем его в релиз
      --set "global.ci_timeout=${CI_TIMEOUT:-900}"
     --timeout ${CI_TIMEOUT:-900}
  dependencies:
    - Build

.test-base: &test-base
  extends: .base_deploy
  before_script:
# создаем директорию для будущего отчета, исходя из $CI_COMMIT_REF_SLUG
    - mkdir /mnt/tests/${CI_COMMIT_REF_SLUG} || true
# вынужденный костыль, т.к. GitLab хочет получить артефакты в своем build-dir’е
    - mkdir ./tests || true
    - ln -s /mnt/tests/${CI_COMMIT_REF_SLUG} ./tests/${CI_COMMIT_REF_SLUG}
  after_script:
# после окончания тестов удаляем релиз вместе с Job’ом
# (и, возможно, его инфраструктурой)
    - 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
# мы разрешаем падения, но вы можете сделать иначе
  allow_failure: true
  variables:
    RUN_TESTS: 'yes'
# задаем контекст в werf
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#infrastructure)
    WERF_KUBE_CONTEXT: 'admin@stage-cluster'
  tags:
# используем раннер с тегом `werf-runner`
    - werf-runner
  artifacts:
# требуется собрать артефакт для того, чтобы его можно было увидеть
# в пайплайне и скачать — например, для более вдумчивого изучения
    paths:
      - ./tests/${CI_COMMIT_REF_SLUG}/*
# артефакты старше недели будут удалены
    expire_in: 7 day
# важно: эти строки отвечают за парсинг отчета GitLab’ом
    reports:
      junit: ./tests/${CI_COMMIT_REF_SLUG}/report.xml

# для упрощения здесь показаны всего две стадии
# в реальности же у вас их будет больше — как минимум из-за деплоя
stages:
  - build
  - tests

build:
  stage: build
  script:
# сборка — снова по документации по 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:
# "сама соль" именования namespace’а
# (https://docs.gitlab.com/ce/ci/variables/predefined_variables.html)
    name: tests-${CI_COMMIT_REF_SLUG}
  stage: tests
  except:
    - schedules

Kubernetes

Acum în director .helm/templates să creăm YAML cu Job - tests-job.yaml — pentru a rula teste și resursele Kubernetes de care are nevoie. Vedeți explicațiile 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 fel de resurse descrise în această configurație? La implementare, creăm un spațiu de nume unic pentru proiect (acest lucru este indicat în .gitlab-ci.yaml - tests-${CI_COMMIT_REF_SLUG}) și lansați-l:

  1. ConfigMap cu script de testare;
  2. Loc de munca cu o descriere a podului și directiva specificată command, care doar rulează testele;
  3. PV și PVC, care vă permit să stocați date de testare.

Atenție la starea introductivă cu if la începutul manifestului - în consecință, alte fișiere YAML ale diagramei Helm cu aplicația trebuie să fie împachetate în verso proiectați astfel încât să nu fie implementate în timpul testării. Acesta este:

{{- if ne .Values.global.run_tests "yes" }}
---
я другой ямлик
{{- end }}

Cu toate acestea, dacă testele necesită o anumită infrastructură (de exemplu, Redis, RabbitMQ, Mongo, PostgreSQL...) - YAML-urile lor pot fi nu opriți. Implementați-le și într-un mediu de testare... ajustându-le după cum credeți de cuviință, desigur.

Efectul final

Deoarece asamblarea și implementarea folosind werf funcționează deocamdată numai pe serverul de compilare (cu gitlab-runner), iar podul cu teste este lansat pe master, va trebui să creați un director /mnt/tests pe stăpân și dă-l alergătorului, 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ă faci o partajare NFS direct pe gitlab-runner și apoi să o montezi în pod-uri.

Nota

S-ar putea să vă întrebați de ce complicați totul prin crearea unui Job dacă pur și simplu puteți rula un script cu teste direct pe shell runner? Raspunsul este destul de banal...

Unele teste necesită acces la infrastructură (MongoDB, RabbitMQ, PostgreSQL etc.) pentru a verifica dacă funcționează corect. Facem testarea unificată - cu această abordare, devine ușor să includeți astfel de entități suplimentare. Pe lângă asta, primim standard abordare de implementare (chiar dacă se utilizează NFS, montare suplimentară a directoarelor).

Rezultat

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

Solicitarea de îmbinare va afișa statistici rezumate pentru testele rulate în cel mai recent canal:

JUnit în GitLab CI cu Kubernetes

Fiecare eroare poate fi făcută clic aici pentru detalii:

JUnit în GitLab CI cu Kubernetes

NB: Cititorul atent va observa că testăm o aplicație NodeJS, iar în capturi de ecran - .NET... Nu vă mirați: tocmai în cadrul pregătirii articolului, nu au fost găsite erori la testarea primei aplicații, dar au fost găsite în alta.

Concluzie

După cum puteți vedea, nimic complicat!

În principiu, dacă aveți deja un colector de shell și funcționează, dar nu aveți nevoie de Kubernetes, atașarea testării la acesta va fi o sarcină și mai simplă decât cea descrisă aici. Si in Documentația GitLab CI veți găsi exemple pentru Ruby, Go, Gradle, Maven și alții.

PS

Citește și pe blogul nostru:

Sursa: www.habr.com

Adauga un comentariu