servicecontract

package
v1.3.1 Latest Latest
Warning

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

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

Documentation

Overview

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

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

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

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

Он решает, ЧТО МОЖНО ЗАПИСАТЬ. Согласованность записанного с тем, что процесс РЕАЛЬНО служит, решает github.com/PRO-Robotech/kacho/pkg/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 — ПРИЧИНА, по которой контур входящего пути этого процесса
	// поднимает не носитель ([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 — представление для журнала. Нулевое значение названо СВОИМ именем, а не уходит в общую ветку «прочее»: неназванное состояние в журнале неотличимо от названного неверно.

Jump to

Keyboard shortcuts

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