JUnit in GitLab CI met Kubernetes

Ondanks het feit dat iedereen heel goed weet dat het testen van je software belangrijk en noodzakelijk is, en velen dit al heel lang automatisch doen, was er in de uitgestrektheid van Habr geen enkel recept voor het opzetten van een combinatie van zulke populaire producten in deze niche als (onze favoriet) GitLab en JUnit. Laten we deze leemte opvullen!

JUnit in GitLab CI met Kubernetes

inleidend

Laat ik eerst wat context geven:

  • Omdat al onze applicaties op Kubernetes draaien, zullen we overwegen om tests uit te voeren op de juiste infrastructuur.
  • Voor montage en plaatsing gebruiken wij werf (qua infrastructuurcomponenten betekent dit ook automatisch dat Helm betrokken is).
  • Ik zal niet ingaan op de details van het daadwerkelijk maken van tests: in ons geval schrijft de klant de tests zelf en zorgen wij alleen voor de lancering ervan (en de aanwezigheid van een bijbehorend rapport in het samenvoegverzoek).


Hoe ziet de algemene volgorde van acties eruit?

  1. Het bouwen van de applicatie - we laten de beschrijving van deze fase achterwege.
  2. Implementeer de applicatie in een aparte naamruimte van het Kubernetes-cluster en begin met testen.
  3. Zoeken naar artefacten en parseren van JUnit-rapporten met GitLab.
  4. Een eerder gemaakte naamruimte verwijderen.

Nu - naar de implementatie!

afstelling

GitLab-CI

Laten we beginnen met een fragment .gitlab-ci.yaml, waarin wordt beschreven hoe u de applicatie implementeert en tests uitvoert. De lijst bleek behoorlijk omvangrijk en werd daarom grondig aangevuld met commentaar:

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

Nu in de map .helm/templates laten we YAML maken met Job - tests-job.yaml – om tests uit te voeren en de Kubernetes-bronnen die het nodig heeft. Zie uitleg na vermelding:

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

Wat voor middelen beschreven in deze configuratie? Bij de implementatie creëren we een unieke naamruimte voor het project (dit wordt aangegeven in .gitlab-ci.yaml - tests-${CI_COMMIT_REF_SLUG}) en rol het uit:

  1. Configuratiekaart met testscript;
  2. Jobomschrijving: met een beschrijving van de pod en de gespecificeerde richtlijn command, die alleen de tests uitvoert;
  3. PV en PVC, waarmee u testgegevens kunt opslaan.

Let op de inleidende voorwaarde met if aan het begin van het manifest - dienovereenkomstig moeten andere YAML-bestanden van het Helm-diagram met de toepassing worden ingepakt achteruit zo te ontwerpen dat ze tijdens het testen niet worden ingezet. Dat is:

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

Echter, als de tests enige infrastructuur nodig (bijvoorbeeld Redis, RabbitMQ, Mongo, PostgreSQL...) - hun YAML's kunnen dat zijn geen uitschakelen. Implementeer ze ook in een testomgeving... en pas ze uiteraard naar eigen inzicht aan.

Laatste greep

Omdat montage en plaatsing voorlopig via werfwerken alleen op de buildserver (met gitlab-runner), en de pod met tests wordt gelanceerd op de master, zul je een map moeten aanmaken /mnt/tests op de meester en geef het aan de loper, bijvoorbeeld via NFS. Een gedetailleerd voorbeeld met uitleg vindt u in K8s-documentatie.

Het resultaat zal zijn:

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

Niemand verbiedt om een ​​NFS-share rechtstreeks op gitlab-runner te maken en deze vervolgens in pods te mounten.

Noot

U vraagt ​​zich misschien af ​​waarom alles ingewikkelder zou worden door een taak te maken als u eenvoudigweg een script met tests rechtstreeks op de shell-runner kunt uitvoeren? Het antwoord is nogal triviaal...

Sommige tests vereisen toegang tot de infrastructuur (MongoDB, RabbitMQ, PostgreSQL, enz.) om te verifiëren dat ze correct werken. Wij maken testen uniform: met deze aanpak wordt het eenvoudig om dergelijke extra entiteiten op te nemen. Daarnaast krijgen we standaard implementatiebenadering (zelfs bij gebruik van NFS, extra aankoppeling van mappen).

Resultaat

Wat zullen we zien als we de voorbereide configuratie toepassen?

Het samenvoegverzoek toont samenvattende statistieken voor tests die in de nieuwste pijplijn zijn uitgevoerd:

JUnit in GitLab CI met Kubernetes

Elke fout kan hier worden geklikt voor meer informatie:

JUnit in GitLab CI met Kubernetes

NB: De oplettende lezer zal merken dat we een NodeJS-applicatie testen, en in de screenshots - .NET... Wees niet verrast: het is alleen dat er tijdens het voorbereiden van het artikel geen fouten zijn gevonden bij het testen van de eerste applicatie, maar ze werden gevonden in een andere.

Conclusie

Zoals je kunt zien, niets ingewikkelds!

Als je al een shell-collector hebt en deze werkt, maar je hebt geen Kubernetes nodig, zal het koppelen van testen daaraan in principe een nog eenvoudiger taak zijn dan hier beschreven. En in GitLab CI-documentatie je vindt voorbeelden voor Ruby, Go, Gradle, Maven en enkele anderen.

PS

Lees ook op onze blog:

Bron: www.habr.com

Voeg een reactie