Documentation
¶
Overview ¶
Package ownerregister — ОДНА форма синхронной регистрации ресурса у владельца прав, общая для всех сервисов-потребителей.
Зачем один пакет, а не пять регистраторов ¶
Форм было пять, и различались они пятью осями сразу: происхождением маркера версии, поведением при отказе на одном из tuple'ов, пробросом личности вызывающего, набором форвардимых полей зеркала и трактовкой несконфигурированного клиента. Ни одно из различий не было записано решением — только комментариями, и ДВА из этих комментариев заявляли паритет с соседом, которого не было:
- заголовок регистратора nlb объявлял себя зеркалом vpc, а его же тело двадцатью строками ниже разбирало, почему это ложь (версия у vpc из writer-транзакции, у nlb — с часов доставки);
- тот же разбор ссылался на функцию, шагающую версию по tuple'ам, «в NewRegisterApplier» nlb — такой функции у nlb нет вовсе, она живёт у registry (предикат: `git grep -n stepSourceVersion` даёт один файл, и это не nlb).
Ложное заявление о паритете и есть то, из-за чего расхождение прожило незамеченным: читатель, сверяющий соседа, видел ссылку и не шёл проверять.
Правило выбора семантики: САМАЯ УЗКАЯ, а не самая снисходительная ¶
Общий код, принявший то, что строгий сервис отвергал, вернул бы отвергнутый дефект во ВСЕ сервисы разом. Поэтому по каждой оси взято самое строгое из существовавшего, и рядом названо, чей это был выбор и что он отвергает:
- МАРКЕР ВЕРСИИ — из writer-транзакции, явный (форма vpc). Обязателен: Registration с нулевым SourceVersion не отправляется вовсе (см. ErrUnversioned). Отвергает часы момента доставки (storage/compute/nlb) и полное отсутствие версии (registry).
- НЕСКОНФИГУРИРОВАННЫЙ КЛИЕНТ — отказ (форма registry): New возвращает ErrNoClient. Отвергает молчаливый no-op (nlb), при котором ускоритель выключен и это ничем не наблюдается.
- ЛИЧНОСТЬ ВЫЗЫВАЮЩЕГО — пробрасывается всегда (форма compute/nlb), чтобы least-priv-гейт принимающей стороны судил реального вызывающего, а не только предъявленный сертификат. Отвергает молчаливую потерю личности (vpc/storage/registry).
- ОТКАЗ НА ОДНОМ tuple'е — не прекращает набор (форма nlb): пробуются ВСЕ, ошибки объединяются. Отвергает короткое замыкание (vpc/storage/registry), при котором указатель принадлежности не доезжает из-за соседа по набору, а вызывающий видит один отказ вместо всех.
- ПОЛЯ ЗЕРКАЛА — форвардится всё, что у доставки есть, включая TraceID и ParentAccountID (форма nlb — самая полная). Пустое поле принимающая сторона обрабатывает штатно; недосланное — молча обедняет зеркало.
Чего здесь НЕТ и почему ¶
Ветки «AlreadyExists — это успех» нет. Контракт RegisterResource идемпотентен и этого кода НЕ производит (см. godoc RegisterResourceUseCase и handler'а internal_iam: «neither AlreadyExists nor NotFound ever surfaces»). Ветка с отсутствующим производителем — это форма без содержания: она молчит на том, ради чего написана, и однажды проглотит настоящий отказ.
Признак первой доставки сюда НЕ добавляется ¶
Он уже landed ПОЛЕМ ДЕСКРИПТОРА сервиса (corelib/servicecontract), а не параметром запроса. Слот под него в Registration был бы вторым местом об одном предмете; приёмка XC-7 §10 п.3 это прямо отвергает. Принимающая сторона принимает признак лишь В КОНЪЮНКЦИИ со своим вердиктом по данным — правило «это не слово вызывающего» сохраняется дословно и здесь не ослабляется.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNoClient — регистратор собран без клиента к владельцу прав. // // Это ОТКАЗ, а не пустая операция. Пустая операция выглядит как исправно // работающий ускоритель, который никогда ничего не ускорял: ни одного // отказа за всю жизнь, ни одной строки в логе, и отличить её от «всё // хорошо» нечем (security.md §Hardening-инвариант 8). ErrNoClient = errors.New("owner-register: клиент владельца прав не сконфигурирован") // ErrUnversioned — регистрация не несёт маркера версии из writer-транзакции. // // РАЗМЕН НАЗВАН ЯВНО. Отказ означает, что эта регистрация не будет доставлена // синхронно, и окно видимости гранта расширяется до дренажа durable-очереди — // корректность при этом сохраняется полностью (доставка at-least-once уже // лежит в очереди той же writer-транзакции). Альтернатива — отправить без // версии — корректна тоже, но принимающая сторона тогда не имеет // доказательства редоставки и ОТКРЫВАЕТСЯ в сторону работы: сервис платит за // обе доставки на каждом создании, тихо и навсегда. Из двух исходов громкий // выбран намеренно: непротащенный маркер — ошибка программиста, и она обязана // быть слышна, а не оплачиваться в рантайме. ErrUnversioned = errors.New("owner-register: регистрация без маркера версии из writer-транзакции") )
Functions ¶
func ParentChain ¶
ParentChain — цепь предков ОДНОЙ доставки регистрации, от ближайшего предка к дальнему, каждый элемент в форме `"<type>:<id>"`.
Зачем это отдельная функция, а не поле, заполняемое на месте ¶
Принимающая сторона держит цепь предков объекта отдельными рёбрами и заменяет их набор ЦЕЛИКОМ на каждой применённой регистрации: цепь — это состояние, а не приращение. У замены две стороны, и вторая — предмет этой функции: производитель, который цепи не называет, на каждой перерегистрации своего объекта СТИРАЕТ уже записанных предков и не ставит новых. Объект без предка молча выпадает из области выдачи и из каскада — вопрос о нём задаётся, ответ «нет», и отказ неотличим от честного.
Что функция делает и чего НЕ делает ¶
explicit непуст ⇒ возвращается как есть. Иерархию глубже двух уровней (репозиторий → реестр → проект) знает только владелец, и вывести её из области нельзя: область знает ровно проект и аккаунт. Подмена укоротила бы цепь до той, которой у объекта нет. explicit пуст ⇒ цепь выводится ИЗ ОБЛАСТИ ЭТОЙ ЖЕ ДОСТАВКИ — из тех самых `ParentProjectID`/`ParentAccountID`, что едут в том же сообщении. Ничего нового не выдумывается: доставка уже утверждает эти два предка, функция лишь называет их в форме цепи.
Пустой explicit означает «владелец не назвал», а НЕ «предков нет»: строка, поставленная в очередь до появления поля, декодируется именно так, и принять её за утверждение значило бы стирать предков на каждом повторе. «Предков нет» выражается пустой ОБЛАСТЬЮ — тогда результат тоже пуст, и это законный ответ для объекта корневого уровня.
Цепь непрерывна: без проекта ближайшим предком становится аккаунт. Дыра остановила бы обход вверх и объявила объект не принадлежащим аккаунту.
Types ¶
type RegisterRPC ¶
type RegisterRPC interface {
RegisterResource(ctx context.Context, in *iamv1.RegisterResourceRequest, opts ...grpc.CallOption) (*iamv1.RegisterResourceResponse, error)
}
RegisterRPC — узкий порт к владельцу прав. Именно узкий, а не весь InternalIAMServiceClient: подставной дублёр обязан выполнять контракт настоящего, и чем меньше поверхность, тем меньше ему есть где оказаться снисходительнее.
type Registrar ¶
type Registrar struct {
// contains filtered or unexported fields
}
Registrar — синхронная доставка регистраций владельцу прав.
УСКОРИТЕЛЬ, А НЕ ИСТОЧНИК ИСТИНЫ. Те же строки лежат durable-намерениями в очереди той же writer-транзакции, поэтому дренаж доводит регистрацию сам. Отсюда и форма отчётности: ошибка ВОЗВРАЩАЕТСЯ вызывающему, но вызывающий её логирует, а не роняет мутацию — провалить уже закоммиченный ресурс значило бы отдать клиенту отказ на существующую вещь (фантом, ban #9).
func New ¶
func New(cli RegisterRPC) (*Registrar, error)
New собирает регистратор. nil-клиент — ОТКАЗ (см. ErrNoClient).
func (*Registrar) Register ¶
func (r *Registrar) Register(ctx context.Context, regs []Registration) error
Register доставляет каждую регистрацию набора владельцу прав.
НЕ КОРОТКОЗАМЫКАЕТСЯ: отказ на одной строке не отменяет попытки по остальным, и все отказы возвращаются объединёнными. Причина не в полноте отчёта, а в том, ЧТО стоит первым в наборе: указатель принадлежности объекта. Через него администратор аккаунта достаёт объект вообще; потерять его из-за отказа на соседнем tuple'е значит потерять весь административный ярус на этом ресурсе до дренажа.
type Registration ¶
type Registration struct {
Tuple Tuple
// TraceID — идентификатор ресурса, к которому относится доставка.
TraceID string
// Labels / ParentProjectID / ParentAccountID — кормление зеркала владельца
// прав. Пустое значение законно (не у всякого ресурса есть метки, и не
// всякий сервис резолвит проект→аккаунт на горячем пути).
Labels map[string]string
ParentProjectID string
ParentAccountID string
// ParentChain — цепь предков от БЛИЖАЙШЕГО к дальнему, каждый элемент в
// форме `"<type>:<id>"`.
//
// Двух полей выше хватает не всякому ресурсу: они несут ровно проект и
// аккаунт, а модель прав требует цепи ПРОИЗВОЛЬНОЙ формы (репозиторий →
// реестр → проект → аккаунт). Ресурс, чей ближайший предок не проект и не
// аккаунт, без этой цепи регистрируется БЕЗ предка — и молча выпадает из
// области выдачи и из каскада: вопрос о нём задаётся, ответ «нет», и это
// неотличимо от честного отказа.
//
// Пусто означает «предков нет»: принимающая сторона заменяет набор рёбер
// объекта целиком, поэтому доставка без цепи не «ничего не меняет», а
// стирает уже записанных предков. Заполнять цепь обязан КАЖДЫЙ потребитель;
// у кого иерархия не глубже проекта — вызовом [ParentChain] по области этой
// же доставки, у кого глубже — своим перечислением звеньев.
ParentChain []string
// SourceVersion — маркер, проштампованный БД ВНУТРИ writer-транзакции,
// породившей это намерение. Не часы момента доставки: обе доставки одной
// строки обязаны нести ОДНО значение, иначе гашение редоставки у принимающей
// стороны зависит от того, кто выиграл гонку. Нулевое значение — отказ
// ([ErrUnversioned]), а не «сойдёт и так».
SourceVersion time.Time
}
Registration — ОДНА строка durable-намерения, готовая к синхронной доставке.
Единица — именно tuple, а не «намерение целиком», и это существенно. Гейт редоставки у принимающей стороны ключуется на строке зеркала, а она ПО ОБЪЕКТУ; при этом одна доставка законно несёт на ОДИН объект несколько tuple'ов (у registry — указатель принадлежности, затем владелец-создатель). Была бы версия одна на набор — второй tuple не менял бы зеркала, был бы неотличим от редоставки и проглатывался вместе с постановкой владельца. Поэтому версию несёт КАЖДАЯ строка, ровно та, которой БД проштамповала её в writer-транзакции: у registry триггер даёт разные clock_timestamp() на разные строки, у nlb эмиттер шагает порядковым номером внутри транзакции, у vpc/storage/compute now() одинаков — но там на один объект приходится одна строка, и совпадение безвредно.