JUnit en GitLab CI con Kubernetes

A pesar de que todos saben que es importante y necesario probar su software, y que muchos lo hacen automáticamente, no se ha encontrado ninguna receta en los rincones de Habra sobre cómo configurar la combinación de productos tan populares en este nicho como (nuestro favorito) GitLab y JUnit. ¡Vamos a llenar este vacío!

JUnit en GitLab CI con Kubernetes

Introducción

Para empezar, definiré el contexto:

  • Dado que todas nuestras aplicaciones funcionan en Kubernetes, se considerará la ejecución de pruebas en la infraestructura correspondiente.
  • Para la construcción y despliegue utilizamos werf (en el sentido de componentes de infraestructura, esto también significa automáticamente que se utiliza Helm).
  • No entraré en detalles sobre cómo crear las pruebas: en nuestro caso, el cliente escribe las pruebas por sí mismo, y nosotros solo aseguramos su ejecución (y la presencia del informe correspondiente en la solicitud de fusión).


¿Cómo se verá la secuencia general de acciones?

  1. Construcción de la aplicación: omitiremos la descripción de esta etapa.
  2. Despliegue de la aplicación en un namespace separado del clúster de Kubernetes y ejecución de pruebas.
  3. Búsqueda de artefactos y análisis del informe JUnit por parte de GitLab.
  4. Eliminación del namespace creado previamente.

¡Ahora, a la implementación!

Configuración

GitLab CI

Comencemos con el fragmento .gitlab-ci.yaml, que describe el despliegue de la aplicación y la ejecución de pruebas. La lista resultó ser bastante extensa, por lo que se ha complementado considerablemente con comentarios:

variables:
# declaramos la versión de werf que vamos a utilizar
  WERF_VERSION: "1.0 beta"

.base_deploy: &base_deploy
  script:
# creamos el namespace en K8s, si no existe
    - kubectl --context="${WERF_KUBE_CONTEXT}" get ns ${CI_ENVIRONMENT_SLUG} || kubectl create ns ${CI_ENVIRONMENT_SLUG}
# descargamos werf y desplegamos — más detalles en la documentación
# (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:-''}"
# pasamos la variable `run_tests`
# se utilizará en el renderizado del lanzamiento Helm
      --set "global.run_tests=${RUN_TESTS:-no}"
      --set "global.env=${CI_ENVIRONMENT_SLUG}"
# cambiamos el timeout (puede haber pruebas largas) y lo pasamos al lanzamiento
      --set "global.ci_timeout=${CI_TIMEOUT:-900}"
     --timeout ${CI_TIMEOUT:-900}
  dependencies:
    - Build

.test-base: &test-base
  extends: .base_deploy
  before_script:
# creamos un directorio para el futuro informe, basado en $CI_COMMIT_REF_SLUG
    - mkdir /mnt/tests/${CI_COMMIT_REF_SLUG} || true
# un workaround necesario, ya que GitLab quiere recibir los artefactos en su directorio de build
    - mkdir ./tests || true
    - ln -s /mnt/tests/${CI_COMMIT_REF_SLUG} ./tests/${CI_COMMIT_REF_SLUG}
  after_script:
# al finalizar las pruebas, eliminamos el lanzamiento junto con el Job
# (y, posiblemente, su infraestructura)
    - 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
# permitimos fallos, pero puedes hacer lo contrario
  allow_failure: true
  variables:
    RUN_TESTS: 'yes'
# configuramos el contexto en werf
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#infrastructure)
    WERF_KUBE_CONTEXT: 'admin@stage-cluster'
  tags:
# utilizamos un runner con la etiqueta `werf-runner`
    - werf-runner
  artifacts:
# se requiere recoger un artefacto para que se pueda ver
# en el pipeline y descargarlo — por ejemplo, para un estudio más detenidos
    paths:
      - ./tests/${CI_COMMIT_REF_SLUG}/*
# los artefactos de más de una semana serán eliminados
    expire_in: 7 day
# importante: estas líneas son responsables de la comprensión del informe por GitLab
    reports:
      junit: ./tests/${CI_COMMIT_REF_SLUG}/report.xml

# para simplificar aquí se muestran solo dos etapas
# en realidad, tendrás más — al menos debido al despliegue
stages:
  - build
  - tests

build:
  stage: build
  script:
# construcción — de vuelta a la documentación de 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:
# "el alma" de la denominación del 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 crearemos un YAML con un Job — tests-job.yaml — para ejecutar pruebas y los recursos necesarios de Kubernetes. Para más explicaciones, véase 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é recursos están descritos en esta configuración? Al desplegar, creamos un namespace único para el proyecto (esto está indicado aún en .gitlab-ci.yaml — tests-${CI_COMMIT_REF_SLUG}) y lo desplegamos en:

  1. ConfigMap con el script de la prueba;
  2. Job con la descripción del pod y la directiva indicada command, que es la que ejecuta las pruebas;
  3. PV y PVC, que permiten almacenar los datos de las pruebas.

Tenga en cuenta la condición inicial con if al principio del manifiesto — por lo tanto, otros archivos YAML del Helm chart con la aplicación deben envolverse en la inversa estructura, para que no se despleguen durante las pruebas. Es decir:

{{- if ne .Values.global.run_tests "yes" }}
---
yo otro yaml
{{- end }}

Sin embargo, si las pruebas requieren cierta infraestructura (por ejemplo, Redis, RabbitMQ, Mongo, PostgreSQL…) — sus YAML se pueden no desactivar. Expándalos también en un entorno de prueba… claro, ajustándolos a tu criterio.

Toque final

Como el ensamblaje y el despliegue mediante werf aún funcionan solo en el servidor de construcción (con gitlab-runner), y el pod con las pruebas se inicia en el maestro, será necesario crear un directorio /mnt/tests en el maestro y cederlo al runner, por ejemplo, a través de NFS. Un ejemplo desplegado con explicaciones se puede encontrar en la documentación de K8s.

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 impide crear un recurso compartido NFS directamente en gitlab-runner, tras lo cual se puede montar en los pods.

Nota

Quizás te preguntes, ¿por qué complicar las cosas creando un Job, si se puede simplemente ejecutar un script con las pruebas directamente en el shell-runner? La respuesta es bastante trivial…

Algunas pruebas requieren acceder a la infraestructura (MongoDB, RabbitMQ, PostgreSQL, etc.) para verificar la correcta funcionalidad con ellas. Estamos haciendo que la prueba sea unificada; con este enfoque, incluir entidades adicionales se vuelve sencillo. Además, obtenemos un enfoque estándar en el despliegue (aunque sea utilizando NFS y montando directorios adicionales).

Resultado

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

En la solicitud de fusión aparecerá una estadística general de las pruebas realizadas en su última canalización:

JUnit en GitLab CI con Kubernetes

Aquí se puede hacer clic en cada error 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 te sorprendas: simplemente, en el transcurso de la preparación del artículo, no se encontraron errores en la prueba de la primera aplicación, pero sí en la otra.

Conclusión

Como se puede ver, ¡nada complicado!

En principio, si ya tienes un construidor shell y está funcionando, y no necesitas Kubernetes, añadir pruebas a él será una tarea aún más sencilla que la descrita aquí. Y en la documentación de GitLab CI encontrarás ejemplos para Ruby, Go, Gradle, Maven y algunos otros.

P.D.

También puedes leer en nuestro blog:

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster