Тестирование 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)Детали реализации можно посмотреть в коде, он открыт.