Тестирование Helm Chart. Часть 2
В первой части мы проверяли чарты на соответствие нашим ожиданиям по входящим параметрам, хранящимся в values-файлах. В этой части я расскажу, как мы валидируем пользовательские values-файлы к этом чарту, которые правят разработчики, так как проблема остается той же – недопустимый набор входных параметров порождает неприменимые спецификации Kubernetes, что приводит к ошибкам публикации.
Валидация входных параметров
По аналогии с kubeval хотелось бы иметь какой-то инструмент, который способен валидировать файлы на соответствие нашей собственной схеме, сам kubeval использует схемы в JSON:
{ "description": "Binding ties one object to another; for example, a pod is bound to a node by a scheduler. Deprecated in 1.7, please use the bindings subresource of pods instead.", "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", "type": [ "string", "null" ] }, "kind": { "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", "type": [ "string", "null" ], "enum": [ "Binding" ] }, "metadata": { "$ref": "https://kubernetesjsonschema.dev/master/_definitions.json#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta", "description": "Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata" }, "target": { "$ref": "https://kubernetesjsonschema.dev/master/_definitions.json#/definitions/io.k8s.api.core.v1.ObjectReference", "description": "The target object that you want to bind to the standard object." } }, "required": [ "target" ], "type": "object", "x-kubernetes-group-version-kind": [ { "group": "", "kind": "Binding", "version": "v1" } ], "$schema": "http://json-schema.org/schema#" }
Не самый удобный формат, другой нюанс – kubeval ориентируется по полям kind и apiVersion для получения схем:
// We haven't cached this schema yet; look for one that works primarySchemaBaseURL := determineSchemaBaseURL(config) primarySchemaRef := determineSchemaURL(primarySchemaBaseURL, resource.Kind, resource.APIVersion, config) schemaRefs := []string{primarySchemaRef} for _, additionalSchemaURLs := range config.AdditionalSchemaLocations { additionalSchemaRef := determineSchemaURL(additionalSchemaURLs, resource.Kind, resource.APIVersion, config) schemaRefs = append(schemaRefs, additionalSchemaRef) }
Решаемо. Так может не будем придумывать велосипедов, а попробуем воспользоваться готовым инструментом?
Пробуем запустить валидацию values-файла из репозитория с кодом сервиса как есть:
% kubeval values.yaml ERR - values.yaml: Missing 'kind' key
Добавляем kind и apiVersion, и так знаем, что они нужны:
--- apiVersion: qlean/v1 kind: values # Базовые настройки чарта app-template: app: name: web-widget-navbar component: frontend project: platform
% kubeval values.yaml ..https://kubernetesjsonschema.dev/master-standalone/values-qlean-v1.json: Could not read schema from HTTP, response status is 404 Not Found
Для переопределения адреса источника схем в kubeval есть параметр --schema-location (либо переменная окружения KUBEVAL_SCHEMA_LOCATION), в качестве значения можно задавать как ссылку, так и локальный каталог:
export KUBEVAL_SCHEMA_LOCATION=file://./schemas
% kubeval values.yaml ..open ./schemas/master-standalone/values-qlean-v1.json: no such file or directory
В качестве дочернего каталога выступает значение флага --kubernetes-version (по умолчанию master) и префикс standalone. Создаем в нужном каталоге файл values-qlean-v1.json со следующим тестовым содержимым:
{ "title": "Helm Values", "additionalProperties": false }
Здесь мы определяем additionalProperties, который говорит, что на текущем уровне (в корне) запрещены любые неописанные поля, проверяем:
% kubeval values.yaml ..kind: Additional property kind is not allowed ..apiVersion: Additional property apiVersion is not allowed ..app-template: Additional property app-template is not allowed
{ "title": "Helm Values", "additionalProperties": false, "properties": { "apiVersion": { "type": "string" }, "kind": { "type": "string" }, "app-template": { "type": "object" } } }
% kubeval values.yaml PASS - values.yaml contains a valid values (unknown)
Ну, а далее идем изучать https://json-schema.org и, если хочется, схемы ресурсов Kubernetes, и формируем свою собственную схему, на основе которой будут проверяться пользовательские настройки чарта. Ниже несколько примеров из реальной жизни.
1. Поле `app` должно содержать ключ-значение, есть обязательные ключи, значения должны быть строкой:
"app": { "type": "object", "additionalProperties": { "type": "string" }, "required": [ "name", "component", "project" ] }
2. Поле `image` должно содержать два параметра – repository и tag, и больше никаких других:
"image": { "type": "object", "additionalProperties": false, "properties": { "repository": { "type": "string" }, "tag": { "type": "string" } } }
3. Поле `annotations` должно содержать ключ-значение, но есть ключи, которые использовать нельзя:
"annotations": { "type": "object", "properties": { "pltf_ttl": false, "ci_project_id": false }, "additionalProperties": { "type": "string" } }
4. Значение поля `type` должно быть одним из допустимых:
"type": { "type": "string", "enum": [ "http", "app" ] }
5. Значение `сount` должно быть целым числом и не меньше нуля:
"сount": { "type": "integer", "minimum": 0 }
6. Поле `for` должно содержать строку в формате duration:
"for": { "type": "string", "format": "duration" }
JSON-формат не так удобен для чтения, по сравнению с HCL или YAML, поэтому можно задуматься о том, чтобы конвертировать из подходящего формата в JSON при сборке образа.
Чем детальнее будет описана схема, тем меньше возможностей сделать что-то не так, а само внедрение валидации позволит с меньшими трудозатратами мигрировать на новый формат в дальнейшем.
Со звездочкой
Если стойкое желание сделать свой велосипед к этому моменту сохранилось, полезным будет посмотреть в сторону имплементации JSON Schema на Go, пакет позволяет, в числе прочего, описывать собственные форматы данных, например, для крона может выглядеть так:
package main import "github.com/gorhill/cronexpr" type CronFormat struct{} func init() { gojsonschema.FormatCheckers.Add("cron", CronFormat{}) } func (c CronFormat) IsFormat(cron interface{}) bool { cronString, ok := cron.(string) if !ok { return false } _, err := cronexpr.Parse(cronString) return err == nil }
С последующим применением в таком виде:
"cron": { "type": "string", "format": "cron" }
Ещё один плюс – чтение схем с диска можно описать самому и использовать любой формат, вот, например, YAML:
var bodySchema map[string]interface{} _ = yaml.Unmarshal(schemaBytes, &bodySchema) schemaLoader := gojsonschema.NewGoLoader(bodySchema) return gojsonschema.NewSchema(schemaLoader)
Детали реализации можно посмотреть в коде, он открыт.