JUnit in GitLab CI con Kubernetes

Nonostante sia ben noto a tutti l'importanza di testare il proprio software e che molti lo facciano già automaticamente, su Habr non si è trovata alcuna guida per configurare l'integrazione di strumenti popolari come (il nostro preferito) GitLab e JUnit. Colmiamo questa lacuna!

JUnit in GitLab CI con Kubernetes

Introduzione

Iniziamo a delineare il contesto:

  • Poiché tutte le nostre applicazioni funzionano su Kubernetes, analizzeremo l'esecuzione dei test nell'infrastruttura corrispondente.
  • Per la costruzione e il deployment utilizziamo werf (significa che in termini di componenti infrastrutturali ciò implica automaticamente l'uso di Helm).
  • Non entrerò nei dettagli della creazione dei test: nel nostro caso, il cliente scrive i test da solo e noi ci occupiamo solo della loro esecuzione (e della disponibilità del relativo rapporto nella richiesta di merge).


Quale sarà la sequenza generale delle azioni?

  1. Costruzione dell'applicazione — salteremo la descrizione di questa fase.
  2. Deployment dell'applicazione in uno spazio dei nomi separato del cluster Kubernetes e avvio del testing.
  3. Ricerca degli artifact e parsing del rapporto JUnit da parte di GitLab.
  4. Cancellazione dello spazio dei nomi precedentemente creato.

Ora — passiamo all'implementazione!

Impostazione

GitLab CI

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

variables:
# dichiarando la versione di werf che intendiamo utilizzare
  WERF_VERSION: "1.0 beta"

.base_deploy: &base_deploy
  script:
# creiamo lo spazio dei nomi in K8s, se non esiste
    - kubectl --context="${WERF_KUBE_CONTEXT}" get ns ${CI_ENVIRONMENT_SLUG} || kubectl create ns ${CI_ENVIRONMENT_SLUG}
# carichiamo werf e facciamo il deploy — per ulteriori dettagli vedere 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}"
# modifichiamo il timeout (poiché ci potrebbero essere test lunghi) e lo passiamo nel 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
# workaround forzato, poiché GitLab si aspetta di ricevere artifact nel proprio build-dir
    - mkdir ./tests || true
    - ln -s /mnt/tests/${CI_COMMIT_REF_SLUG} ./tests/${CI_COMMIT_REF_SLUG}
  after_script:
# alla fine dei test cancelliamo 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 è possibile 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 un runner con il tag `werf-runner`
    - werf-runner
  artifacts:
# è necessario raccogliere artifact affinché possano essere visualizzati
# nel pipeline e scaricati — per un'analisi più approfondita
    paths:
      - ./tests/${CI_COMMIT_REF_SLUG}/*
# gli artifact più vecchi di una settimana verranno cancellati
    expire_in: 7 day
# importante: queste righe gestiscono il parsing del rapporto da parte di GitLab
    reports:
      junit: ./tests/${CI_COMMIT_REF_SLUG}/report.xml

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

build:
  stage: build
  script:
# costruzione — ancora una volta secondo la documentazione di 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:
# "la vera essenza" della denominazione dello spazio dei nomi
# (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 creeremo un YAML con il Job — tests-job.yaml — per avviare i test e le risorse necessarie a Kubernetes. Le spiegazioni seguono 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 deployment creiamo un namespace unico per il progetto (questo è indicato anche in .gitlab-ci.yamltests-${CI_COMMIT_REF_SLUG}) e vi distribuiamo:

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

Si prega di notare la condizione introduttiva con in etcdhelper, che non modificherà il servizio kube-dns. all'inizio del manifesto — pertanto, altri file YAML del chart Helm con l'applicazione devono essere racchiusi in una costruzione inversa, affinché non vengano distribuiti durante il test. Cioè:

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

Tuttavia, se i test richiedono alcune infrastrutture (ad esempio, Redis, RabbitMQ, Mongo, PostgreSQL…) — i loro YAML possono essere non disattivati. Distribuite anche quelli nell'ambiente di test… ovviamente, modificandoli a piacimento.

Touch Finale

Poiché la build e il deploy tramite werf attualmente funzionano solo sul server di build (con gitlab-runner), e il pod dei test viene eseguito sul master, sarà necessario creare una directory /mnt/tests sul master e condividerla con il runner, ad esempio, tramite NFS. Un esempio distribuito con spiegazioni può essere trovato nella documentazione di 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 vieta di creare una condivisione NFS direttamente su gitlab-runner, per poi montarla nei pod.

Nota

Potreste chiedervi perché complicarsi la vita creando un Job, se si può semplicemente eseguire lo script dei test direttamente sul runner shell? La risposta è piuttosto banale…

Alcuni test richiedono l'accesso all'infrastruttura (MongoDB, RabbitMQ, PostgreSQL, ecc.) per verificare il corretto funzionamento con esse. Rendiamo il test standardizzato — con questo approccio diventa facile includere tali ulteriori entità. Inoltre, otteniamo un approccio standard nel deploy (anche se con l'uso di NFS e montaggio aggiuntivo delle directory).

Risultato

Cosa vedremo quando applicheremo la configurazione preparata?

Nella merge request verrà mostrata una statistica complessiva sui test eseguiti nell'ultimo pipeline:

JUnit in GitLab CI con Kubernetes

Su ogni errore qui è possibile fare clic per ottenere maggiori dettagli:

JUnit in GitLab CI con Kubernetes

NB: Il lettore attento noterà che stiamo testando un'applicazione NodeJS, ma negli screenshot è presente .NET… Non sorprendetevi: semplicemente, nell'ambito della preparazione dell'articolo non sono stati trovati errori nel test del primo tipo di applicazione, ma sono stati riscontrati nell'altro.

Conclusione

Come si può vedere, niente di complicato!

In linea di principio, se avete già un builder shell e funziona, e Kubernetes non vi serve — aggiungere il test sarà un compito ancora più semplice di quello descritto qui. E nella documentazione di GitLab CI troverete esempi per Ruby, Go, Gradle, Maven e alcuni altri.

P.S.

Leggete anche nel nostro blog:

Fonte: habr.com

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