Documentation
¶
Overview ¶
Package servicecontract — то, что сервис объявляет О СЕБЕ, чтобы носитель входящего пути (`pkg/servicehost`) поднял его контур работы с владельцем прав.
Почему это ДАННЫЕ, а не сборка ¶
Семь сервисов собирали цепочку звеньев каждый у себя, и порядок держался тем, что авторы написали одинаковое. Здесь сервис приносит ЗНАЧЕНИЯ — кто он, в каком режиме, каким транспортом говорит с владельцем прав, что эмитит, что сужает, что скрывает, — и ни одного поля интерсепторного типа. Невыразимость чужой цепочки и есть механизм: восьмой сервис, желающий свой порядок звеньев, не «нарушит правило» — ему не на чем его записать.
Что этот пакет НЕ решает ¶
Он решает, ЧТО МОЖНО ЗАПИСАТЬ. Согласованность записанного с тем, что процесс РЕАЛЬНО служит, решает github.com/PRO-Robotech/kacho/pkg/servicehost — там живут отказы, которым нужен служимый набор RPC и выведенный из него каталог прав. Применение записанного не закрывается ни тем, ни другим: его держат гейты по дереву. Смешивать эти три уровня нельзя — выдавать гейт за свойство построения значит объявить закрытым то, что открыто.
ДВЕ ПОЛОВИНЫ ОДНОГО [Spec], и их различие несущее ¶
Первая — ПОСАДКА: режим, шифрование до собственной базы, круг отправителей переданной личности. Она есть у КАЖДОГО развёрнутого процесса, и ровно её требует ban #16. Вторая — ПРОВОДКА НОСИТЕЛЯ: что эмитить, что сужать, что скрывать, какие пределы ставить звеньям. Её читает только носитель.
Пока половины были неразличимы, принять дескриптор мог лишь тот, кто приносит обе, — то есть процессы с собственным контуром через единый источник не проходили вовсе. Различает их Spec.OwnContour; посадка судится при любом его значении, проводка при непустом ЗАПРЕЩЕНА.
Поля `Domain` здесь НЕТ намеренно ¶
Домен ВЫВОДИТСЯ из имён зарегистрированных gRPC-сервисов (носитель снимает их у самого сервера). Объявление ввело бы второй источник, опечатка в котором молча выбирает ноль строк каталога — отказ старта такое поймает, но диагностика хуже, а второго источника не должно быть в принципе.
Index ¶
- func Modes() []string
- func Verified(c credentials.TransportCredentials) bool
- type Admission
- type AuthzSource
- type Axis
- type BootGate
- type DeliveryProvenance
- type Descriptor
- type ExistenceProbe
- type ForwarderKnobs
- type ListNarrower
- type MethodFQN
- type Mode
- type NotFoundFormat
- type ObjectType
- type PeerEdge
- type ServiceName
- type Spec
- type Surface
- type SurfaceAuthMech
- type SurfaceDescriptor
- type SurfaceName
- type SurfaceReach
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Modes ¶
func Modes() []string
Modes — допустимые написания посадки, в порядке нарастания строгости.
Существует ради ТЕКСТОВ ОТКАЗА: страж старта, перечисляющий словарь своими руками, есть второе место об одном предмете, и расходится оно первым. Копия возвращается вызывающему намеренно — перечень не должен меняться из-под дома.
func Verified ¶
func Verified(c credentials.TransportCredentials) bool
Verified — проверенный ли это транспорт, ПО ОТВЕТУ САМОГО ТРАНСПОРТА.
Types ¶
type Admission ¶
type Admission struct {
// Public — величины публичного слушателя, на ЛИЧНОСТЬ КОНЕЧНОГО
// ПОЛЬЗОВАТЕЛЯ.
Public grpcsrv.AdmissionLimits
// Internal — величины внутреннего слушателя, на ЛИЧНОСТЬ СЕРТИФИКАТА
// вызывающего модуля.
Internal grpcsrv.AdmissionLimits
}
Spec — по-процессная плоскость: всё, что сервис объявляет о себе.
Полей типа `grpc.UnaryServerInterceptor` / `grpc.StreamServerInterceptor` / `grpc.ServerOption` здесь НЕТ. Это не упущение и не стиль — это механизм: цепочку невозможно принести, поэтому её невозможно собрать по-своему. Admission — величины потолка ОБОИХ слушателей процесса.
Пара, а не два поля Spec: они объявляются вместе или не объявляются вовсе. Частичное объявление — самопротиворечие, а не «часть защиты»: процесс, у которого ограничен публичный слушатель и не ограничен внутренний, пишет в журнал «ограничитель взведён» — про ту половину, которая взведена.
func AdmissionFromPosture ¶
func AdmissionFromPosture(public, internal grpcsrv.AdmissionKnobs) (Admission, error)
AdmissionFromPosture — величины ОБОИХ слушателей из ручек посадки.
Пол платформы там, где посадка молчит; её собственные величины там, где она назвала ВЕСЬ набор; отказ там, где назвала часть либо назвала негодное.
Отказ, а не дополнение полом: оператор, задавший темп и забывший одновременность, получил бы наполовину свои, наполовину чужие величины и считал бы предел выставленным. Отказ называет СЛУШАТЕЛЯ — искать причину оператор пойдёт в файл настроек, где, по его мнению, всё написано верно.
type AuthzSource ¶
type AuthzSource uint8
AuthzSource — откуда берётся решение о доступе. Перечисление, а не перегрузка нулевого указателя: «клиента нет» и «решение принимаю сам» — разные утверждения, и различать их обязан тип, а не отсутствие значения.
Третьего значения — «решения нет» — здесь НЕТ и не будет. Ветка «цепочка без звена решения» в носителе не выражается: `Serve` либо собрал контур, либо отказал. Именованная ручка, снимающая проверку прав, была бы ровно тем обходом, которого в этом дереве быть не должно.
const ( // AuthzViaIAM — решение принимает владелец модели, сервис спрашивает его по // объявленному ребру. AuthzViaIAM AuthzSource // AuthzSelf — сервис САМ владелец модели и решает у себя. Ребра к себе он не // объявляет, и это не пропуск. AuthzSelf )
type Axis ¶
type Axis[T any] struct { // contains filtered or unexported fields }
Axis — ось, у которой ПУСТОЕ ЗНАЧЕНИЕ ЗАКОННО.
Зачем отдельный тип, а не просто поле ¶
«Пусто» и «не применимо» неразличимы без явного «почему». Сервис, у которого ось пуста по построению (глобальный справочник не раздаёт пообъектных грантов), и сервис, у которого её просто забыли заполнить, выглядят одинаково — а решения по ним противоположны. Пока различия не было, необъяснённая пустая клетка читалась как «здесь ничего не нужно», и проверить это было нечем.
Поэтому у оси ТРИ состояния, а не два:
- значение — Value;
- «не применимо, потому что …» — NotApplicable с НЕПУСТОЙ причиной;
- ничего не сказано — нулевое значение, которое New отвергает (О10).
Пустая причина объявлением НЕ является намеренно: иначе закрыть ось стало бы дешевле, чем заполнить, и `NotApplicable("")` превратился бы в то самое умолчание, ради устранения которого тип и заводится.
Почему тип заводится ТОЛЬКО для таких осей ¶
Обёртка на каждом поле была бы церемонией: у большинства полей пустое значение незаконно, и там достаточно обязательного поля с отвергающим конструктором. Тип берут ровно те оси, у которых «пусто» бывает ЗАКОННЫМ ответом, и таких причин две: пустое ЗНАЧЕНИЕ законно (набор эмитируемых отношений, регистрируемые типы, сужатели, формы скрытия, происхождение доставки) либо законно САМО ОТСУТСТВИЕ величины — потолок стрима у процесса без подписок, отсечка отказов у владельца модели, загрузочный гейт у того, кто ничего не эмитит, потолок запроса у потоковой поверхности.
Числа здесь не выписаны намеренно: прежняя редакция называла «четыре плюс одна», и это перестало быть верным раньше, чем кто-нибудь заметил, — новые потребители заводились по второй причине, которой в тексте не было вовсе. Перепись берётся предикатом, а не памятью: `git grep -c 'Axis\[' pkg/servicecontract/*.go` (объявления полей — в Spec и в Surface).
Заявление «не применимо» СУДИТСЯ, а не принимается на слово ¶
Само по себе объяснение — слово автора. Оно становится проверяемым там, где у него есть внешний судья: `NotApplicable` на оси сужателей при наличии строки `scope_filtered` того же домена есть НАХОДКА ([servicehost] О3), и ровно так же судится ось скрытия существования (О5). Поэтому исключение истекает само: как только у домена появляется первая такая строка каталога, заявление перестаёт быть верным и роняет старт.
func NotApplicable ¶
NotApplicable объявляет ось неприменимой С ПРИЧИНОЙ. Пустая причина объявлением не является — Axis.Declared на ней ложен, и New отказывает.
func Value ¶
Value объявляет ось значением. Пустое ЗНАЧЕНИЕ (пустой срез, пустая карта) — законное объявление и отличается от необъявленности: сервис вправе сказать «набор пуст» явно.
func (Axis[T]) Declared ¶
Declared сообщает, сказал ли автор про эту ось ХОТЬ ЧТО-ТО. Это и есть предикат О10: незаявленная ось не доживает до обслуживания запроса.
func (Axis[T]) Get ¶
Get отдаёт значение оси; ok ложен, если ось объявлена неприменимой или не объявлена вовсе.
func (Axis[T]) NotApplicableBecause ¶
NotApplicableBecause отдаёт причину неприменимости; ok ложен, если ось несёт значение или не объявлена.
type BootGate ¶
type BootGate interface {
// GuardMutation возвращает отказ, если мутацию принимать нельзя, и nil иначе.
GuardMutation() error
}
BootGate — загрузочный гейт мутаций в той форме, в какой его спрашивает носитель: РЕШЕНИЕ по мутирующему вызову, и только оно.
Порт, а не конкретный тип, потому что реализация живёт в `pkg/outbox/bootgate` и тянуть очередь в контракт процесса незачем.
Прежняя редакция требовала здесь ещё и `Ready() bool` — «носителю нужны ровно два ответа». Носителю нужен один: готовность читает проба готовности пода, а её поднимает сам сервис своим диагностическим слушателем, которого в контуре нет. Объявленный и никем не вызываемый метод — тот же мёртвый страж, что и мёртвое поле, только этажом ниже: реализация обязана его нести, а спросить его некому.
type DeliveryProvenance ¶
type DeliveryProvenance uint8
DeliveryProvenance — ОТКУДА происходят намерения регистрации, которые сервис отправляет владельцу прав.
Почему это поле дескриптора, а не поле запроса ¶
Признак первой доставки принимающая сторона принимает как доказательство ЛИШЬ В КОНЪЮНКЦИИ со своим вердиктом по данным — правило «это не слово вызывающего» остаётся дословным. Поставить признак В ЗАПРОС значило бы завести провод, по которому это слово можно СКАЗАТЬ, и тем ослабить то, что сегодня держится отсутствием слота. Здесь признак — свойство ПРОЦЕССА, объявленное один раз при старте: он говорит, как устроен сервис, а не что верно про конкретную строку очереди.
Почему у поля есть читатель ¶
Ось судится соседкой: эмитент обязан назвать происхождение, а тот, кто ничего не эмитит, назвать его НЕ вправе — это было бы утверждением без предмета. Объявленное-и-никем-не-читаемое поле есть мёртвый страж, и заводить его запрещено (`00-kacho-core.md` #16).
const ( // DeliveryWriterTransaction — намерение порождается ТОЙ ЖЕ writer-транзакцией, // что и создание ресурса. Самая узкая семантика из существующих: происхождение // доказано записью, а не выведено из часов в момент доставки. DeliveryWriterTransaction DeliveryProvenance // DeliveryReconciled — намерение восстанавливается сверкой состояния, а не // порождается транзакцией создания. Доказательством первой доставки НЕ является // и заявляться им не может. DeliveryReconciled )
func (DeliveryProvenance) IsProven ¶
func (d DeliveryProvenance) IsProven() bool
IsProven сообщает, доказано ли происхождение намерения записью.
Отдельный предикат вместо сравнения с константой у каждого вызывающего: так «не задано» и «восстановлено сверкой» отвечают одинаково — не доказано, — и добавление третьего недоказательного вида не потребует обойти всех, кто спрашивает. Нулевое значение здесь СЧИТАЕТСЯ, а не подразумевается.
func (DeliveryProvenance) String ¶
func (d DeliveryProvenance) String() string
String — представление для журнала.
Нулевое значение названо СВОИМ именем, а не уходит в общую ветку «прочее»: «происхождение не задано» и «происхождение неизвестного вида» — разные факты, и первый обязан быть отличим от второго в журнале. Ветка `default` остаётся для значения вне словаря — она и есть признак того, что перечень разошёлся с кодом, который его читает.
type Descriptor ¶
type Descriptor struct {
// contains filtered or unexported fields
}
Descriptor — ПРИНЯТЫЙ Spec.
Поле неэкспортируемое: литералом не собрать, поэтому «мимо конструктора» — не обход правила, а невыразимость. Всё, что носитель читает, он читает отсюда, и значит читает только проверенное.
func New ¶
func New(s Spec) (Descriptor, error)
New принимает Spec или отказывает, НАЗЫВАЯ КАЖДУЮ незаполненную ось и каждое несогласованное поле — все разом, а не первое попавшееся.
Почему все разом: отказ по первой находке заставляет автора чинить по одной и узнавать о следующей только на следующем старте. Четыре перезапуска вместо одного — и это при том, что все находки уже известны в момент первой.
Здесь живут отказы, которые являются свойствами САМОГО ДЕСКРИПТОРА: О1 (круг), О6 (ребро), О7 (окно и бюджет), О8 (боевая посадка), О10 (незаявленная ось), О12 (кеш вердиктов), О13 (задержка вызова), О14 (проводка носителя у процесса с собственным контуром). Отказы, которым нужен служимый набор RPC и выведенный из него каталог, живут в `pkg/servicehost`: О2, О3, О4, О5, О9.
func (Descriptor) Accepted ¶
func (d Descriptor) Accepted() bool
Accepted — прошёл ли дескриптор конструктор. Нулевое значение отвечает `false`, поэтому носитель отличает принятый дескриптор от собранного литералом.
func (Descriptor) OwnContour ¶
func (d Descriptor) OwnContour() string
OwnContour — причина, по которой контур входящего пути поднимает не носитель. Пустая строка означает «поднимает носитель». Носитель спрашивает ЕЁ, а не поле напрямую: дескриптор, собранный литералом, `Accepted()` не проходит, и решение о подъёме обязано читать только принятое.
func (Descriptor) Spec ¶
func (d Descriptor) Spec() Spec
Spec отдаёт принятые значения. Копия структуры: изменить объявленное после того, как отказы старта его одобрили, вызывающий не должен.
type ExistenceProbe ¶
type ExistenceProbe interface {
ObjectExists(ctx context.Context, objectType, objectID string) (bool, error)
// ProbeableTypes — типы объектов, о которых проба УМЕЕТ ответить.
//
// # Зачем порт объявляет свой охват, а не отвечает ошибкой по факту
//
// Неизвестный тип проба отвергает ошибкой, а вызывающий трактует ошибку как
// «не могу подтвердить отсутствие» и оставляет отказ отказом (fail-closed).
// Это верное поведение НА ЗАПРОСЕ и негодное как единственное: пока охват
// проверяется только запросом, тип, до которого проба не доросла, ведёт себя
// не как дефект, а как «нет доступа» — то есть неотличимо от исправной
// работы. Наблюдалось (задача продукта #1931): у compute из трёх пообъектных
// типов карты прав проба знала ОДИН, и два соседних типа того же сервиса
// отвечали на одном и том же входе разным кодом.
//
// Объявленный охват даёт носителю вторую сторону сравнения, и сравнение
// становится ВЫВЕДЕННЫМ с обеих сторон: пообъектные типы выводятся из карты
// прав, охват — из самой пробы. Ни одного выписанного перечня в месте
// сравнения нет, поэтому расхождение не может пережить правку одной стороны.
//
// Отдаётся КОПИЯ либо свежий срез: носитель читает возвращённое и не вправе
// зависеть от того, что за ним стоит внутренняя карта пробы.
ProbeableTypes() []string
}
ExistenceProbe — порт «есть ли такой объект в МОЕЙ базе».
Нужен ровно там, где сервис скрывает существование: отказ приходит текстом промаха владельца, а придумывать «not found» не из чего, пока не установлено, что объект существует и просто не принадлежит вызывающему. Поэтому порт судится осью скрытия: объявлено скрытие — порт обязателен, не объявлено — порт есть проводка, которую никто не спросит.
type ForwarderKnobs ¶
type ForwarderKnobs struct {
// SANs — имя ручки, которой задаётся круг. Нужно тексту отказа.
SANs string
// TrustAny — имя ручки landed-опт-ина. Нужно тексту отказа: сообщение,
// не называющее ручку, оставляет стенд неподнятым и непонятным.
TrustAny string
// OptIn — испрошен ли опт-ин. Передаётся landed-стражу как есть; в боевом
// режиме страж его НЕ читает. Проверено парой проб: тот же пустой круг с
// испрошенным опт-ином в боевом режиме остаётся отказом, вне боевого —
// принимается.
OptIn bool
}
ForwarderKnobs — имена ручек круга отправителей и значение dev-опт-ина.
Имена нужны ТЕКСТУ ОТКАЗА, который читает оператор: сообщение, не называющее ручку, оставляет стенд неподнятым и непонятным. Это одно из трёх мест, выведенных из-под запрета на подробности в публичных артефактах, — рантайм- диагностика, а не рассказ о том, где было открыто.
Поля `Production` здесь НЕТ: боевая ли посадка, решает Spec.Mode, а не вызывающий. Пока это были две величины, страж мерил не то, что транспорт.
Опт-ин на несужение — НЕ новое послабление, и вот чем это проверяется ¶
Ни одно поле этой структуры не заводит обхода. Решение по кругу принимает landed-страж `grpcsrv.TrustedForwarders.Require`, и опт-ин в нём читается ТОЛЬКО вне боевого режима (`if !g.Production && g.DevTrustAny`); в боевом он не читается вовсе, поэтому «включить ручку и снять защиту на боевом стенде» невыразимо. Здесь опт-ин лишь ПЕРЕДАЁТСЯ стражу вместе с двумя именами ручек, которые нужны ТЕКСТУ ОТКАЗА.
Обратный порядок — «убрать опт-ин ради чистоты» — вернул бы дефект, из которого он и родился: страж, срабатывающий на каждом старте, остановил бы локальные посадки, и защиту сняли бы целиком. Опт-ин переводит пустой круг из УМОЛЧАНИЯ в ЯВНУЮ ПРОСЬБУ — это выигрыш, и он остаётся выигрышем после того, как назван поверхностью. Остаточная поверхность записана в §9 п.12 приёмки XC-7, а не оставлена в цене фазы.
type ListNarrower ¶
type ListNarrower = *listnarrow.Narrower
ListNarrower — ПРОВОДКА сужателя списочной выдачи одного метода.
Это ПСЕВДОНИМ единственной landed-реализации, а не свой интерфейс. Причина — в том, что закрывается: пока сужатель был значением композиционного корня, «взять реализацию» было можно, а «не взять» — тоже, и обязанности не держало ничто. Псевдоним делает второй исход невыразимым: подставить сюда свою реализацию нельзя, потому что типа для неё нет.
Перечня сужаемых методов дескриптор НЕ несёт — его даёт каталог прав. Двух объявлений одного предмета не существует, поэтому расходиться нечему; вместо сверки объявлений носитель сверяет ПРОВОДКУ с каталогом в обе стороны (О3/О4).
type MethodFQN ¶
type MethodFQN string
MethodFQN — полное имя метода в той форме, в какой его передаёт grpc-go (`/kacho.cloud.geo.v1.RegionService/Get`). Та же форма — ключ каталога прав, поэтому проводка и каталог соединяются без переводчика между ними.
type Mode ¶
type Mode uint8
Mode — посадка процесса. Разбирается ОДНИМ местом (ParseMode): пока каждый сервис разбирал строку окружения сам, «боевой режим» означал у семерых семь слегка разных вещей, и сверять их было не с чем.
Словарь ЗНАЧЕНИЙ тоже один, и это отдельное утверждение ¶
Адрес ручки сведён к одному написанию раньше; ЗНАЧЕНИЯ, которые она принимает, оставались у сервисов своими. Замер (задача продукта #1656): объявлений словаря в дереве было ПЯТЬ, и одно расходилось с остальными В ОБЕ СТОРОНЫ — принимало алиасы, которых не принимал никто (`prod`, `development`, пустая строка), и отвергало `production-strict`, в котором работали соседи.
Наблюдалось это оператором как две противоположные поломки сразу: попытка выровнять флот на строгой посадке роняла один сервис отказом старта, а попытка написать короткий алиас роняла шесть из семи и поднимала один. То есть единой команды «ужесточить флот» не существовало, и увидеть это можно было только на выкатке.
const ( // ModeDev — локальные фикстуры и отладка. ModeDev Mode // ModeProduction — боевая посадка. ModeProduction // ModeProductionStrict — боевая посадка со строгой проверкой сертификата БД. ModeProductionStrict )
func ParseMode ¶
ParseMode разбирает строку окружения. Неизвестное значение — ОШИБКА, а не откат к умолчанию: умолчание здесь есть выбор посадки, сделанный никем.
Сравнение ТОЧНОЕ — ни регистр, ни обрамляющие пробелы не прощаются, и это решение дома, а не недосмотр. Смягчение выглядит вежливым, а означает, что посадка выбирается не тем, что написано в профиле, а тем, что удалось из этого вывести; отказ же называет ручку и весь допустимый набор, поэтому чинится за секунды. Три сервиса прощали регистр и пробелы своими копиями словаря — и вместе с этим прощали `prod`, `development` и пустую строку, которых не принимал никто.
Перечень для текста отказа берётся у словаря, а не пишется здесь: страж, перечисляющий набор своими руками, есть второе место об одном предмете.
func (Mode) IsProduction ¶
IsProduction — боевая ли посадка. Единственный предикат, по которому принимаются решения о строгости; сравнивать со строками на местах запрещено.
type NotFoundFormat ¶
type NotFoundFormat string
NotFoundFormat — контракт-тон отказа владельца ресурса (`"Network %s not found"`) с ЕДИНСТВЕННЫМ `%s` под идентификатор, который назвал вызывающий.
Форма важна дословно: скрытие существования работает ровно настолько, насколько текст отказа неотличим от настоящего промаха владельца. Любой различимый текст — оракул существования.
type ObjectType ¶
type ObjectType string
ObjectType — тип объекта модели прав (`vpc_network`, `project`). Отдельный тип, а не строка, чтобы ось регистрируемых типов и ось скрытия существования нельзя было заполнить чем угодно.
type PeerEdge ¶
type PeerEdge struct {
// contains filtered or unexported fields
}
PeerEdge — ребро к соседу: адрес И транспорт, ЗАДАННЫЕ ЯВНО.
Экспортируемых полей нет намеренно. Умолчание вида «взять базовый адрес соседа и приклеить путь» для ребра, от которого зависит решение о доступе, запрещено: оно всегда непустое, поэтому контроль выглядит включённым, а ведёт в никуда, и ни один профиль развёртывания не обязан ничего задавать, чтобы это заметить. Конструктора «вывести из чужого адреса» здесь нет, поэтому вывод невыразим, а не «не рекомендован».
func NewPeerEdge ¶
func NewPeerEdge(addr string, creds credentials.TransportCredentials) PeerEdge
NewPeerEdge объявляет ребро. Обе половины обязательны и проверяются New: адрес без транспорта и транспорт без адреса — одинаково несобранное ребро.
func (PeerEdge) Creds ¶
func (e PeerEdge) Creds() credentials.TransportCredentials
Creds — транспорт ребра.
type ServiceName ¶
type ServiceName string
ServiceName — короткое имя процесса (`kacho-geo`). Попадает в метрики, в самоотчёт о посадке и в текст отказа оператору, поэтому пустым быть не может: отказ, не называющий сервиса, на стенде из семи процессов бесполезен.
type Spec ¶
type Spec struct {
// Service — короткое имя процесса.
Service ServiceName
// Mode — посадка. Из неё, и только из неё, выводится боевая строгость.
Mode Mode
// Logger — журнал процесса. Ноль резолвится в [slog.Default]: журнал —
// единственное поле, чьё умолчание не является решением о доступе.
Logger *slog.Logger
// OwnContour — ПРИЧИНА, по которой контур входящего пути этого процесса
// поднимает не носитель ([github.com/PRO-Robotech/kacho/pkg/servicehost]), а
// его собственный композиционный корень. Пустая строка означает «поднимает
// носитель» и является обычным случаем.
//
// # Зачем поле вообще есть
//
// [Spec] описывает ДВЕ разные вещи. Первая — ПОСАДКА: режим, шифрование до
// своей базы, круг отправителей. Она есть у КАЖДОГО развёрнутого процесса, и
// ровно её требует ban #16. Вторая — ПРОВОДКА НОСИТЕЛЯ: что эмитить, что
// сужать, что скрывать, какие пределы ставить звеньям. Её читает ТОЛЬКО
// носитель, и больше никто.
//
// Пока поля не было, принять дескриптор мог лишь тот, кто приносит обе
// половины. Процессы с собственным контуром — фасад личности и внешний край —
// не могли принести вторую, потому что её некому прочитать, и потому не
// проходили через единый источник ВОВСЕ: набор осей, которые каждый из них
// судит, выбирал он сам, а расхождение между наборами было невидимо.
// Заполнить проводку «правдоподобно» было бы хуже отказа: объявление, которое
// никто не читает, расходится с фактической ручной сборкой молча.
//
// # Почему это НЕ ручка, снимающая проверки
//
// Три свойства сразу, и ни одно не держится обещанием:
//
// - ПОСАДКУ поле не трогает: она судится при любом его значении. Снимается
// только проводка носителя — то, что при собственном контуре не читает
// никто;
// - проводка при непустом значении не просто не требуется, а ЗАПРЕЩЕНА
// (О14): принесённая и непрочитанная, она была бы вторым местом об одном
// предмете, из которых верно одно;
// - заявление стоит ПРОЦЕССУ НОСИТЕЛЯ. `servicehost.Serve` отказывает
// дескриптору с непустым `OwnContour`, поэтому объявить его ложно значит
// собрать весь контур руками — работа, которую нельзя ни сделать
// незаметно, ни объяснить удобством.
//
// Причина обязана быть непустой по той же причине, по какой её требует
// [NotApplicable]: изъятие без причины не отличимо от забывчивости.
OwnContour string
// Forwarders — круг личностей сертификата, которым разрешено передавать
// личность конечного пользователя.
//
// Семантика нулевого значения ВНУТРИ оси — «круг НЕ сужен»: это действующая
// семантика общей библиотеки, и тип её не переопределяет. «Забыл настроить»
// ловит отказ старта (О1) по ТОМУ ЖЕ предикату `IsNarrowed()`, который
// читает транспорт, а не смена смысла пустого множества.
//
// # Почему ОСЬ, а не поле (введено вместе с переводом края, задача #1407)
//
// Пока это было поле, «круг не сужен» и «принимать переданную личность
// НЕКОМУ» записывались ОДНИМ И ТЕМ ЖЕ нулевым значением, а отличаются они
// ровно тем, ради чего страж заведён. Процесс, который переданную личность
// не принимает, а ОТПРАВЛЯЕТ, сужать ему нечего: у него нет ни ручки круга,
// ни звена, которое его прочтёт. Полем такое состояние выражается только
// выдуманным непустым кругом — то есть объявлением защиты, которой нет.
//
// Изъятие ИСТЕКАЕТ САМО и не памятью автора: обход дерева
// (`internal/repohygiene.TestServiceDeclaringPostureKnobsHasABootGuard`)
// требует, чтобы компонент, объявивший ось неприменимой, НЕ объявлял её
// ручек. Появится у него ручка круга — изъятие станет находкой.
Forwarders Axis[grpcsrv.TrustedForwarders]
// TrustDomain — домен доверия установки: то, чьи сертификаты она признаёт
// своими. Круг отправителей называет, КОМУ позволено говорить за
// пользователя; домен — чьи вообще предъявители наши.
//
// # Почему ОСЬ, а не поле
//
// По той же причине, что у соседки: «домен не объявлен» и «личность
// сертификата этот процесс не разбирает» записывались бы одним нулевым
// значением, а отличаются они ровно тем, ради чего страж заведён. Процесс,
// поднимающий контур САМ, вправе не разбирать личность вовсе; процесс,
// чей контур поднимает носитель, разбирает её всегда — носитель ставит пару
// звеньев извлечения безусловно и на обоих слушателях.
//
// Семантика нулевого значения ВНУТРИ оси — «домен не назван», и она
// фейл-клоуз: по необъявленному домену не опознаётся ни один предъявитель.
// Умолчания у домена нет и быть не может: непустое умолчание сделало бы
// контроль на вид включённым и увело бы установку, забывшую назвать свой
// домен, в чужой (`security.md` §«Адрес зависимости… НЕ выводится из чужого»).
TrustDomain Axis[grpcsrv.TrustDomain]
// TrustDomainKnob — имя ручки для текста отказа. Читается только там, где
// [Spec.TrustDomain] несёт значение: у процесса, не разбирающего личность,
// ручки нет, и называть её в отказе было бы нечем.
TrustDomainKnob string
// ForwarderKnobs — имена ручек для текста отказа и dev-опт-ин. Читаются
// только там, где [Spec.Forwarders] несёт значение: у процесса без круга
// ручек нет, и называть их в отказе было бы нечем.
ForwarderKnobs ForwarderKnobs
// Authz — кто принимает решение о доступе.
Authz AuthzSource
// CheckEdge — ребро к владельцу модели. Обязательно при [AuthzViaIAM].
CheckEdge PeerEdge
// SelfCheck — решатель ВЛАДЕЛЬЦА модели. Обязателен при [AuthzSelf] и
// запрещён при [AuthzViaIAM].
//
// Поле существует затем, чтобы «сам себе владелец» не оказалось веткой,
// которую носитель не умеет поднять. Пара с [Spec.CheckEdge] исчерпывающая и
// взаимоисключающая: у каждого источника решения ровно один способ его
// принести, и перепутать их нельзя — оба судятся [New].
SelfCheck authz.CheckClient
// PeerCheck — СБОРЩИК решателя из соединения с владельцем модели.
// Обязателен при [AuthzViaIAM] и запрещён при [AuthzSelf].
//
// # Почему сборщик приносит сервис, а не носитель делает его сам
//
// Перевод вопроса о доступе в контракт владельца — знание о КОНТРАКТЕ
// СЛУЖБЫ ДОСТУПА, а носитель принадлежит фундаменту. Пока адаптер жил в
// носителе, фундамент импортировал этот контракт: после разъезда на три
// модуля `corelib` потребовал бы `kaname`, который уже требует `corelib`, —
// цикл, который Go не собирает (приёмка K3-1 §7.2, задача #2131).
//
// Носитель по-прежнему сам НАБИРАЕТ соседа по объявленному ребру
// ([Spec.CheckEdge]) и сам закрывает соединение: посадку ребра судит эта же
// проверка, и ребро, которое никто не набирает, стало бы объявлением без
// предмета. Наружу вынесено ровно одно — перевод в чужой контракт.
//
// Пара с [Spec.SelfCheck] остаётся исчерпывающей и взаимоисключающей: у
// каждого источника решения ровно один способ его принести.
PeerCheck authz.CheckClientFrom
// CacheWindow — окно кэша положительных вердиктов, оно же ОКНО ОТЗЫВА:
// столько субъект, у которого право уже отобрали, продолжает проходить.
// Умолчания нет: параметр безопасности, которого никто не выбирал, нельзя
// ни обсудить, ни сузить на конкретной посадке.
CacheWindow time.Duration
// ClientBudget — срок одного вопроса владельцу модели. Умолчания нет по той
// же причине.
ClientBudget time.Duration
// DenyBudget — устойчивый темп проверок (в секунду на принципала), чей исход
// кэш НЕ поглощает: отказ, сокрытие существования, промах «нет пути» и
// недоступность модели. По исчерпании звено отвечает `ResourceExhausted`, не
// обращаясь к владельцу модели, — то есть сбрасывает шторм с него.
//
// # Почему ОСЬ, а не число, и почему нулю здесь не место
//
// Механизм (`pkg/authz`) читает ноль как «ограничения нет»: при `ratePerSec
// <= 0` бюджет есть всегда. Значит незаполненное поле МОЛЧА выключило бы
// отсечку, ветка `DecisionRateLimited` стала бы недостижимой, а её счётчик —
// навсегда нулевым; заметить пропажу нечем, потому что «шторма не было» и
// «отсечки не было» выглядят одинаково.
//
// Поэтому три состояния вместо двух: величина (строго больше нуля),
// «не применимо, потому что …» с непустой причиной — либо отказ старта.
// Выключить отсечку по-прежнему можно, но только НАЗВАВ причину, и причина
// видна в обзоре, а не выводится из пустоты. Законный повод у неё есть:
// владелец модели решает У СЕБЯ ([AuthzSelf]), и сетевого соседа, которого
// шторм мог бы уронить, у него нет.
DenyBudget Axis[float64]
// AuthzObserve — приёмник читателя величин КЕША ПОЛОЖИТЕЛЬНЫХ ВЕРДИКТОВ.
// Носитель зовёт его ровно один раз, собрав звено решения, и передаёт
// функцию, читающую счётчики ТОГО звена — вместе с величинами окна вердиктов,
// которое оно спрашивает.
//
// # Почему это поле, а не «корень сам достанет»
//
// Кеш строит носитель ([servicehost]), а диагностическую поверхность держит
// композиционный корень. Достать величины корню НЕ ИЗ ЧЕГО: `Serve` не
// возвращает ни сервера, ни звена — это то же свойство построения, которым
// у сервиса отобрана возможность собрать свою цепочку. Значит переход через
// границу обязан быть объявлен, и объявлен здесь.
//
// # Почему обязательное, а не «поставит тот, кому надо»
//
// Доля попаданий — единственное число, которым отвечают на вопрос «сколько
// даёт кеш», и без неё утверждение о кеше непроверяемо В ОБЕ СТОРОНЫ: кеш,
// не попадающий ни разу, снаружи неотличим от кеша, поглощающего весь поток.
// Пока поле было бы необязательным, шесть процессов из шести не выставляли
// бы его — что и наблюдалось: величины существовали, читателя не имел ни
// один. Умолчанием тут была бы тишина, а тишина о параметре, вокруг которого
// принимают решения, — не умолчание, а отсутствие предмета разговора.
//
// Пустая функция конструктор пройдёт: он судит объявление, а не то, куда
// величины уехали. Вторую половину держит обход дерева
// (`internal/repohygiene.TestEveryCarrierServiceExportsItsVerdictCacheHitRate`):
// сервис у носителя обязан строить коллектор `pkg/authz/authzmetrics`.
AuthzObserve func(read func() authz.Metrics)
// Metrics — реестр, в котором носитель заводит измеритель ЗАДЕРЖКИ
// обслуженного вызова (`pkg/grpcsrv.ServerLatency`).
//
// # Почему поле, а не «сервис померит у себя»
//
// Тот же довод, которым здесь стоят пределы слушателя и круг отправителей:
// пока гистограмму заводит каждый сервис у себя, «не завёл» НЕОТЛИЧИМО от
// «завёл такую же». Отсутствие серии ничего не печатает, слушатель
// поднимается молча, и узнают об этом ровно тогда, когда задержку
// понадобилось посмотреть, — то есть в разборе происшествия, когда данных
// уже не будет.
//
// # Почему ОБЯЗАТЕЛЬНОЕ, а не Axis
//
// У [Axis] тип берут оси, у которых «пусто» бывает ЗАКОННЫМ ответом. Здесь
// такого ответа не существует: всякий слушатель служит вызовы, и у всякого
// вызова есть длительность. Сервиса, которому задержка «не применима», не
// бывает — значит и клетки «не применимо, потому что …» заводить не из чего.
//
// # Почему судится на ЛЮБОЙ посадке, а не только на боевой
//
// Соблазн — освободить dev: там-де метрики никто не скребёт. Освобождение
// снимает предмет целиком. Во-первых, посадка стенда в этом продукте и так
// боевая (правило «production-mode ВЕЗДЕ»), то есть освобождать нечего.
// Во-вторых, стенд разработчика — ровно то место, где задержку меряют перед
// тем, как что-нибудь про неё утверждать; освободив его, мы получили бы
// процесс, у которого «не наблюдает» неотличимо от «наблюдает», в
// единственной посадке, где на это смотрят руками.
//
// Отличие от [Spec.Forwarders] (О1), где строгость ЗАВИСИТ от режима,
// намеренно: там у послабления есть предмет — dev-фикстура законно говорит
// за кого угодно, и цена сужения реальна. Здесь цена соблюдения — одна
// строка (`prometheus.NewRegistry()`), а выгода послабления нулевая.
// Послабление без предмета — это просто выключенный контроль.
//
// # Почему реестр, а не готовый измеритель
//
// Принеси сервис собранный измеритель — он мог бы собрать его над реестром,
// который никто не скребёт, и отказ старта этого не увидел бы. Реестр же
// заводит серии РУКАМИ НОСИТЕЛЯ: несогласованное объявление (то же имя с
// другой размерностью) становится отказом подъёма, а не молчаливой пропажей
// семейства с диагностической поверхности.
//
// Пустой реестр конструктор пройдёт: он судит объявление, а не то, скребёт
// ли кто-нибудь эту поверхность. Вторую половину держит обход дерева
// (`internal/repohygiene.TestEveryGRPCListenerObservesItsLatency`).
Metrics prometheus.Registerer
// HandlingBudget — верхняя граница обработки ОДНОГО вызова: если у входящего
// контекста срока нет либо он дальше границы, носитель ставит свой. Более
// строгий срок вызывающего уважается — окно не расширяется никогда.
//
// Обязательное поле БЕЗ «не применимо», и отличие от [Spec.DenyBudget]
// намеренное: состояния «границы нет» как посадки не существует. Вызов без
// срока держит соединение из ограниченного пула столько, сколько выполняется
// его запрос, поэтому `MaxConns` таких вызовов исчерпывают пул и отказывает
// весь сервис (CWE-770). Сказать «мне граница не нужна» значит сказать «мой
// процесс вправе держать чужой ресурс сколько угодно».
//
// Величина накрывает ТОЛЬКО одиночный вызов. Для серверного стрима она не
// применяется вовсе — у подписки своя ось, [Spec.StreamBudget].
HandlingBudget time.Duration
// StreamBudget — СРОК ЖИЗНИ СЕРВЕРНОГО СТРИМА: столько живёт подписка,
// прежде чем носитель оборвёт её истечением контекста.
//
// # Почему это ОТДЕЛЬНАЯ величина, а не та же самая
//
// «Верхняя граница обработки» и «срок жизни подписки» — разные предметы, и
// общее у них только название единицы. Обоснование [Spec.HandlingBudget]
// говорит про исчерпание пула соединений ЗАПРОСАМИ; подписка держит
// соединение по построению, столько, сколько клиент хочет слушать события, и
// граница, взятая от одиночного вызова, рвала бы её каждые полминуты — причём
// клиент видел бы это как СЕТЕВОЙ СБОЙ, а не как наш отказ. Одно правило на
// два вида вызовов с принципиально разным сроком жизни — это намеренно узкая
// семантика, прочитанная за общий случай.
//
// # Три состояния, и каждое СУДИТСЯ служимым набором
//
// - величина — стрим-цепочка накрывается ЕЮ (не границей обработки);
// - «не применимо, потому что …» — стрим-цепочка границей не накрывается
// вовсе, и это законно ровно пока процесс не служит ни одного серверного
// стрима: появился первый — заявление истекло, старт отказан (О11);
// - не объявлена — [New] отказывает (О10).
//
// Обратная сторона — величина у процесса БЕЗ служимых стримов — тоже находка
// ([servicehost] О11): это проводка без предмета, и без неё ось ловила бы
// форму («что-то объявлено»), а не существо («объявлено про то, что есть»).
//
// # Почему величина обязана ПРЕВОСХОДИТЬ границу обработки
//
// Значение, не большее [Spec.HandlingBudget], возвращает ровно тот разрыв,
// ради устранения которого ось заведена, — только теперь с виду осознанно:
// подписка обрывалась бы не позже, чем истекает потолок ОДИНОЧНОГО вызова.
// Такое объявление неотличимо от «взяли унарную величину», а отличие как раз
// и есть предмет оси, поэтому оно отвергается [New] с названной причиной.
StreamBudget Axis[time.Duration]
// Admission — ПОТОЛОК ТЕМПА и ОДНОВРЕМЕННОСТИ на вызывающего, по одному
// набору на слушатель.
//
// # Почему ось, а не поле с умолчанием
//
// Механизм жил в фундаменте (`pkg/grpcsrv/admission.go`) и был провязан у
// ОДНОГО места сборки сервера из десяти, причём заметить это можно было
// только сплошной переписью. Поле с умолчанием повторило бы историю:
// следующий процесс поднялся бы без потолка и выглядел бы в точности как с
// потолком. Ось убирает «забыл» как исход — сказать про потолок обязан
// каждый, а сказанное судится здесь.
//
// # Почему наборов ДВА
//
// У слушателей разные вызывающие и разная цена ошибки: публичный зовёт
// арендатор через край (ключ — личность конечного пользователя), внутренний
// зовут наши же модули по проверенному сертификату (ключ — личность
// СЕРТИФИКАТА, потому что запрос модуля несёт личности разных арендаторов).
// Общий набор был бы решением, принятым за оба сразу.
//
// # Что здесь законно
//
// Величины — от посадки либо пол платформы
// ([grpcsrv.PlatformPublicAdmission] / [grpcsrv.PlatformInternalAdmission]);
// изъятие `NotApplicable` — только ВНЕ боевого режима (внутрипроцессная
// фикстура, чьи слушатели наружу не выставлены). На боевой посадке «потолка
// не надо» означает «один арендатор вправе занять сервис чтением», и это
// отвергается вместе с остальной боевой строгостью.
Admission Axis[Admission]
// DBSSLMode — режим шифрования до собственной БД (`sslmode`).
//
// # Почему ОСЬ, а не строка (введено вместе с переводом края, задача #1407)
//
// Пустая строка означала бы `disable` — то есть открытый канал, — и на
// боевой посадке отвергалась бы. Процессу БЕЗ собственной базы отвечать на
// этот вопрос нечем: любое значение, которое он подставит, будет
// утверждением о соединении, которого он не открывает. Строкой это
// выражается только правдоподобной константой («require»), и она проходит
// стража всегда, ничего при этом не описывая.
//
// Изъятие ИСТЕКАЕТ САМО: обход дерева требует, чтобы компонент, объявивший
// ось неприменимой, не объявлял ручки `*_DB_SSLMODE` / `ssl-mode`. Заведёт
// базу — заведёт ручку, и изъятие станет находкой.
DBSSLMode Axis[string]
// PublicAddr / InternalAddr — адреса публичного и внутреннего слушателей.
PublicAddr, InternalAddr string
// PublicCreds / InternalCreds — транспорт слушателей. Внутренний НЕ
// освобождён: «internal = доверенный» — запрещённое допущение.
PublicCreds, InternalCreds credentials.TransportCredentials
// Emits — отношения, которые сервис эмитит владельцу прав. Элемент —
// половина решения: приём судится ТРОЙКОЙ (субъект · отношение · тип
// объекта) правилом `proxytuple.ValidateTuple`, а не членством в наборе.
Emits Axis[[]proxytuple.Relation]
// Registers — типы объектов, которые сервис регистрирует у владельца прав.
Registers Axis[[]ObjectType]
// Narrowers — ПРОВОДКА сужателя по методу. Перечень сужаемых методов даёт
// каталог; здесь только реализация, и подставить свою нельзя.
Narrowers Axis[map[MethodFQN]ListNarrower]
// HideExistence — форма отказа для типов, чьё существование скрывается.
HideExistence Axis[map[ObjectType]NotFoundFormat]
// Delivery — происхождение намерений регистрации. Судится осью [Spec.Emits].
Delivery Axis[DeliveryProvenance]
// Existence — порт «есть ли объект в моей базе». Судится осью
// [Spec.HideExistence]: объявлено скрытие — обязателен, не объявлено —
// запрещён.
Existence ExistenceProbe
// BootGate — загрузочный гейт мутаций: создание ресурса принимается только
// при поднятом пути доставки намерений регистрации.
//
// # Почему ось, а не «nil значит гейта нет»
//
// Пока поле было указателем, у него не было ЧИТАТЕЛЯ вовсе: объявлено,
// провязано в трёх сервисах из семи, и ни одна строка носителя его не
// спрашивала. Со стороны обзора это выглядело исполненной работой, а на деле
// в окне, когда дренаж не поднят, ресурсы создавались без доставляемого
// намерения — то есть без владельца. Отличить «гейта нет, потому что очереди
// нет» от «гейт забыли принести» по `nil` было нечем, и это ровно та
// неразличимость, ради которой заведён [Axis].
//
// Судится соседкой в ОДНУ сторону: гейт, принесённый сервисом, который ничего
// не эмитит, — находка (проводка без предмета). Обратная сторона — эмитент
// БЕЗ гейта — сегодня отказом НЕ является, и это названо, а не умолчано:
// на дереве два эмитента гейта не несут (`git grep -l 'bootgate\.'
// services/*/cmd services/*/internal` — vpc, compute, nlb и никто больше),
// поэтому такой отказ закрыл бы им перевод раньше, чем у них появится гейт.
// Пустая клетка при этом молчаливой не остаётся: закрыть ось можно только
// [NotApplicable] с причиной, а причина видна в обзоре.
BootGate Axis[BootGate]
}
type Surface ¶
type Surface struct {
// Service — короткое имя процесса. Попадает в журнал и в текст отказа.
Service ServiceName
// Name — имя поверхности внутри процесса. См. [SurfaceName].
Name SurfaceName
// Mode — посадка процесса. Читается самоотчётом при подъёме: посадка,
// объявленная процессом, — то, что сверяет гейт стенда.
Mode Mode
// Logger — журнал. Ноль резолвится в [slog.Default].
Logger *slog.Logger
// Addr — адрес слушателя, и ТРИ его состояния вместо двух.
//
// Пустая строка в этом дереве означала «поверхность выключена», и означала
// это МОЛЧА: профиль развёртывания, забывший задать эндпоинт, был неотличим
// от посадки, где скрейпа нет намеренно. У шести из одиннадцати слушателей
// это выглядело как ветка `if addr == ""` без единого слова о том, законно ли
// сюда попадать; у одного (реестр) рядом стояло предупреждение, и оно же
// доказывает, что различие кому-то было нужно.
//
// Поэтому: [Value] с непустым адресом — поднимаем; [NotApplicable] с
// причиной — выключено ОБЪЯВЛЕНИЕМ, причина видна в журнале и в обзоре;
// необъявленная ось и `Value("")` — отказ (Н3).
Addr Axis[string]
// Handler — что обслуживается. Обязателен при включённой поверхности (Н8).
Handler http.Handler
// TLS — транспорт слушателя. `nil` — открытый текст.
//
// Отказа «боевая посадка и открытый текст» здесь НЕТ, и это названо, а не
// умолчано: у внешних поверхностей этого продукта TLS терминируется на входе
// кластера, то есть шифрование хопа — факт РАЗВЁРТЫВАНИЯ, которого процесс не
// знает. Отказ, выведенный из незнания, отказал бы верной посадке.
TLS *tls.Config
// Reach — откуда досягаема. Обязательна (Н4).
Reach SurfaceReach
// Auth — РЕШЕНИЕ ОБ АУТЕНТИФИКАЦИИ, объявленное данными.
//
// Три состояния, и среднее — весь смысл оси:
//
// - [Value] — аутентификация есть, и значение НАЗЫВАЕТ ЧЕМ;
// - [NotApplicable] — её нет, и причина названа. Так объявляется
// задокументированное исключение (метрики; зеркало публичных ключей
// проверки, `security.md` §AuthN+AuthZ ВЕЗДЕ);
// - не объявлена — отказ старта (Н5).
//
// Почему отказ, а не умолчание в любую сторону: умолчание «нет» дало бы
// незащищённую поверхность, поднятую молча, а умолчание «есть» — ложное
// объявление о защите, которой нет. Различить забытое от снятого осознанно
// можно только тем, что снятие ТРЕБУЕТ слов, а забытое их не имеет.
Auth Axis[SurfaceAuthMech]
// ReadHeaderBudget — сколько ждать ЗАГОЛОВОК запроса. Обязателен (Н9): без
// него медленный отправитель держит соединение сколько угодно, ничего не
// прислав, — и поверхность падает от одного клиента.
ReadHeaderBudget time.Duration
// RequestBudget — потолок чтения и записи ВСЕГО запроса.
//
// Ось, а не обязательное поле, потому что у потоковой поверхности его быть
// НЕ ДОЛЖНО: слой образа едет минутами, и потолок записи разорвал бы
// исправную передачу. «Не применимо, потому что …» — законное объявление,
// а необъявленность — отказ (Н10). Величина, не большая [ReadHeaderBudget],
// бессмысленна и тоже отказ (Н11): потолок запроса не бывает уже потолка его
// заголовка.
RequestBudget Axis[time.Duration]
// IdleBudget — сколько живёт ПРОСТАИВАЮЩЕЕ keep-alive соединение.
//
// Обязателен (Н12) и отличается от [RequestBudget] предметом: там потолок
// работы, здесь — потолок безделья. Ноль в stdlib означает «взять потолок
// чтения», а при неназванном потолке чтения — «не ограничивать вовсе»;
// то есть незаполненное поле МОЛЧА снимало бы ограничение ровно на той
// поверхности, где оно нужнее всего (потоковой, у которой потолка чтения нет
// намеренно).
IdleBudget time.Duration
// ShutdownBudget — сколько ждать завершения начатых запросов при гашении.
//
// Обязателен (Н13). Гашение без срока — не гашение: две поверхности этого
// дерева гасились контекстом БЕЗ срока, то есть процесс на остановке ждал
// последнего скрейпа неограниченно, и уносил его только внешний убийца.
ShutdownBudget time.Duration
}
Surface — то, что сервис объявляет о ПОВЕРХНОСТИ, которая не gRPC.
Полей `*http.Server`, `net.Listener` и `func() error` здесь НЕТ, и это тот же механизм, что в Spec: сервер невозможно принести, поэтому невозможно оставить его непогашенным. Единственное, что вызывающий получает от профиля, — исход, а не объект.
type SurfaceAuthMech ¶
type SurfaceAuthMech string
SurfaceAuthMech — ЧЕМ поверхность аутентифицирует запрос.
Строка, а не перечисление, намеренно: механизмы здесь принадлежат чужим протоколам (подпись вебхука провайдера, ключ служебной учётки, Bearer против зеркала ключей), и закрытое перечисление пришлось бы дополнять при каждом новом соседе. Судится не значение, а ФАКТ объявления — плюс пара с Surface.Reach.
type SurfaceDescriptor ¶
type SurfaceDescriptor struct {
// contains filtered or unexported fields
}
SurfaceDescriptor — ПРИНЯТАЯ Surface.
Поля неэкспортируемые по той же причине, что у Descriptor: собранный литералом профиль не проходил ни одного отказа, и поднимать по нему слушатель значит не проверить ничего.
func NewSurface ¶
func NewSurface(s Surface) (SurfaceDescriptor, error)
NewSurface принимает Surface либо отказывает, НАЗЫВАЯ ВСЕ находки разом.
Отказы (Н1…Н13) — свойства самого объявления: служимого набора у не-gRPC поверхности нет, выводить из неё нечего, поэтому второго места, где её судят, не заводится. Этим профиль проще gRPC-контура, где половина отказов живёт в носителе.
func (SurfaceDescriptor) Accepted ¶
func (d SurfaceDescriptor) Accepted() bool
Accepted — прошёл ли профиль конструктор.
func (SurfaceDescriptor) AuthStatement ¶
func (d SurfaceDescriptor) AuthStatement() string
AuthStatement — решение об аутентификации ОДНОЙ строкой для журнала и текста отказа.
Собирается здесь, а не у каждого вызывающего: пока строку собирал вызывающий, «аутентификации нет» и «про аутентификацию не написали» выглядели в журнале одинаково — то есть самоотчёт терял ровно то различие, ради которого ось заведена.
func (SurfaceDescriptor) DisabledBecause ¶
func (d SurfaceDescriptor) DisabledBecause() string
DisabledBecause — объявленная причина, по которой поверхность не поднимается. Пусто, если она поднимается либо профиль не принят.
func (SurfaceDescriptor) Enabled ¶
func (d SurfaceDescriptor) Enabled() bool
Enabled — поднимается ли поверхность. Ложно ровно тогда, когда адрес объявлен неприменимым; на непринятом профиле — тоже ложно.
func (SurfaceDescriptor) Spec ¶
func (d SurfaceDescriptor) Spec() Surface
Spec отдаёт принятые значения (копия).
func (SurfaceDescriptor) UnderTLS ¶
func (d SurfaceDescriptor) UnderTLS() bool
UnderTLS — идёт ли провод поверхности под транспортом.
Отвечает про ПРОВОД, а не про то, поднимается ли поверхность: у объявления, где адрес не задан, а транспорт объявлен, ответы разные, и слить их значило бы отчитываться о защите того, чего нет.
На непринятом профиле — ложь, а не «не знаю»: профиль, собранный литералом, не проходил ни одного отказа, и утверждать о его транспорте нечего. Строгая сторона выбрана намеренно — забывчивость обязана ронять гейт посадки, а не проходить его молча.
Заведено для самоотчёта о посадке (pkg/observability, own_rest_*_tls): величина оси обязана выводиться из ТОГО ЖЕ объявления, по которому поверхность поднимается, иначе самоотчёт и доклад поверхности при подъёме расходятся молча.
type SurfaceName ¶
type SurfaceName string
SurfaceName — имя поверхности В ЖУРНАЛЕ И В ТЕКСТЕ ОТКАЗА («метрики», «плоскость данных OCI»).
Отдельное от ServiceName имя нужно потому, что у одного процесса таких поверхностей бывает четыре: отказ «kaname: слушатель не поднимается» оператору не адресует ничего.
type SurfaceReach ¶
type SurfaceReach uint8
SurfaceReach — ОТКУДА к поверхности можно достучаться.
Ось существует ради ОДНОГО отказа (Н7): «аутентификации нет» защитимо на поверхности, доступной только внутри кластера, и не защитимо на той, до которой дотягивается кто угодно. Без оси оба случая выглядели бы одинаково — как объявленное решение, — и отличить осознанно снятую аутентификацию диагностики от снятой на плоскости данных было бы нечем.
const ( // ReachClusterInternal — поверхность выставлена только на внутренний Service. ReachClusterInternal SurfaceReach // ReachExternal — до поверхности дотягиваются извне кластера (через вход). ReachExternal )
func (SurfaceReach) String ¶
func (r SurfaceReach) String() string
String — представление для журнала. Нулевое значение названо СВОИМ именем, а не уходит в общую ветку «прочее»: неназванное состояние в журнале неотличимо от названного неверно.