Тестирование 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, но кейсы – это лишь подготовленные наборы, которые могут отличаться от задаваемых, поэтому в следующей части я опишу, как мы боремся с валидацией входных параметров чарта, которые отдаются на растерзанием пользователям-разработчикам.