JUnit i GitLab CI med Kubernetes

På trods af at alle ved, at det er vigtigt at teste sin software, og mange allerede gør det automatisk, er der ikke fundet nogen opskrift på Habr om opsætning af kombinationen af populære producenter i denne niche, såsom (vores favorit) GitLab og JUnit. Lad os udfylde dette hul!

JUnit i GitLab CI med Kubernetes

Indledende

Lad mig først skitsere konteksten:

  • Da alle vores applikationer kører i Kubernetes, vil vi se på kørsel af tests i den tilsvarende infrastruktur.
  • Til bygning og deployment bruger vi werf (i forhold til infrastrukturkomponenter betyder dette også automatisk, at Helm er involveret).
  • Jeg vil ikke gå i dybden med oprettelsen af tests: i vores tilfælde skriver kunden selv tests, og vi sikrer blot deres kørsel (og tilstedeværelsen af den relevante rapport i merge request’en).


Hvordan vil den overordnede sekvens af handlinger se ud?

  1. Bygning af applikationen - vi springer beskrivelsen af dette trin over.
  2. Deployment af applikationen til et separat namespace i Kubernetes-klusteret og start af testningen.
  3. Søgning efter artefakter og parsing af JUnit-rapporten af GitLab.
  4. Sletning af det tidligere oprettede namespace.

Nu - til implementeringen!

Indstillinger

GitLab CI

Lad os starte med fragmentet .gitlab-ci.yaml, som beskriver deployment af applikationen og kørsel af tests. Udkastet blev ret omfattende, så det er grundigt suppleret med kommentarer:

variabler:
# erklærer den version af werf, som vi vil bruge
  WERF_VERSION: "1.0 beta"

.base_deploy: &base_deploy
  script:
# opretter et namespace i K8s, hvis det ikke allerede findes
    - kubectl --context="${WERF_KUBE_CONTEXT}" get ns ${CI_ENVIRONMENT_SLUG} || kubectl create ns ${CI_ENVIRONMENT_SLUG}
# downloader werf og deployer — læs mere om dette i dokumentationen
# (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:-''}"
# overfører variablen `run_tests`
# den vil blive brugt i renderingen af Helm-udgivelsen
      --set "global.run_tests=${RUN_TESTS:-no}"
      --set "global.env=${CI_ENVIRONMENT_SLUG}"
# ændrer timeout (tests kan tage lang tid) og overfører den til udgivelsen
      --set "global.ci_timeout=${CI_TIMEOUT:-900}"
     --timeout ${CI_TIMEOUT:-900}
  dependencies:
    - Build

.test-base: &test-base
  extends: .base_deploy
  before_script:
# opretter en mappe til den kommende rapport, baseret på $CI_COMMIT_REF_SLUG
    - mkdir /mnt/tests/${CI_COMMIT_REF_SLUG} || true
# en tvungen workaround, da GitLab ønsker at modtage artefakter i sin build-mappe
    - mkdir ./tests || true
    - ln -s /mnt/tests/${CI_COMMIT_REF_SLUG} ./tests/${CI_COMMIT_REF_SLUG}
  after_script:
# efter testene afsluttes, fjerner vi udgivelsen sammen med jobbet
# (og måske dens 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
# vi tillader fejl, men du kan vælge anderledes
  allow_failure: true
  variables:
    RUN_TESTS: 'yes'
# sætter konteksten i werf
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#infrastructure)
    WERF_KUBE_CONTEXT: 'admin@stage-cluster'
  tags:
# bruger runneren med tagget `werf-runner`
    - werf-runner
  artifacts:
# kræves at genere artefakten for at kunne ses
# i pipeline og downloades — f.eks. til mere grundig analyse
    paths:
      - ./tests/${CI_COMMIT_REF_SLUG}/*
# artefakter ældre end en uge vil blive fjernet
    expire_in: 7 day
# vigtigt: disse linjer styrer parsing af rapporten af GitLab
    reports:
      junit: ./tests/${CI_COMMIT_REF_SLUG}/report.xml

# for enkelhed vises kun to faser her
# i virkeligheden vil du have flere — mindst på grund af deployment
stages:
  - build
  - tests

build:
  stage: build
  script:
# bygning — endnu engang henvises til dokumentationen for 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

afvikle tests:
  <<: *test-base
  environment:
# "selve navnet" på namespace’et
# (https://docs.gitlab.com/ce/ci/variables/predefined_variables.html)
    name: tests-${CI_COMMIT_REF_SLUG}
  stage: tests
  except:
    - schedules

Kubernetes

Nu i mappen .helm/templates opretter vi en YAML med Job'ens detaljer — tests-job.yaml — til at køre tests og de nødvendige ressourcer fra Kubernetes. Forklaringer ses efter listen:

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

Hvilke ressourcer er beskrevet i denne konfiguration? Ved deployment opretter vi et unikt namespace til projektet (det er angivet tidligere i .gitlab-ci.yaml — tests-${CI_COMMIT_REF_SLUG}) og vi distribuerer til det:

  1. ConfigMap med testscriptet;
  2. Job med beskrivelsen af pod'en og den angivne direktiv command, som netop starter testene;
  3. PV og PVC, der gør det muligt at gemme testdata.

Bemærk det indledende betingelse med if i starten af manifestet — andre YAML-filer i Helm-diagrammet skal derfor indhylles i den omvendte konstruktion, så de ikke distribueres under testning. Det vil sige:

{{- if ne .Values.global.run_tests "yes" }}
---
jeg en anden yaml
{{- end }}

Men hvis testene kræver en vis infrastruktur (for eksempel, Redis, RabbitMQ, Mongo, PostgreSQL…) — deres YAML-filer kan ikke slukkes. Udrul dem også i et testmiljø… selvfølgelig justeret som du ønsker.

Den afsluttende touch

Da opbygning og deployment ved hjælp af werf stadig fungerer kun på build-serveren (med gitlab-runner), og pod'en med testene køres på masteren, skal der oprettes et bibliotek /mnt/tests på masteren og gøres tilgængeligt for runneren, for eksempel via NFS. Et udrullet eksempel med forklaringer kan findes i K8s-dokumentationen.

Resultatet vil være:

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

Ingen forhindrer dig i at lave et NFS-share direkte på gitlab-runner'en og derefter montere det i pod'erne.

Bemærk

Måske spørger du, hvorfor overhovedet komplicere med at oprette en Job, når man blot kan køre et script med testene direkte på shell-runneren? Svaret er ganske trivielt...

Nogle tests kræver adgang til infrastrukturen (MongoDB, RabbitMQ, PostgreSQL osv.) for at kontrollere korrektheden af interaktionerne. Vi gør testningen ensartet — med denne tilgang bliver det let at inkludere sådanne ekstra enheder. Derudover opnår vi standard tilgang i deployment (selv med brug af NFS, med yderligere montering af mapper).

Resultat

Hvad ser vi, når vi anvender den forberedte konfiguration?

I merge request'en vil der blive vist en oversigt over statistikken for tests, der er kørt i dens seneste pipeline:

JUnit i GitLab CI med Kubernetes

For hver fejl her kan du klikke for at få detaljer:

JUnit i GitLab CI med Kubernetes

NB: Den opmærksomme læser vil bemærke, at vi tester en NodeJS-applikation, men på skærmbillederne er det .NET… Bliv ikke overrasket: blot fordi der ikke var fejl i testningen af den første applikation i artikelens forberedelse, fandt vi dem i den anden.

Konklusion

Som det kan ses, er der ikke noget kompliceret!

I princippet, hvis du allerede har en shell-builder og den fungerer, og Kubernetes ikke er nødvendig for dig — at koble testning til den vil være en endnu enklere opgave end den, der er beskrevet her. Og i GitLab CI-dokumentationen vil du finde eksempler for Ruby, Go, Gradle, Maven og nogle andre.

P.S.

Læs også på vores blog:

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster