JUnit no GitLab CI com Kubernetes

Embora todos saibam que testar seu software é importante e necessário, e muitos já fazem isso automaticamente, não encontramos nenhuma receita no Habra para configurar a combinação de produtos tão populares nesta área, como (nossa favorita) GitLab e JUnit. Vamos preencher essa lacuna!

JUnit no GitLab CI com Kubernetes

Introdução

Para começar, vou definir o contexto:

  • Como todos os nossos aplicativos funcionam no Kubernetes, será abordada a execução de testes na infraestrutura correspondente.
  • Para construção e implantação, utilizamos werf (no sentido de componentes de infraestrutura, isso também significa automaticamente que o Helm está envolvido).
  • Não vou entrar em detalhes sobre a criação direta dos testes: no nosso caso, o cliente escreve os testes por conta própria, e nós apenas garantimos sua execução (e a presença do relatório correspondente na solicitação de merge).


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

  1. Construção do aplicativo — descreveremos esta etapa, mas a omitiremos.
  2. Implantação do aplicativo em um namespace separado do cluster Kubernetes e início da execução dos testes.
  3. Busca de artefatos e parsing do relatório JUnit pelo GitLab.
  4. Remoção do namespace criado anteriormente.

Agora — vamos à implementação!

Configuração

GitLab CI

Começaremos com o fragmento .gitlab-ci.yaml, que descreve a implantação do aplicativo e a execução dos testes. A listagem acabou ficando bastante volumosa, portanto foi suficientemente complementada com comentários:

variáveis:
# declaramos a versão do werf que pretendemos usar
  WERF_VERSION: "1.0 beta"

.base_deploy: &base_deploy
  script:
# criamos o namespace no K8s, se não existir
    - kubectl --context="${WERF_KUBE_CONTEXT}" get ns ${CI_ENVIRONMENT_SLUG} || kubectl create ns ${CI_ENVIRONMENT_SLUG}
# carregamos o werf e implantamos — mais detalhes estão na documentação
# (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:-''}"
# passamos a variável `run_tests`
# ela será usada na renderização do Helm release
      --set "global.run_tests=${RUN_TESTS:-no}"
      --set "global.env=${CI_ENVIRONMENT_SLUG}"
# alteramos o timeout (podem haver testes longos) e o passamos na release
      --set "global.ci_timeout=${CI_TIMEOUT:-900}"
     --timeout ${CI_TIMEOUT:-900}
  dependencies:
    - Build

.test-base: &test-base
  extends: .base_deploy
  before_script:
# criamos um diretório para o futuro relatório, com base no $CI_COMMIT_REF_SLUG
    - mkdir /mnt/tests/${CI_COMMIT_REF_SLUG} || true
# solução alternativa forçada, pois o GitLab espera que os artefatos estejam em seu diretório build-dir
    - mkdir ./tests || true
    - ln -s /mnt/tests/${CI_COMMIT_REF_SLUG} ./tests/${CI_COMMIT_REF_SLUG}
  after_script:
# após a conclusão dos testes, removemos a release junto com o Job
# (e, possivelmente, sua infraestrutura)
    - 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 falhas, mas você pode optar por diferente
  allow_failure: true
  variables:
    RUN_TESTS: 'yes'
# definimos o contexto no werf
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#infrastructure)
    WERF_KUBE_CONTEXT: 'admin@stage-cluster'
  tags:
# usamos um runner com o tag `werf-runner`
    - werf-runner
  artifacts:
# é necessário coletar um artefato para que possa ser visto
# no pipeline e baixado — por exemplo, para uma análise mais profunda
    paths:
      - ./tests/${CI_COMMIT_REF_SLUG}/*
# artefatos com mais de uma semana serão removidos
    expire_in: 7 days
# importante: essas linhas são responsáveis pela análise do relatório pelo GitLab
    reports:
      junit: ./tests/${CI_COMMIT_REF_SLUG}/report.xml

# por simplificação, aqui estão mostradas apenas duas etapas
# na realidade você terá mais — principalmente devido à implantação
stages:
  - build
  - tests

build:
  stage: build
  script:
# construção — novamente, de acordo com a documentação do 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:
# "a essência" da nomeação do namespace
# (https://docs.gitlab.com/ce/ci/variables/predefined_variables.html)
    name: tests-${CI_COMMIT_REF_SLUG}
  stage: tests
  except:
    - schedules

Kubernetes

Agora na diretório .helm/templates vamos criar um YAML com Job — tests-job.yaml — para executar os testes e os recursos necessários do Kubernetes. Para explicações, veja após a 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 }}

Quais recursos estão descritos nesta configuração? Ao implantar, criamos um namespace único para o projeto (isso é indicado ainda em .gitlab-ci.yaml — tests-${CI_COMMIT_REF_SLUG}) e implantamos nele:

  1. ConfigMap com o script de teste;
  2. Trabalho com a descrição do pod e a diretiva especificada command, que inicia os testes;
  3. PV e PVC, que permitem armazenar os dados dos testes.

Note a condição introdutória com if no início do manifesto — portanto, outros arquivos YAML do Helm chart com o aplicativo devem ser envolvidos em uma construção oposta, para que não sejam implantados durante os testes. Ou seja:

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

No entanto, se os testes exigirem alguma infraestrutura (como Redis, RabbitMQ, Mongo, PostgreSQL...) — seus YAMLs podem ser não desligar. Expanda-os e utilize em um ambiente de teste… claro, ajustando conforme sua preferência.

Toque Final

Como a montagem e o deploy usando werf atualmente funcionam apenas no servidor de build (com gitlab-runner), e o pod com os testes é iniciado no mestre, será necessário criar um diretório /mnt/tests no mestre e disponibilizá-lo para o runner, por exemplo, via NFS. Um exemplo expandido com explicações pode ser encontrado na documentação do 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

Nada impede que você também crie um compartilhamento NFS diretamente no gitlab-runner, depois montando-o nos pods.

Nota

Pode ser que você se pergunte por que complicar tudo criando um Job, quando se pode simplesmente executar um script de testes diretamente no runner shell? A resposta é bastante trivial…

Alguns testes exigem acesso à infraestrutura (MongoDB, RabbitMQ, PostgreSQL, etc.) para verificar a integridade do funcionamento com eles. Estamos tornando os testes padronizados — com essa abordagem, incluir tais entidades adicionais torna-se fácil. Além disso, obtemos o nível padrão uma abordagem para o deploy (mesmo que utilizando NFS e montagens adicionais de diretórios).

Resultado

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

No merge request, será mostrada uma estatística resumida dos testes executados em seu último pipeline:

JUnit no GitLab CI com Kubernetes

Para cada erro, é possível clicar e obter detalhes:

JUnit no GitLab CI com Kubernetes

NB: O leitor atento notará que estamos testando um aplicativo NodeJS, mas nas capturas de tela aparece .NET… Não se surpreenda: durante a preparação do artigo, não foram encontrados erros na testagem do primeiro aplicativo, mas erros foram encontrados no segundo.

Conclusão

Como pode ser visto, nada complicado!

Na verdade, se você já tem um builder shell que está funcionando, e não precisa do Kubernetes — integrar testes a ele será uma tarefa ainda mais simples do que a descrita aqui. E na documentação do GitLab CI você encontrará exemplos para Ruby, Go, Gradle, Maven e alguns outros.

P.S.

Confira também em nosso blog:

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster