JUnit no GitLab CI com Kubernetes

Apesar de todos saberem perfeitamente que testar seu software é importante e necessário, e muitos já o fazem de forma automática há muito tempo, na vastidão de Habr não havia uma única receita para configurar uma combinação de produtos tão populares em esse nicho como (nosso favorito) GitLab e JUnit. Vamos preencher essa lacuna!

JUnit no GitLab CI com Kubernetes

Introdutório

Primeiro, deixe-me dar um contexto:

  • Como todos os nossos aplicativos são executados no Kubernetes, consideraremos a execução de testes na infraestrutura apropriada.
  • Para montagem e implantação usamos bem (em termos de componentes de infraestrutura, isso também significa automaticamente que o Helm está envolvido).
  • Não entrarei em detalhes da própria criação dos testes: no nosso caso, o próprio cliente escreve os testes, e nós apenas garantimos o seu lançamento (e a presença do relatório correspondente na solicitação de mesclagem).


Como será a sequência geral de ações?

  1. Construindo a aplicação – omitiremos a descrição desta etapa.
  2. Implante o aplicativo em um namespace separado do cluster Kubernetes e comece a testar.
  3. Procurando artefatos e analisando relatórios JUnit com GitLab.
  4. Excluindo um namespace criado anteriormente.

Agora - para implementação!

Fixação

CI do GitLab

Vamos começar com um fragmento .gitlab-ci.yaml, que descreve a implantação do aplicativo e a execução de testes. A listagem revelou-se bastante volumosa, por isso foi totalmente complementada com comentários:

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

Agora no diretório .helm/templates vamos criar YAML com Job - tests-job.yaml — para executar testes e os recursos Kubernetes necessários. Veja explicações após listagem:

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

Que tipo de recursos descrito nesta configuração? Ao implantar, criamos um namespace exclusivo para o projeto (isso é indicado em .gitlab-ci.yaml - tests-${CI_COMMIT_REF_SLUG}) e implemente-o:

  1. Mapa de configuração com roteiro de teste;
  2. Trabalho com uma descrição do pod e a diretiva especificada command, que apenas executa os testes;
  3. Fotovoltaica e PVC, que permitem armazenar dados de teste.

Preste atenção à condição introdutória com if no início do manifesto - respectivamente, outros arquivos YAML do gráfico Helm com o aplicativo devem ser agrupados em reverter design para que eles não sejam implantados durante o teste. Aquilo é:

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

Contudo, se os testes requer alguma infraestrutura (por exemplo, Redis, RabbitMQ, Mongo, PostgreSQL...) - seus YAMLs podem ser não desligar. Implante-os também em um ambiente de teste... ajustando-os como achar melhor, é claro.

Toque final

Porque montagem e implantação usando werf funcionam por enquanto apenas no servidor de compilação (com gitlab-runner), e o pod com testes for iniciado no master, você precisará criar um diretório /mnt/tests no mestre e entregue ao corredor, por exemplo, via NFS. Um exemplo detalhado com explicações pode ser encontrado em Documentação K8s.

O 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

Ninguém proíbe fazer um compartilhamento NFS diretamente no gitlab-runner e depois montá-lo em pods.

Nota

Você pode estar se perguntando por que complicar tudo criando um Job se você pode simplesmente executar um script com testes diretamente no shell runner? A resposta é bastante trivial...

Alguns testes requerem acesso à infraestrutura (MongoDB, RabbitMQ, PostgreSQL, etc.) para verificar se funcionam corretamente. Tornamos os testes unificados - com essa abordagem, fica fácil incluir essas entidades adicionais. Além disso, obtemos padrão abordagem de implantação (mesmo usando NFS, montagem adicional de diretórios).

resultado

O que veremos quando aplicarmos a configuração preparada?

A solicitação de mesclagem mostrará estatísticas resumidas dos testes executados em seu pipeline mais recente:

JUnit no GitLab CI com Kubernetes

Cada erro pode ser clicado aqui para obter detalhes:

JUnit no GitLab CI com Kubernetes

NB: O leitor atento notará que estamos testando um aplicativo NodeJS, e nas capturas de tela - .NET... Não se surpreenda: é que durante a preparação do artigo não foram encontrados erros no teste do primeiro aplicativo, mas eles foram encontrados em outro.

Conclusão

Como você pode ver, nada complicado!

Em princípio, se você já possui um coletor de shell e ele funciona, mas não precisa do Kubernetes, anexar testes a ele será uma tarefa ainda mais simples do que a descrita aqui. E em Documentação de CI do GitLab você encontrará exemplos para Ruby, Go, Gradle, Maven e alguns outros.

PS

Leia também em nosso blog:

Fonte: habr.com

Adicionar um comentário