JUnit v GitLab CI s Kubernetesom

Kljub dejstvu, da vsi dobro vedo, da je testiranje vaše programske opreme pomembno in potrebno, in mnogi to že dolgo počnejo samodejno, v prostranstvih Habra ni bilo niti enega recepta za postavitev kombinacije tako priljubljenih izdelkov v to nišo kot (naš najljubši) GitLab in JUnit. Zapolnimo to vrzel!

JUnit v GitLab CI s Kubernetesom

Uvodna

Najprej naj podam nekaj konteksta:

  • Ker se vse naše aplikacije izvajajo na Kubernetesu, bomo razmislili o izvajanju testov na ustrezni infrastrukturi.
  • Za montažo in postavitev uporabljamo werf (glede infrastrukturnih komponent to samodejno pomeni tudi Helm).
  • Ne bom se spuščal v podrobnosti dejanskega ustvarjanja testov: v našem primeru stranka sam piše teste, mi pa zagotovimo le njihov zagon (in prisotnost ustreznega poročila v zahtevi za združitev).


Kako bo videti splošno zaporedje dejanj?

  1. Gradnja aplikacije – opis te faze bomo izpustili.
  2. Razmestite aplikacijo v ločen imenski prostor gruče Kubernetes in začnite s testiranjem.
  3. Iskanje artefaktov in razčlenjevanje poročil JUnit z GitLabom.
  4. Brisanje predhodno ustvarjenega imenskega prostora.

Zdaj - k izvedbi!

prilagoditev

GitLab CI

Začnimo z fragmentom .gitlab-ci.yaml, ki opisuje uvajanje aplikacije in izvajanje testov. Seznam se je izkazal za precej obsežnega, zato je bil temeljito dopolnjen s komentarji:

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

Zdaj v imeniku .helm/templates ustvarimo YAML z Jobom - tests-job.yaml — za izvajanje testov in virov Kubernetes, ki jih potrebuje. Glej pojasnila po navedbi:

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

Kakšna sredstva opisano v tej konfiguraciji? Pri uvajanju ustvarimo edinstven imenski prostor za projekt (to je navedeno v .gitlab-ci.yaml - tests-${CI_COMMIT_REF_SLUG}) in ga razvaljamo:

  1. ConfigMap s testnim scenarijem;
  2. Job z opisom stroka in navedeno direktivo command, ki samo izvaja teste;
  3. PV in PVC, ki vam omogočajo shranjevanje testnih podatkov.

Bodite pozorni na uvodni pogoj z if na začetku manifesta - v skladu s tem morajo biti druge datoteke YAML grafikona Helm z aplikacijo zavite v vzvratno oblikovani tako, da se med testiranjem ne bodo razmestili. To je:

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

Vendar, če testi zahteva nekaj infrastrukture (na primer Redis, RabbitMQ, Mongo, PostgreSQL ...) - njihovi YAML-ji so lahko ne ugasni. Razmestite jih tudi v testno okolje ... in jih seveda prilagodite, kot se vam zdi primerno.

Zadnji dotik

Ker sestavljanje in uvajanje z uporabo werf za zdaj deluje Samo na gradbenem strežniku (z gitlab-runnerjem) in je pod s testi zagnan na glavnem, boste morali ustvariti imenik /mnt/tests na gospodarja in ga daj tekaču, na primer prek NFS. Podroben primer z razlago najdete v Dokumentacija K8s.

Rezultat bo:

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

Nihče ne prepoveduje, da bi delili NFS neposredno na gitlab-runner in ga nato montirali v pods.

Obvestilo

Morda se sprašujete, zakaj vse komplicirati z ustvarjanjem opravila, če lahko preprosto zaženete skript s testi neposredno na izvajalcu lupine? Odgovor je precej trivialen...

Nekateri testi zahtevajo dostop do infrastrukture (MongoDB, RabbitMQ, PostgreSQL itd.), da se preveri, ali delujejo pravilno. Testiranje poenotimo – s tem pristopom postane enostavno vključiti takšne dodatne entitete. Poleg tega dobimo Standard pristop uvajanja (tudi če uporabljate NFS, dodatna namestitev imenikov).

Rezultat

Kaj bomo videli, ko bomo uporabili pripravljeno konfiguracijo?

Zahteva za združitev bo prikazala povzetek statistike za teste, ki se izvajajo v zadnjem cevovodu:

JUnit v GitLab CI s Kubernetesom

Vsako napako lahko kliknete tukaj za podrobnosti:

JUnit v GitLab CI s Kubernetesom

NB: Pozoren bralec bo opazil, da testiramo aplikacijo NodeJS, na posnetkih pa - .NET ... Ne bodite presenečeni: med pripravo članka pri testiranju prve aplikacije ni bilo odkritih napak, ampak so našli v drugem.

Zaključek

Kot lahko vidite, nič zapletenega!

Načeloma, če že imate zbiralnik lupin in deluje, vendar ne potrebujete Kubernetesa, bo pripenjanje testiranja nanj še enostavnejša naloga, kot je opisano tukaj. In v Dokumentacija GitLab CI našli boste primere za Ruby, Go, Gradle, Maven in nekatere druge.

PS

Preberite tudi na našem blogu:

Vir: www.habr.com

Dodaj komentar