JUnit في GitLab CI مع Kubernetes

على الرغم من حقيقة أن الجميع يعلم جيدًا أن اختبار برنامجك أمر مهم وضروري، وأن الكثيرين يقومون بذلك تلقائيًا لفترة طويلة، إلا أنه في اتساع نطاق حبر لم تكن هناك وصفة واحدة لإعداد مجموعة من هذه المنتجات الشائعة في هذا المكان المخصص هو GitLab وJUnit (المفضل لدينا). دعونا ملء هذه الفجوة!

JUnit في GitLab CI مع Kubernetes

استهلالي

أولاً، اسمحوا لي أن أقدم بعض السياق:

  • نظرًا لأن جميع تطبيقاتنا تعمل على Kubernetes، فسنفكر في إجراء اختبارات على البنية التحتية المناسبة.
  • للتجميع والنشر نستخدم werf (فيما يتعلق بمكونات البنية التحتية، فهذا يعني تلقائيًا أيضًا أن شركة Helm متورطة).
  • لن أخوض في تفاصيل الإنشاء الفعلي للاختبارات: في حالتنا، يكتب العميل الاختبارات بنفسه، ونحن نضمن فقط إطلاقها (ووجود تقرير مطابق في طلب الدمج).


كيف سيبدو التسلسل العام للإجراءات؟

  1. بناء التطبيق – سنغفل وصف هذه المرحلة.
  2. انشر التطبيق على مساحة اسم منفصلة لمجموعة Kubernetes وابدأ الاختبار.
  3. البحث عن القطع الأثرية وتحليل تقارير JUnit باستخدام GitLab.
  4. حذف مساحة الاسم التي تم إنشاؤها مسبقًا.

الآن - للتنفيذ!

تعديل

جيت لاب CI

لنبدأ بجزء .gitlab-ci.yaml، الذي يصف نشر التطبيق وإجراء الاختبارات. تبين أن القائمة ضخمة جدًا، لذلك تم استكمالها بالكامل بالتعليقات:

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

الآن في الدليل .helm/templates لنقم بإنشاء YAML باستخدام Job - tests-job.yaml — لتشغيل الاختبارات وموارد Kubernetes التي يحتاجها. انظر التوضيحات بعد الإدراج:

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

أي نوع من الموارد الموصوفة في هذا التكوين؟ عند النشر، نقوم بإنشاء مساحة اسم فريدة للمشروع (يشار إلى ذلك في .gitlab-ci.yaml - tests-${CI_COMMIT_REF_SLUG}) وطرحها:

  1. خريطة التكوين مع البرنامج النصي للاختبار.
  2. وظيفة مع وصف الكبسولة والتوجيه المحدد command، الذي يقوم فقط بإجراء الاختبارات؛
  3. الكهروضوئية والبلاستيكيةوالتي تسمح لك بتخزين بيانات الاختبار.

انتبه إلى الشرط التمهيدي مع if في بداية البيان - وفقًا لذلك، يجب تضمين ملفات YAML الأخرى الخاصة بمخطط Helm مع التطبيق يعكس التصميم بحيث لا يتم نشرها أثناء الاختبار. إنه:

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

ومع ذلك، إذا كانت الاختبارات تتطلب بعض البنية التحتية (على سبيل المثال، Redis وRabbitMQ وMongo وPostgreSQL...) - يمكن أن تكون YAMLs الخاصة بهم لا أطفأ. قم بنشرها في بيئة اختبار أيضًا... وقم بتعديلها كما تراه مناسبًا بالطبع.

اللمسة النهائية

لأن التجميع والنشر باستخدام werf يعمل في الوقت الحالي فقط على خادم البناء (باستخدام gitlab-runner)، ويتم تشغيل الكبسولة مع الاختبارات على الخادم الرئيسي، وسوف تحتاج إلى إنشاء دليل /mnt/tests على السيد واعطائها للعداء، على سبيل المثال، عبر NFS. يمكن العثور على مثال مفصل مع التوضيحات في وثائق K8.

ستكون النتيجة:

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

لا أحد يمنع مشاركة NFS مباشرة على gitlab-runner، ثم تركيبها في القرون.

لاحظ

ربما تتساءل عن سبب تعقيد كل شيء عن طريق إنشاء وظيفة إذا كان بإمكانك ببساطة تشغيل برنامج نصي مع اختبارات مباشرة على مشغل الصدفة؟ الجواب تافه جدا..

تتطلب بعض الاختبارات الوصول إلى البنية التحتية (MongoDB، RabbitMQ، PostgreSQL، وما إلى ذلك) للتحقق من أنها تعمل بشكل صحيح. نحن نجعل الاختبار موحدًا - وبهذا النهج، يصبح من السهل تضمين مثل هذه الكيانات الإضافية. بالإضافة إلى هذا نحصل على معيار نهج النشر (حتى في حالة استخدام NFS، تركيب إضافي للدلائل).

نتيجة

ماذا سنرى عندما نطبق التكوين المعد؟

سيعرض طلب الدمج إحصائيات ملخصة للاختبارات التي يتم إجراؤها في أحدث مسار له:

JUnit في GitLab CI مع Kubernetes

يمكن النقر على كل خطأ هنا للحصول على التفاصيل:

JUnit في GitLab CI مع Kubernetes

NB: سيلاحظ القارئ اليقظ أننا نختبر تطبيق NodeJS، وفي لقطات الشاشة - .NET... لا تتفاجأ: كجزء من إعداد المقالة فقط، لم يتم العثور على أي أخطاء في اختبار التطبيق الأول، ولكن تم العثور عليهم في مكان آخر.

اختتام

كما ترون، لا شيء معقد!

من حيث المبدأ، إذا كان لديك بالفعل أداة تجميع الصدفة وهي تعمل، لكنك لا تحتاج إلى Kubernetes، فإن إرفاق الاختبار بها سيكون مهمة أبسط مما هو موضح هنا. و في وثائق GitLab CI ستجد أمثلة لـ Ruby وGo وGradle وMaven وبعض الآخرين.

PS

اقرأ أيضًا على مدونتنا:

المصدر: www.habr.com

إضافة تعليق