JUnit in GitLab CI con Kubernetes

Nonostante tutti sappiano perfettamente che testare il proprio software è importante e necessario, e molti lo fanno automaticamente da molto tempo, nella vastità di Habr non esisteva un'unica ricetta per creare una combinazione di prodotti così popolari in questa nicchia come (il nostro preferito) GitLab e JUnit. Colmiamo questa lacuna!

JUnit in GitLab CI con Kubernetes

Introduttivo

Per prima cosa, lasciatemi fornire un po’ di contesto:

  • Poiché tutte le nostre applicazioni funzionano su Kubernetes, prenderemo in considerazione l'esecuzione di test sull'infrastruttura appropriata.
  • Per l'assemblaggio e la distribuzione utilizziamo werf (in termini di componenti infrastrutturali ciò significa automaticamente anche il coinvolgimento di Helm).
  • Non entrerò nei dettagli della creazione vera e propria dei test: nel nostro caso è il cliente stesso a scrivere i test, e noi ci limitiamo a garantirne l'avvio (e la presenza di un corrispondente report nella richiesta di fusione).


Come sarà la sequenza generale delle azioni?

  1. Creazione dell'applicazione: ometteremo la descrizione di questa fase.
  2. Distribuisci l'applicazione in uno spazio dei nomi separato del cluster Kubernetes e avvia il test.
  3. Ricerca di artefatti e analisi dei report JUnit con GitLab.
  4. Eliminazione di uno spazio dei nomi creato in precedenza.

Ora - all'implementazione!

registrazione

CI GitLab

Cominciamo con un frammento .gitlab-ci.yaml, che descrive la distribuzione dell'applicazione e l'esecuzione dei test. L'elenco si è rivelato piuttosto voluminoso, quindi è stato completamente integrato con commenti:

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

Ora nella directory .helm/templates creiamo YAML con Job - tests-job.yaml — per eseguire i test e le risorse Kubernetes di cui ha bisogno. Vedi le spiegazioni dopo l'elenco:

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

Che tipo di risorse descritto in questa configurazione? Durante la distribuzione, creiamo uno spazio dei nomi univoco per il progetto (questo è indicato in .gitlab-ci.yaml - tests-${CI_COMMIT_REF_SLUG}) e stendilo:

  1. Mappa di configurazione con script di prova;
  2. Lavoro con una descrizione del pod e la direttiva specificata command, che esegue semplicemente i test;
  3. FV e PVC, che consentono di memorizzare i dati dei test.

Presta attenzione alla condizione introduttiva con if all'inizio del manifest - di conseguenza, devono essere racchiusi altri file YAML del grafico Helm con l'applicazione retromarcia progettare in modo che non vengano distribuiti durante i test. Questo è:

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

Tuttavia, se i test richiedono alcune infrastrutture (ad esempio, Redis, RabbitMQ, Mongo, PostgreSQL...) - i loro YAML possono essere no spegnere. Distribuiscili anche in un ambiente di test... modificandoli come ritieni opportuno, ovviamente.

Tocco finale

Perché assemblaggio e distribuzione utilizzando werf funziona per ora solo sul build server (con gitlab-runner), e il pod con i test viene lanciato sul master, dovrai creare una directory /mnt/tests sul maestro e dallo al corridore, ad esempio, tramite NFS. Un esempio dettagliato con spiegazioni può essere trovato in Documentazione del K8.

Il risultato sarà:

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

Nessuno vieta di creare una condivisione NFS direttamente su gitlab-runner e poi montarla nei pod.

Nota

Forse ti starai chiedendo perché complicare tutto creando un Job se puoi semplicemente eseguire uno script con test direttamente sullo Shell Runner? La risposta è abbastanza banale...

Alcuni test richiedono l'accesso all'infrastruttura (MongoDB, RabbitMQ, PostgreSQL, ecc.) per verificare che funzionino correttamente. Rendiamo i test unificati: con questo approccio diventa facile includere tali entità aggiuntive. Oltre a questo, otteniamo standard approccio di distribuzione (anche se si utilizza NFS, montaggio aggiuntivo di directory).

risultato

Cosa vedremo quando applicheremo la configurazione preparata?

La richiesta di unione mostrerà le statistiche di riepilogo per i test eseguiti nella sua pipeline più recente:

JUnit in GitLab CI con Kubernetes

È possibile fare clic su ciascun errore qui per i dettagli:

JUnit in GitLab CI con Kubernetes

NB: Il lettore attento noterà che stiamo testando un'applicazione NodeJS e negli screenshot - .NET... Non stupitevi: è solo che durante la preparazione dell'articolo non sono stati trovati errori nel testare la prima applicazione, ma sono sono stati trovati in un altro.

conclusione

Come puoi vedere, niente di complicato!

In linea di principio, se hai già uno Shell Collector e funziona, ma non hai bisogno di Kubernetes, allegarvi dei test sarà un compito ancora più semplice di quello descritto qui. E dentro Documentazione CI di GitLab troverai esempi per Ruby, Go, Gradle, Maven e alcuni altri.

PS

Leggi anche sul nostro blog:

Fonte: habr.com

Aggiungi un commento