JUnit en GitLab CI con Kubernetes

A pesar de que todo el mundo sabe perfectamente que probar su software es importante y necesario, y muchos lo han estado haciendo automáticamente durante mucho tiempo, en la inmensidad de Habr no existía una receta única para configurar una combinación de productos tan populares en este nicho como (nuestro favorito) GitLab y JUnit. ¡Llenemos este vacío!

JUnit en GitLab CI con Kubernetes

Introductorio

Primero, déjame darte un poco de contexto:

  • Dado que todas nuestras aplicaciones se ejecutan en Kubernetes, consideraremos realizar pruebas en la infraestructura adecuada.
  • Para el montaje y despliegue utilizamos patio (En términos de componentes de infraestructura, esto también significa automáticamente que Helm está involucrado).
  • No entraré en detalles sobre la creación real de pruebas: en nuestro caso, el cliente escribe las pruebas él mismo y nosotros solo nos aseguramos de su lanzamiento (y la presencia del informe correspondiente en la solicitud de fusión).


¿Cómo será la secuencia general de acciones?

  1. Creación de la aplicación: omitiremos la descripción de esta etapa.
  2. Implemente la aplicación en un espacio de nombres separado del clúster de Kubernetes y comience a probar.
  3. Búsqueda de artefactos y análisis de informes JUnit con GitLab.
  4. Eliminar un espacio de nombres creado previamente.

Ahora, ¡a la implementación!

Ajuste

CI de GitLab

Empecemos con un fragmento. .gitlab-ci.yaml, que describe la implementación de la aplicación y la ejecución de pruebas. La lista resultó ser bastante voluminosa, por lo que se complementó minuciosamente con comentarios:

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

Ahora en el directorio .helm/templates creemos YAML con Job - tests-job.yaml – para ejecutar pruebas y los recursos de Kubernetes que necesita. Ver explicaciones después del listado:

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

¿Qué tipo de recursos descrito en esta configuración? Al implementar, creamos un espacio de nombres único para el proyecto (esto se indica en .gitlab-ci.yaml - tests-${CI_COMMIT_REF_SLUG}) y extiéndalo:

  1. Mapa de configuración con guión de prueba;
  2. Trabajos con una descripción del pod y la directiva especificada command, que simplemente ejecuta las pruebas;
  3. Fotovoltaico y PVC, que le permiten almacenar datos de prueba.

Preste atención a la condición introductoria con if al comienzo del manifiesto; en consecuencia, otros archivos YAML del gráfico Helm con la aplicación deben estar empaquetados en inversa diseñar para que no se implementen durante las pruebas. Eso es:

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

Sin embargo, si las pruebas Requiere alguna infraestructura (por ejemplo, Redis, RabbitMQ, Mongo, PostgreSQL...): sus YAML pueden ser no apagar. Implementarlos también en un entorno de prueba... ajustándolos como mejor le parezca, por supuesto.

Toque final

Porque montaje e implementación utilizando werf works por ahora sólo en el servidor de compilación (con gitlab-runner) y el pod con pruebas se inicia en el maestro, deberá crear un directorio /mnt/tests en el maestro y dárselo al corredor, por ejemplo, a través de NFS. Un ejemplo detallado con explicaciones se puede encontrar en documentación de K8.

El resultado será:

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

Nadie prohíbe compartir NFS directamente en gitlab-runner y luego montarlo en pods.

Nota

Quizás se pregunte por qué complicar todo creando un trabajo si simplemente puede ejecutar un script con pruebas directamente en el shell runner. La respuesta es bastante trivial...

Algunas pruebas requieren acceso a la infraestructura (MongoDB, RabbitMQ, PostgreSQL, etc.) para verificar que funcionan correctamente. Hacemos que las pruebas sean unificadas; con este enfoque, resulta fácil incluir dichas entidades adicionales. Además de esto, obtenemos estándar enfoque de implementación (incluso si se utiliza NFS, montaje adicional de directorios).

resultado

¿Qué veremos cuando apliquemos la configuración preparada?

La solicitud de fusión mostrará estadísticas resumidas de las pruebas ejecutadas en su última canalización:

JUnit en GitLab CI con Kubernetes

Se puede hacer clic en cada error aquí para obtener más detalles:

JUnit en GitLab CI con Kubernetes

NB: El lector atento notará que estamos probando una aplicación NodeJS, y en las capturas de pantalla - .NET... No se sorprenda: es solo que mientras preparamos el artículo, no se encontraron errores al probar la primera aplicación, pero fueron encontrados en otro.

Conclusión

Como puedes ver, ¡nada complicado!

En principio, si ya tiene un recopilador de shell y funciona, pero no necesita Kubernetes, adjuntarle pruebas será una tarea aún más sencilla que la que se describe aquí. Y en Documentación de GitLab CI encontrará ejemplos para Ruby, Go, Gradle, Maven y algunos otros.

PS

Lea también en nuestro blog:

Fuente: habr.com

Añadir un comentario