Help us shape the future of Go! Take the 2026 Go Developer Survey

servicecontract

package
v1.16.0 Latest Latest
Warning

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

Go to latest
Published: Oct 30, 2026 License: Apache-2.0 Imports: 15 Imported by: 1

Documentation

Overview

Package servicecontract — то, что сервис объявляет О СЕБЕ, чтобы носитель входящего пути (`pkg/servicehost`) поднял его контур работы с владельцем прав.

Почему это ДАННЫЕ, а не сборка

Семь сервисов собирали цепочку звеньев каждый у себя, и порядок держался тем, что авторы написали одинаковое. Здесь сервис приносит ЗНАЧЕНИЯ — кто он, в каком режиме, каким транспортом говорит с владельцем прав, что эмитит, что сужает, что скрывает, — и ни одного поля интерсепторного типа. Невыразимость чужой цепочки и есть механизм: восьмой сервис, желающий свой порядок звеньев, не «нарушит правило» — ему не на чем его записать.

Что этот пакет НЕ решает

Он решает, ЧТО МОЖНО ЗАПИСАТЬ. Согласованность записанного с тем, что процесс РЕАЛЬНО служит, решает соседний пакет фундамента `servicehost` — там живут отказы, которым нужен служимый набор RPC и выведенный из него каталог прав. Применение записанного не закрывается ни тем, ни другим: его держат гейты по дереву. Смешивать эти три уровня нельзя — выдавать гейт за свойство построения значит объявить закрытым то, что открыто.

ДВЕ ПОЛОВИНЫ ОДНОГО [Spec], и их различие несущее

Первая — ПОСАДКА: режим, шифрование до собственной базы, круг отправителей переданной личности. Она есть у КАЖДОГО развёрнутого процесса, и ровно её требует ban #16. Вторая — ПРОВОДКА НОСИТЕЛЯ: что эмитить, что сужать, что скрывать, какие пределы ставить звеньям. Её читает только носитель.

Пока половины были неразличимы, принять дескриптор мог лишь тот, кто приносит обе, — то есть процессы с собственным контуром через единый источник не проходили вовсе. Различает их Spec.OwnContour; посадка судится при любом его значении, проводка при непустом ЗАПРЕЩЕНА.

Поля `Domain` здесь НЕТ намеренно

Домен ВЫВОДИТСЯ из имён зарегистрированных gRPC-сервисов (носитель снимает их у самого сервера). Объявление ввело бы второй источник, опечатка в котором молча выбирает ноль строк каталога — отказ старта такое поймает, но диагностика хуже, а второго источника не должно быть в принципе.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Modes

func Modes() []string

Modes — допустимые написания посадки, в порядке нарастания строгости.

Существует ради ТЕКСТОВ ОТКАЗА: страж старта, перечисляющий словарь своими руками, есть второе место об одном предмете, и расходится оно первым. Копия возвращается вызывающему намеренно — перечень не должен меняться из-под дома.

func Verified

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

func NotApplicable[T any](because string) Axis[T]

NotApplicable объявляет ось неприменимой С ПРИЧИНОЙ. Пустая причина объявлением не является — Axis.Declared на ней ложен, и New отказывает.

func Value

func Value[T any](v T) Axis[T]

Value объявляет ось значением. Пустое ЗНАЧЕНИЕ (пустой срез, пустая карта) — законное объявление и отличается от необъявленности: сервис вправе сказать «набор пуст» явно.

func (Axis[T]) Declared

func (a Axis[T]) Declared() bool

Declared сообщает, сказал ли автор про эту ось ХОТЬ ЧТО-ТО. Это и есть предикат О10: незаявленная ось не доживает до обслуживания запроса.

func (Axis[T]) Get

func (a Axis[T]) Get() (T, bool)

Get отдаёт значение оси; ok ложен, если ось объявлена неприменимой или не объявлена вовсе.

func (Axis[T]) NotApplicableBecause

func (a Axis[T]) NotApplicableBecause() (string, bool)

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

func ParseMode(s string) (Mode, error)

ParseMode разбирает строку окружения. Неизвестное значение — ОШИБКА, а не откат к умолчанию: умолчание здесь есть выбор посадки, сделанный никем.

Сравнение ТОЧНОЕ — ни регистр, ни обрамляющие пробелы не прощаются, и это решение дома, а не недосмотр. Смягчение выглядит вежливым, а означает, что посадка выбирается не тем, что написано в профиле, а тем, что удалось из этого вывести; отказ же называет ручку и весь допустимый набор, поэтому чинится за секунды. Три сервиса прощали регистр и пробелы своими копиями словаря — и вместе с этим прощали `prod`, `development` и пустую строку, которых не принимал никто.

Перечень для текста отказа берётся у словаря, а не пишется здесь: страж, перечисляющий набор своими руками, есть второе место об одном предмете.

func (Mode) IsProduction

func (m Mode) IsProduction() bool

IsProduction — боевая ли посадка. Единственный предикат, по которому принимаются решения о строгости; сравнивать со строками на местах запрещено.

func (Mode) String

func (m Mode) String() string

String — представление для журнала и самоотчёта о посадке. Читает ТОТ ЖЕ словарь, что и ParseMode: обратимость разбора становится свойством построения, а не совпадением двух свичей.

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) Addr

func (e PeerEdge) Addr() string

Addr — адрес соседа.

func (PeerEdge) Creds

Creds — транспорт ребра.

func (PeerEdge) Declared

func (e PeerEdge) Declared() bool

Declared — объявлено ли ребро целиком.

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 — ПРИЧИНА, по которой контур входящего пути этого процесса
	// поднимает не носитель (соседний пакет фундамента `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 — представление для журнала. Нулевое значение названо СВОИМ именем, а не уходит в общую ветку «прочее»: неназванное состояние в журнале неотличимо от названного неверно.

Jump to

Keyboard shortcuts

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