March 22, 2022

Тестирование Helm Chart. Часть 1

Тестирование – не совсем корректное слово, речь пойдет о способах валидации этих самых чартов и входных параметров для них.

Так уж случилось, что мне не нравится helm, не нравится он как раз тем функционалом, которым хвастает – гибкая шаблонизация, параметризация.

И то, и другое легко в освоении, не требует глубоких знаний, но и приносит ряд проблем:

  1. шаблоны невозможно читать – в смеси из yaml и go template легко запутаться и ошибиться, а еще у нас есть хелперы, о которые нужно держать в голове; Проверку синтаксиса можно легко доверить helm lint, но результирующие спецификации все равно могут оказаться нерабочими;
  2. helm lint, или же helm template (в упрощенном применении), подойдет, чтобы удостовериться в корректности входных параметров, но лишь тех, которые идут "из коробки"; особенность спецификаций в том, что у них есть конкретный формат и некорректная типизация или неверное приведение к типу может привести к ошибке в боевом применении, но позволит пропустить при проверке на базовых входных параметрах чарта.

Все что нам нужно – убедиться, что при определенных входных параметрах мы получаем точно те спецификации, что ожидаем, а с другой стороны, что входные параметры именно в том формате, который нам нужен.

Проблема с валидацией входных параметров имеет место быть в том случае, когда это нам неподконтрольно, например, параметры описаны в values.yaml файле, который размещен в репозитории с кодом сервиса, куда вносят измения разработчики. А начну я с тестирования шаблонов.

Тестирование шаблонов

Имеем чарт, имеем определенные наборы входных данных в видел values-файлов, задача – проверить, что в итоге получаем то, что нужно и результирующие спецификации применимы.

Применять будем несколько инструментов:

  • helm – нам все же нужно получать результат шаблонизации;
  • kubeval – валидация спецификаций Kubernetes согласно имеющимся в базе схем;
  • conftest – утилита, с помощью которой будем запускать тесты на языке Rego, который входит в состав проекта Open Policy Agent.

Структура проекта (чарта), не отличается от общепринятой, но на всякий случай я её продублирую:

Структура чарта

В каталоге templates хранятся сами шаблоны, в values.yaml – базовые входные параметры, в Chart.yaml описание чарта – минимальный обязательный набор, чтобы считать каталог чартом. Здесь же – changelog, чтобы отмечать основные изменения, readme с базовым описанием и каталоге test, который рассмотрим подробно.

В каталоге test лежат готовые тест-кейсы – определенные наборы входных данных и описание критериев, по которым мы можем точно сказать корректен результат или нет:

Я рассматриваю самый обычный чарт пользовательского сервиса, в котором определенные входные параметры напрямую влияют на результат шаблонизации, сложность шаблонов не регулируется, отсюда возможны самые разнообразные кейсы начиная от простой проверки на существование значение, заканчивая полным безумием, например:

{{- if .Values.rules}}
{{- range $rule := .Values.rules }}
{{ if and $rule.alert }}
  {{ $rule_type := "q4-apps" }}
  {{ $rule_severity := "warning" }}
  {{ $rule_owner_team := (default "" $.Values.app.owner_team) }}
  {{ if $rule.labels }}
    {{ $rule_type = (default $rule_type $rule.labels.type) }}
    {{ $rule_severity = (default $rule_severity $rule.labels.severity ) }}
    {{ $rule_owner_team = (default $.Values.app.owner_team (default "" $rule.labels.owner_team)) }}
  {{ end }}
  {{ if $rule.labels }}
    {{ $_ := set $rule.labels "type" $rule_type }}
  {{ else }}
    {{ $_ := set $rule "labels" (dict "type" $rule_type) }}
  {{ end }}
  {{ $_ := set $rule.labels "severity" $rule_severity }}
  {{ if not (empty $rule_owner_team) }}
    {{ $_ := set $rule.labels "owner_team" $rule_owner_team }}
  {{ end }}
{{ end }}
{{- end }}

Да, здесь нет ничего от yaml, но хранится именно в этом файле и результат выполнения этого куска сложно так просто предугадать.

Вернемся к тест-кейсу, который я начал описывать, на входе имеем следующее содержимое values.yaml файла:

---
extraEnv:
  PORT: "4000"
  GRPC_PORT: 5000
  FOOBAR: true

Данный тест описывает применение параметра extraEnv, назначение которого – добавление переменных окружения в шаблоны подов для Deployment, в коде шаблона это выглядит так:

        {{- if .Values.extraEnv }}
        {{- range $k,$v := .Values.extraEnv }}
          - name: {{ $k }}
            value: {{ $v }}
        {{- end }}
        {{- end }}

В тесте мы проверяем только наличие параметра, так как его отсуствие покрывается наличием других тест-кейсов. Параметр может содержать самые разнообразные типы данных, однако в конечном итоге список env в шаблоне пода должен содержать значения полей name и values исключительно в формате строки.

Данный кейс примечателен тем, что здесь комбинируется сразу два инструмента – conftest и kubeval, с помощью первого мы можем написать проверку на наличие необходимой переменной окружения – наше требование, с помощью второго проверить, что результат согласуется со схемами – требование Kubernetes.

Проверка на наличие переменных может выглядеть следующим образом:

package main

contains_env(envs, name) = true {
	envs[_].name = name
} else = false { true }

deny[msg] {
	input.kind = "Deployment"
	envs := input.spec.template.spec.containers[0].env
	not contains_env(envs, "GRPC_PORT")
	msg = "Deployment не содержит переменной окружения GRPC_PORT"
}

deny[msg] {
	input.kind = "Deployment"
	envs := input.spec.template.spec.containers[0].env
	not contains_env(envs, "PORT")
	msg = "Deployment не содержит переменной окружения PORT"
}

deny[msg] {
	input.kind = "Deployment"
	envs := input.spec.template.spec.containers[0].env
	not contains_env(envs, "FOZBAR")
	msg = "Deployment не содержит переменной окружения FOZBAR"
}

Здесь мы проверяем наличие заданных нами переменных в итоговых сущностях Kubernetes типа Deployment, но прежде – проверяем валидны ли итоговые файлы.

Итак, пишем небольшой скрипт, который соберет все проверки воедино:

  1. helm lint;
  2. helm template на основе базового values-файла, проверка результата по схемам;
  3. helm template на основе values-файла из тест кейса, проверка результата по схемам, OPA-тесты.

Сам код:

#!/bin/bash

set -o errexit
set -o pipefail

helm lint

helm template app-template . | kubeval --ignore-missing-schemas --exit-on-error

for d in test/*; do
	echo "${d}"
	helm template -f "${d}/values.yaml" app-template . > result.yaml
	kubeval --ignore-missing-schemas --exit-on-error result.yaml
	conftest test -p "${d}/policy" result.yaml
done

Для автоматизации запуска тестов мы используем Gitlab CI, поэтому обновляем содержимое .gitlab-ci.yml файла, в моем случае это выглядит так:

---
stages:
  - test

lint:
  stage: test
  image: name:tag
  script:
    - ./helm-test.sh

В данном случае в образе уже собрано все необходимое, убедитесь, что ваш образ имеет все необходимые инструменты, которые я описал, их установку можно так же добавить в директиву before_script, например:

  before_script:
    - wget -qO - https://get.helm.sh/helm-v3.8.1-linux-amd64.tar.gz | tar -zxOf - linux-amd64/helm > /usr/bin/helm
    - chmod +x /usr/bin/helm
    - wget -qO - https://github.com/instrumenta/kubeval/releases/download/v0.16.1/kubeval-linux-amd64.tar.gz | tar -zxOf - kubeval > /usr/bin/kubeval
    - chmod +x /usr/bin/kubeval
    - wget -qO - https://github.com/open-policy-agent/conftest/releases/download/v0.30.0/conftest_0.30.0_Linux_x86_64.tar.gz | tar -zxOf - conftest > /usr/bin/conftest
    - chmod +x /usr/bin/conftest

Пушим все это дело в Gitlab, смотрим результат:

Как говорится – внимательный читатель обратил внимание на ошибку, но сначала посмотрим, что нам пишут в лог:

..1.value: Invalid type. Expected: [string,null], given: boolean
..2.value: Invalid type. Expected: [string,null], given: integer
..3.value: Invalid type. Expected: [string,null], given: integer

На нашем входном наборе данных мы получаем некорректные спецификации с точки зрения схем Kubernetes – значения в списке переменных окружения неверного типа, должны быть строки, а у нас чёрт знает что, исправляем шаблон:

        {{- if .Values.extraEnv }}
        {{- range $k,$v := .Values.extraEnv }}
          - name: {{ $k }}
            value: {{ $v | quote }}
        {{- end }}
        {{- end }}

Теперь все значения будут в двойных ковычках, тем самым приводятся к строке, пушим:

..contains a valid Deployment (runner.app-template)

Итоговый Deployment получается корректным, следом запускаются уже наши тесты через conftest, получаем такое сообщение:

..Deployment не содержит переменной окружения FOZBAR

И действительно, такой переменной в нашем файле values и не предполагалось, правим в тестах имя переменной на FOOBAR, которая точно должна присутствовать, пушим:

..0 warnings, 0 failures, 0 exceptions

Итого мы получили полностью то, что ожидаем – переменные на месте, наличие переменных разных типов не ломает спецификации Kubernetes, но кейсы – это лишь подготовленные наборы, которые могут отличаться от задаваемых, поэтому в следующей части я опишу, как мы боремся с валидацией входных параметров чарта, которые отдаются на растерзанием пользователям-разработчикам.

Полезные ссылки

  1. https://www.conftest.dev
  2. https://github.com/open-policy-agent/conftest/tree/master/examples
  3. https://www.openpolicyagent.org