Тестирование Helm Chart. Часть 1
Тестирование – не совсем корректное слово, речь пойдет о способах валидации этих самых чартов и входных параметров для них.
Так уж случилось, что мне не нравится helm, не нравится он как раз тем функционалом, которым хвастает – гибкая шаблонизация, параметризация.
И то, и другое легко в освоении, не требует глубоких знаний, но и приносит ряд проблем:
- шаблоны невозможно читать – в смеси из yaml и go template легко запутаться и ошибиться, а еще у нас есть хелперы, о которые нужно держать в голове; Проверку синтаксиса можно легко доверить helm lint, но результирующие спецификации все равно могут оказаться нерабочими;
- 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, но прежде – проверяем валидны ли итоговые файлы.
Итак, пишем небольшой скрипт, который соберет все проверки воедино:
- helm lint;
- helm template на основе базового values-файла, проверка результата по схемам;
- 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, но кейсы – это лишь подготовленные наборы, которые могут отличаться от задаваемых, поэтому в следующей части я опишу, как мы боремся с валидацией входных параметров чарта, которые отдаются на растерзанием пользователям-разработчикам.