JUnit in GitLab CI con Kubernetes

Nonostante tutti sappiano che è importante e necessario testare il proprio software e che molti lo fanno già in modo automatico, su Habr non è stato trovato alcun ricetta per configurare l'integrazione di prodotti così popolari in questo settore, come (nostro amato) GitLab e JUnit. Colmiamo questa lacuna!

JUnit in GitLab CI con Kubernetes

Introduzione

Per iniziare, delineerò il contesto:

  • Poiché tutte le nostre applicazioni funzionano in Kubernetes, verrà esaminato l'avvio dei test nell'infrastruttura corrispondente.
  • Per la costruzione e il deployment, utilizziamo werf (in termini di componenti infrastrutturali, questo significa anche che viene utilizzato Helm).
  • Non entrerò nei dettagli della creazione dei test: nel nostro caso, il cliente scrive i test da solo e noi ci limitiamo a garantirne l'esecuzione (e l'esistenza di un report corrispondente nella merge request).


Quale sarà la sequenza generale delle azioni?

  1. La costruzione dell'applicazione - salteremo la descrizione di questo passaggio.
  2. Il deployment dell'applicazione in un namespace separato del cluster Kubernetes e l'avvio del testing.
  3. Ricerca degli artefatti e parsing del report JUnit da parte di GitLab.
  4. Cancellazione del namespace creato in precedenza.

Ora, passiamo all'implementazione!

Configurazione

GitLab CI

Iniziamo con il frammento .gitlab-ci.yaml, che descrive il deployment dell'applicazione e l'avvio dei test. Il listing è piuttosto voluminoso, quindi è stato ampiamente arricchito di commenti:

variabili:
# dichiariamo la versione di werf che intendiamo utilizzare
  WERF_VERSION: "1.0 beta"

.base_deploy: &base_deploy
  script:
# creiamo un namespace in K8s, se non esiste
    - kubectl --context="${WERF_KUBE_CONTEXT}" get ns ${CI_ENVIRONMENT_SLUG} || kubectl create ns ${CI_ENVIRONMENT_SLUG}
# scarichiamo werf e deploy — per ulteriori informazioni consultare la documentazione
# (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:-''}"
# passiamo la variabile `run_tests`
# sarà utilizzata nel rendering del rilascio Helm
      --set "global.run_tests=${RUN_TESTS:-no}"
      --set "global.env=${CI_ENVIRONMENT_SLUG}"
# cambiamo il timeout (ci possono essere test lunghi) e lo passiamo al rilascio
      --set "global.ci_timeout=${CI_TIMEOUT:-900}"
     --timeout ${CI_TIMEOUT:-900}
  dependencies:
    - Build

.test-base: &test-base
  extends: .base_deploy
  before_script:
# creiamo una directory per il futuro rapporto, in base a $CI_COMMIT_REF_SLUG
    - mkdir /mnt/tests/${CI_COMMIT_REF_SLUG} || true
# un workaround necessario, poiché GitLab vuole ricevere gli artefatti nella sua build-dir
    - mkdir ./tests || true
    - ln -s /mnt/tests/${CI_COMMIT_REF_SLUG} ./tests/${CI_COMMIT_REF_SLUG}
  after_script:
# dopo la fine dei test, eliminiamo il rilascio insieme al Job
# (e, possibilmente, alla sua infrastruttura)
    - 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
# consentiamo i fallimenti, ma potete fare diversamente
  allow_failure: true
  variables:
    RUN_TESTS: 'yes'
# impostiamo il contesto in werf
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#infrastructure)
    WERF_KUBE_CONTEXT: 'admin@stage-cluster'
  tags:
# utilizziamo il runner con il tag `werf-runner`
    - werf-runner
  artifacts:
# è necessario raccogliere l'artefatto affinché possa essere visto
# nel pipeline e scaricato — per uno studio più attento
    paths:
      - ./tests/${CI_COMMIT_REF_SLUG}/*
# gli artefatti più vecchi di una settimana saranno eliminati
    expire_in: 7 day
# importante: queste righe sono responsabili dell'analisi del rapporto da parte di GitLab
    reports:
      junit: ./tests/${CI_COMMIT_REF_SLUG}/report.xml

# per semplificare sono mostrate solo due fasi
# in realtà ne avrete di più — almeno a causa del deploy
stages:
  - build
  - tests

build:
  stage: build
  script:
# build — di nuovo secondo la documentazione su 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

esegui test:
  <<: *test-base
  environment:
# "il vero nome" del 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 un YAML con il Job — tests-job.yaml — per l'esecuzione dei test e le risorse Kubernetes necessarie. Le spiegazioni sono disponibili dopo il listing:

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

Quali risorse sono descritte in questa configurazione? Durante il deploy creiamo un namespace unico per il progetto (questo è specificato anche in .gitlab-ci.yamltests-${CI_COMMIT_REF_SLUG}) e in esso rilasciamo:

  1. ConfigMap con lo script di test;
  2. Job con la descrizione del pod e la direttiva specificata command, che avvia effettivamente i test;
  3. PV e PVC, che consentono di memorizzare i dati dei test.

Si prega di notare la condizione di input con if all'inizio del manifesto — di conseguenza, altri file YAML del chart Helm con l'app devono essere racchiusi in una controparte costruzione, affinché non siano rilasciati durante il test. Cioè:

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

Tuttavia, se i test richiedono un'infrastruttura particolare. (ad esempio, Redis, RabbitMQ, Mongo, PostgreSQL…) — i loro YAML possono non essere disattivati. Espandili e anche loro nell'ambiente di test… naturalmente, modificandoli a tuo piacimento.

Ultimo ritocco

Poiché la costruzione e il deployment tramite werf attualmente funzionano solo sul server di build (con gitlab-runner), e il pod con i test viene eseguito sul master, sarà necessario creare una directory /mnt/tests sul master e fornirla al runner, ad esempio, tramite NFS. Un esempio distribuito con spiegazioni può essere trovato nella documentazione K8s.

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 ha vietato di creare una condivisione NFS direttamente su gitlab-runner, dopo di che montarla nei pod.

Nota

Potresti chiederti perché complicare tutto creando un Job, se è possibile semplicemente eseguire uno script con i test direttamente sul runner shell? La risposta è piuttosto triviale…

Alcuni test richiedono l'interazione con l'infrastruttura (MongoDB, RabbitMQ, PostgreSQL, ecc.) per verificare la correttezza del funzionamento con esse. Rendiamo i test uniformi: con questo approccio includere tali entità aggiuntive diventa facile. Inoltre, otteniamo un approccio standard al deployment (anche se con l'uso di NFS e montaggio aggiuntivo delle directory).

Risultato

Cosa vedremo quando applichiamo la configurazione preparata?

Nel merge request verrà mostrata una statistica riepilogativa sui test eseguiti nel suo ultimo pipeline:

JUnit in GitLab CI con Kubernetes

Cliccando su ogni errore qui, si possono ottenere dettagli:

JUnit in GitLab CI con Kubernetes

NB: Il lettore attento noterà che stiamo testando un'applicazione NodeJS, mentre negli screenshot c'è .NET… Non sorprenderti: semplicemente nella preparazione dell'articolo non ci sono stati errori nel testing della prima applicazione, ma ne sono stati trovati in un'altra.

Conclusione

Come si può vedere, non c'è niente di complicato!

Fondamentalmente, se hai già un costruttore shell e funziona, e Kubernetes non ti serve, aggiungere i test sarà un compito ancora più semplice rispetto a quanto descritto qui. Inoltre, nella documentazione di GitLab CI troverai esempi per Ruby, Go, Gradle, Maven e alcuni altri.

P.S.

Leggi anche nel nostro blog:

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster