validate

package
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 10, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package validate содержит общие валидаторы полей API Kachō, общие для всех сервисов (Folder.Name, Network.Name, Subnet.Name и т. п.).

Все валидаторы возвращают gRPC ошибку `InvalidArgument` с `BadRequest.field_violations[]` через `pkg/errors.InvalidArgument()`.

Контракт валидации полей:

  • Name: единственная форма имени ресурса в дереве — DNS label по RFC 1123, `^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$` (строчные буквы, цифры, дефис; первый и последний символ — буква или цифра; 1..63). Пустая строка именем не является: на правке она отвергается `<field> is required`, на создании означает «назови сам» и заменяется NameOrDefault на имя, производное от id.
  • Description: до 256 символов.
  • Labels: до 64 пар; ключ `^[a-z][-_./\\@a-z0-9]{0,62}$` (1..63 байта); значение 0..63 байта.
  • UpdateMask: каждое поле должно быть известно сервисом; неизвестное — `InvalidArgument`.

Index

Constants

View Source
const (
	// MaxNameLen — максимум для Name полей ресурсов.
	MaxNameLen = 63
	// MaxDescriptionLen — лимит описания.
	MaxDescriptionLen = 256
	// MaxLabels — максимальное число label-пар на ресурс.
	MaxLabels = 64
	// MaxLabelKeyLen — длина ключа label.
	MaxLabelKeyLen = 63
	// MaxLabelValueLen — длина значения label.
	MaxLabelValueLen = 63
	// MaxPageSize — верхняя граница для page_size в List RPC.
	MaxPageSize int64 = 1000
	// DefaultPageSize — значение по-умолчанию, когда клиент не задал page_size.
	DefaultPageSize int64 = 50
)
View Source
const EnvExtraResourceIDHyphenPrefixes = "KACHO_EXTRA_RESOURCE_ID_HYPHEN_PREFIXES"

EnvExtraResourceIDHyphenPrefixes — имя env-переменной с ДОПОЛНИТЕЛЬНЫМИ hyphen-form prefix'ами (comma-separated, напр. "foo,bar" — сам prefix БЕЗ дефиса). Параллель к EnvExtraResourceIDPrefixes, но для going-forward формы "<prefix>-<base32>" (B3). Назначение то же: новый домен на hyphen-канон маршрутизируется на authz-edge api-gateway БЕЗ релиза corelib — оператор задаёт prefix через config. Канонические platform-prefix'ы (см. ids.KnownHyphenPrefixes) остаются захардкожены; config-путь — только расширение вперёд.

View Source
const EnvExtraResourceIDPrefixes = "KACHO_EXTRA_RESOURCE_ID_PREFIXES"

EnvExtraResourceIDPrefixes — имя env-переменной с ДОПОЛНИТЕЛЬНЫМИ известными 3-символьными resource-id prefix'ами (comma-separated, напр. "xyz,qqq"). Читается один раз при инициализации пакета и мёржится в базовый набор.

Назначение — снять blast-radius «новый домен → InvalidArgument на authz-edge api-gateway, пока corelib не отредактирован и не перевыпущен»: оператор api-gateway задаёт префикс нового семейства через config/env, БЕЗ релиза corelib. Базовые платформенные prefix'ы остаются захардкожены (стабильны, покрыты регрессионным guard-тестом); config-путь — только расширение вперёд.

View Source
const NameForm = nameform.Form

NameForm — единственная форма имени ресурса в дереве (RFC 1123 DNS label).

Сама строка объявлена в `pkg/validate/nameform` — пакете БЕЗ транспорта, потому что ту же форму обязан читать слой домена, которому grpc запрещён (см. документацию того пакета). Здесь — только ссылка: копии литерала не заводится, поэтому расходиться нечему.

Variables

This section is empty.

Functions

func CanonFieldName

func CanonFieldName(name string) string

CanonFieldName приводит имя поля к форме контракта (`hostClasses` → `host_classes`), посегментно относительно точки.

func Description

func Description(field, value string) error

Description проверяет длину поля description (UTF-8).

func FieldNameEq

func FieldNameEq(a, b string) bool

FieldNameEq отвечает, называют ли две строки ОДНО поле контракта.

Форму имени в `update_mask` выбирает не сервис. Край разбирает тело запроса через protojson, и тот приводит `updateMask` к именам полей контракта: клиент прислал `countryCode`, сервис получил `country_code`. Прямой вызов gRPC несёт то, что положил вызывающий. Сравнение строк ДОСЛОВНО поэтому отвечает «нет» на верном входе — и делает это тихо: поле объявлено изменяемым, запрос принят, ответ успешен, значение не изменилось.

Класс не виден, пока все изменяемые поля односложны: у `status` и `labels` обе формы совпадают. Первое многословное поле вскрывает его на пути ЗАПИСИ, а не проверки — то есть на два шага дальше того места, где о форме имени вообще думают.

Обе стороны приводятся к форме имени поля контракта; точка разделяет вложенные поля и в приведении не участвует.

func Labels

func Labels(field string, labels map[string]string) error

Labels проверяет map labels: число пар, длину и regex ключа, длину значения.

func Name

func Name(field, value string) error

Name проверяет имя ресурса против единственной формы дерева (NameForm).

Пустая строка — НЕ имя. Она отвергается отдельным сообщением `<field> is required`, а не общим сообщением о форме: вызывающий, забывший поле, и вызывающий, приславший `My_Name`, ошиблись по-разному, и отказ обязан это различать.

На пути СОЗДАНИЯ пустая строка остаётся законным входом — там зовут не эту функцию, а пару NameOnCreate (форма) + NameOrDefault (что записать).

func NameOnCreate

func NameOnCreate(field, value string) error

NameOnCreate — форма имени на пути СОЗДАНИЯ: пустая строка законна, непустая обязана соответствовать NameForm.

Пустое здесь означает не «имя отсутствует», а «назови сам»: до записи оно заменяется умолчанием (NameOrDefault), и ресурс с пустым именем не возникает. Требовать имя на создании значило бы ломающее изменение у каждого создающего глагола ради величины косметической: адресуется ресурс по неизменяемому id (ban #15).

Отвечает на вопрос «допустим ли ввод», а НЕ «что будет записано» — второй вопрос задают в момент, когда id уже сгенерирован, и отвечает на него NameOrDefault. Два вопроса, два момента, две функции.

func NameOnUpdate

func NameOnUpdate(field string, mask []string, value string) (bool, error)

NameOnUpdate — ЕДИНСТВЕННОЕ решение «что делать с именем на правке».

Возвращает, следует ли записывать имя, и отказ, если присланное недопустимо. Пять исходов, и они выведены из того, что proto3 НЕ РАЗЛИЧАЕТ «поле не прислано» и «поле пусто»:

маска пуста, имя пусто          → (false, nil) — полная правка, имени не касались
маска пуста, имя непусто        → (true, форма)
маска не называет name          → (false, nil)
маска называет name, имя пусто  → (false, `<field> is required`)
маска называет name, непусто    → (true, форма)

Почему пустая маска НЕ отвергает пустое имя. Полная правка — законный способ изменить описание или метки, не трогая имя; отказ там сломал бы каждого, кто правит объект целиком. Почему она при этом не ЗАПИСЫВАЕТ пустое: записать значило бы вернуть безымянный ресурс той самой дверью, которую закрывает отказ строкой ниже, — и упереться в ограничение формы уже на стороне базы, отдав вызывающему внутреннюю ошибку вместо контрактного отказа.

Почему функция общая. Правило горизонтально — оно про форму запроса, а не про предмет сервиса, и рассыпанное по use-case'ам разойдётся ровно так же, как разошлись четыре регулярки. Первая его редакция и завелась копией в одном сервисе; здесь она сведена к одной (#715).

func NameOrDefault

func NameOrDefault(value, id string) string

NameOrDefault — имя, которое СЛЕДУЕТ ЗАПИСАТЬ: пустое заменяется умолчанием, производным от id.

Зовётся в use-case создания в точке, где id уже сгенерирован. Точка вызова у каждого сервиса своя, и это нормально; общей обязана быть функция — правило «пустое имя не доживает до записи», рассыпанное по сервисам как `if name == "" { name = … }`, разойдётся ровно так же, как разошлись четыре регулярки.

Гонки нет by construction: значение известно ДО вставки, вставка одна, конфликту взяться неоткуда. Форму value эта функция НЕ проверяет — её проверил NameOnCreate на входе.

func PageSize

func PageSize(field string, value int64) (int64, error)

PageSize проверяет границы page_size в List RPC.

Семантика контракта:

  • page_size == 0 → допустимо; клиент явно не задал, репозиторий применяет DefaultPageSize. Возвращает (DefaultPageSize, nil).
  • page_size < 0 или > MaxPageSize → InvalidArgument с FieldViolation; возвращает (0, err). Не silent fallback — это нарушение контракта.
  • 1..MaxPageSize → возвращает (value, nil).

Возвращаемое effective значение нужно использовать в LIMIT-выражении SQL. Каждый репозиторий-метод List должен вызывать PageSize первой строкой и пробрасывать err наружу через service.

func ResourceID

func ResourceID(resourceType, expectedPrefix, id string) error

ResourceID проверяет, что resource-id синтаксически валиден — начинается с известного prefix Kachō в ОДНОЙ из двух форм (B3, redesign-2026):

  • legacy слитная форма "<prefix><17-crockford-base32>" — первые 3 символа ∈ resourceIDPrefixes (`net…`, `epd…`, `acb…`);
  • going-forward hyphen-форма "<prefix>-<crockford-base32>" — сегмент ДО первого дефиса ∈ hyphenResourceIDPrefixes (`ins-…`, `ns-…`, `mt-…`).

Крокфорд-тело дефис не содержит, поэтому наличие дефиса — однозначный сигнал новой формы; классификация **строго аддитивна** — legacy-поведение не меняется (hyphen-приём только ДОБАВЛЯЕТ acceptance, а не отзывает). Сервисы мигрируют свой prefix по одному, поэтому router обязан принимать обе формы в переходный период. Пустой id — пропускается (required-проверка / transcoding-роутинг — отдельно).

Контракт: на malformed / нераспознанный resource-id мутирующие и read-RPC отдают sync `InvalidArgument` с flat-message `"invalid <resourceType> id '<id>'"` (НЕ `NotFound`). Семантика **family-agnostic**: prefix должен быть из известного набора, но НЕ обязан совпадать с типом ресурса (`enp`-id, переданный как subnet-id, проходит → дальше `repo.Get` → `NotFound`). Длину/алфавит тела внутри здесь не проверяем.

resourceType   — имя ресурса в нижнем регистре ("network", "subnet",
                 "security group", "folder", "gateway", "private endpoint", ...).
expectedPrefix — prefix семейства этого ресурса (ids.PrefixNetwork и т.п.).
                 По контракту проверка family-agnostic (см. выше), поэтому
                 значение осознанно НЕ сверяется с id — параметр документирует
                 в call-site'е, какое семейство ожидается, не навязывая strict-
                 сверку. Это не «зарезервировано на будущее»: family-agnostic —
                 конечный контракт (enp-id как subnet-id должен доходить до
                 repo.Get → NotFound, а не отбиваться InvalidArgument здесь).

Возвращаемая ошибка — готовый gRPC `status` с нужным flat-message (не field-violation builder — в этом случае контракт требует flat-message).

func UpdateMask

func UpdateMask(field string, mask []string, known map[string]struct{}) error

UpdateMask проверяет, что все field-ы в mask содержатся в known.

Используется в *.Update методах: каждый сервис указывает свой набор разрешенных для апдейта полей; все остальное — InvalidArgument.

func ZoneId

func ZoneId(field, value string) error

ZoneId — format/required-валидация: проверяет, что value не пустой.

Список валидных зон НЕ хардкодится. Existence-валидация (есть ли такая зона в БД) — ответственность сервиса, владеющего таблицей `zones` (kacho-vpc). Здесь только required-check — формируем единообразный FieldViolation для пустого zone_id.

Пустая строка → InvalidArgument c FieldViolation `<field> is required`. Непустое значение → nil (caller обязан выполнить existence-check).

Types

This section is empty.

Directories

Path Synopsis
Package nameform несёт ЕДИНСТВЕННУЮ форму имени ресурса Kachō и чистый предикат к ней — без транспорта.
Package nameform несёт ЕДИНСТВЕННУЮ форму имени ресурса Kachō и чистый предикат к ней — без транспорта.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL