errors

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 10, 2026 License: AGPL-3.0 Imports: 3 Imported by: 0

Documentation

Overview

Package errors — pgx-free sentinel error family for kaname.

Every use-case returns a sentinel-family error (ErrNotFound / ErrAlreadyExists / ErrFailedPrecondition / ErrInvalidArg / ErrInternal / ErrUnavailable / ErrPermissionDenied / ErrUnauthenticated / ErrAborted); the handler layer maps to a gRPC code (shared.MapRepoErr). The within-service invariant forbids software-precheck for within-service refs — a within-service violation is detected by catching the pgx SQLSTATE and wrapping it in the appropriate sentinel. That SQLSTATE→sentinel bridge (which needs pgx/pgconn) deliberately lives in the ADAPTER layer (internal/repo/kaname/pg/pgmaperr.go), NOT here: this package stays pgx-free so the ~40 use-case/handler files that import it for the sentinels never pull pgx into their build closure (architecture.md dependency-rule).

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrNotFound           = stderrors.New("not found")
	ErrAlreadyExists      = stderrors.New("already exists")
	ErrFailedPrecondition = stderrors.New("failed precondition")
	ErrInvalidArg         = stderrors.New("invalid argument")
	ErrInternal           = stderrors.New("internal")
	ErrUnavailable        = stderrors.New("unavailable")
	// ErrPermissionDenied — caller is authenticated but lacks the required
	// permission / is not the designated actor (e.g. JitPending
	// non-designated-approver, ComplianceReport FGA gate). Maps to gRPC
	// PERMISSION_DENIED.
	ErrPermissionDenied = stderrors.New("permission denied")
	// ErrUnauthenticated — caller's token does not satisfy the required
	// authentication assurance (step-up acr). Maps to gRPC UNAUTHENTICATED.
	ErrUnauthenticated = stderrors.New("unauthenticated")

	// ErrQuotaExceeded — потолок на число ресурсов вида у носителя достигнут:
	// строка учёта есть, место выбрано. Маппится в RESOURCE_EXHAUSTED (край →
	// HTTP 429) с признаком `QUOTA_EXCEEDED`; администратору требуется ПОДНЯТЬ
	// предел.
	ErrQuotaExceeded = stderrors.New("resource count quota exceeded")

	// ErrQuotaNotProvisioned — потолок не назван НИ НА ОДНОЙ области видимости.
	//
	// Отдельный sentinel, а не оттенок предыдущего, и это не стиль: сведи их в
	// один — и читающий «место кончилось» пойдёт искать, что понизить, там, где
	// ничего не назначено. Маппится в FAILED_PRECONDITION с признаком
	// `QUOTA_NOT_PROVISIONED`: ввод арендатора корректен, не выполнено
	// предусловие ПЛАТФОРМЫ.
	ErrQuotaNotProvisioned = stderrors.New("resource count quota not provisioned")

	// ErrQuotaRateExceeded — потолок на ТЕМП заведения достигнут: за текущее окно
	// принято столько, сколько названо величиной.
	//
	// Отдельный sentinel, а не оттенок `ErrQuotaExceeded`, и различие несущее:
	// отказ по объёму ТЕРМИНАЛЕН (лечится поднятием предела), отказ по темпу
	// ВРЕМЕНЕН (лечится ожиданием следующего окна). Свести их в один — значит
	// отправить арендатора ждать того, что не наступит, либо просить поднять
	// предел, который он не исчерпал. Маппится в RESOURCE_EXHAUSTED (край → HTTP
	// 429) с признаком `QUOTA_RATE_EXCEEDED`.
	ErrQuotaRateExceeded = stderrors.New("admission rate quota exceeded")

	// ErrReferenceMissing / ErrReferenceInUse — ДВЕ ПРОТИВОПОЛОЖНЫЕ стороны
	// одного нарушения ссылочной целостности, и они разведены намеренно.
	//
	// Прежде обе приходили одним текстом «referenced resource not found or still
	// in use». Первая лечится СОЗДАНИЕМ ссылаемого ресурса, вторая —
	// ОСВОБОЖДЕНИЕМ ссылок; вызывающий выбрать не мог, потому что различающего
	// признака у него не было ни в тексте, ни в коде, ни в деталях. Отказ,
	// объединяющий взаимоисключающие причины, следующего шага не восстанавливает
	// by construction: любой выбранный шаг верен ровно в половине случаев.
	//
	// Оба ВЛОЖЕНЫ в ErrFailedPrecondition, а не заменяют его: код отказа не
	// меняется (состояние ресурсов не позволяет операцию — это по-прежнему
	// FAILED_PRECONDITION), меняется только различимость. Признак полосы едет
	// в `google.rpc.ErrorInfo` (`shared.referenceRefusal`), потому что разбор
	// прозы вызывающим запрещён конвенцией.
	ErrReferenceMissing = fmt.Errorf("%w: referenced resource missing", ErrFailedPrecondition)
	ErrReferenceInUse   = fmt.Errorf("%w: resource still referenced", ErrFailedPrecondition)

	// ErrAborted — a transient concurrency conflict the caller can retry (the
	// operation was aborted, typically a transaction serialization failure).
	// Maps to gRPC ABORTED, the idiomatic "retry the transaction" code — unlike
	// FAILED_PRECONDITION, which tells a well-behaved client NOT to retry.
	ErrAborted = stderrors.New("aborted")

	// ErrSelfRevoke — caller tries to revoke its own cluster admin grant
	// (self-protection). Maps to gRPC FAILED_PRECONDITION with
	// the Kachō text "cannot revoke own cluster admin grant".
	//
	// CHECK constraint cannot express this (constraint doesn't know caller —
	// runtime-property), so the guard lives in the SQL WHERE-clause of
	// the CAS UPDATE in ClusterAdminGrantWriter.Revoke.
	ErrSelfRevoke = stderrors.New("self revoke forbidden")

	// ErrLastAdmin — caller tries to revoke the last remaining active
	// cluster admin (lock-out protection). Maps to gRPC
	// FAILED_PRECONDITION with the Kachō text "cannot revoke last active
	// cluster admin".
	//
	// Implemented via single-statement CAS UPDATE with subquery
	// `(SELECT count(*) FROM cluster_admin_grants WHERE granted_until IS NULL) > 1`
	// — atomic, no separate SELECT-then-UPDATE race window.
	ErrLastAdmin = stderrors.New("last admin revoke forbidden")

	// ErrMembershipCarriesRights — членство снимают, пока на него опирается живая
	// выдача. Отложенный триггер `membership_carrying_rights_is_kept` (миграция
	// 472002) отвергает это НА КОММИТЕ. Маппится в FAILED_PRECONDITION — так
	// называет полосу контракт RemoveFromAccount.
	//
	// # Почему СВОЙ признак, а не общий ErrFailedPrecondition
	//
	// Контракт обещает отказ «с перечисленными выдачами», а перечень добывается
	// ОТДЕЛЬНЫМ чтением: триггер отложенный, и к моменту отказа транзакция мертва
	// — спросить у неё, что помешало, нельзя ни одним запросом. Значит у отказа
	// есть потребитель (use-case исключения), который обязан отличить ЭТУ полосу
	// от всех прочих предусловий, чтобы знать, что именно дочитывать. Общий
	// признак этого не даёт: под ним лежат десятки разных отказов, и «дочитать
	// выдачи» на любом из них было бы догадкой.
	//
	// Признак уезжает и КЛИЕНТУ — токеном `MEMBERSHIP_CARRIES_RIGHTS` в
	// `google.rpc.ErrorInfo` (`shared.membershipRefusal`), поэтому полоса
	// различается машинно и не зависит от языка прозы.
	//
	// # Это СПЕЦИАЛИЗАЦИЯ предусловия, а не полоса рядом с ним
	//
	// `errors.Is(err, ErrFailedPrecondition)` на нём по-прежнему ИСТИНА, и это
	// несущее свойство, а не удобство: на общий признак ветвятся пять мест
	// не-тестового дерева и не одна проба. Заведи полосу рядом — и каждое из них
	// молча перестало бы узнавать этот отказ, причём ни одно не сказало бы об
	// этом: они просто ушли бы в ветку «прочее».
	//
	// Текст СОВПАДАЕТ с общим намеренно. Он служит префиксом, который снимает
	// `StripSentinel`, и собственный текст потребовал бы записи в её перечне —
	// то есть второго места об одном предмете. Наружу этот текст не уезжает
	// никогда: он снимается до отправки.
	ErrMembershipCarriesRights error = specialisedSentinel{/* contains filtered or unexported fields */}

	// ErrUnknownResourceType — регистрация ресурса назвала тип объекта, у
	// которого нет ЖИВОЙ строки в каталоге ресурсов платформы.
	//
	// # Почему это ОТДЕЛЬНАЯ полоса, а не общий неверный аргумент
	//
	// У отказа есть потребитель, и ему мало кода. Путь регистрации отвергает
	// вход по двум разным причинам: грамматика кортежа (пусто, пробел, два
	// двоеточия) и отсутствие референта у типа. Первая — ошибка формы, вторая —
	// утверждение о КАТАЛОГЕ, и вызывающему она говорит другое: «форма верна,
	// такого ресурса у платформы нет; прочти каталог». Поле в отказе называет
	// именно эту полосу, поэтому её надо узнать до того, как статус собран.
	//
	// # Это СПЕЦИАЛИЗАЦИЯ неверного аргумента, а не полоса рядом с ним
	//
	// `errors.Is(err, ErrInvalidArg)` на нём остаётся ИСТИНОЙ намеренно: на общий
	// признак ветвится и `shared.MapRepoErr`, и пробы; завести полосу рядом
	// значило бы молча увести отказ в ветку «прочее» — то есть в непрозрачный
	// INTERNAL вместо названного поля.
	//
	// Текст СОВПАДАЕТ с общим по той же причине, что у соседа выше: он служит
	// префиксом, который снимает `StripSentinel`, и наружу не уезжает.
	ErrUnknownResourceType error = specialisedSentinel{/* contains filtered or unexported fields */}
)

Sentinel error family (parity with services/vpc/internal/repo/helpers.Err*).

Functions

func StripSentinel

func StripSentinel(err error) string

StripSentinel — extracts the "useful" part of the message (after "sentinel: ") so the handler layer can show the client the canonical Kachō text without the internal prefix (parity with services/vpc/internal/apps/kacho/shared/serviceerr/map.go::stripSentinel).

Имя признака снимается ГДЕ БЫ ОНО НИ СТОЯЛО, а не только в начале

Прежде имя снималось префиксом ВСЕГО сообщения. Вызывающий, добавивший свой контекст перед отказом репозитория (`fmt.Errorf("%w: %s: %w", ErrWriteFailed, name, err)`), ставит свой текст ВПЕРЁД — префикс перестаёт совпадать, и служебное имя остаётся в середине:

moduleroles: writing the declared role failed: vpc.network.admin:
failed precondition: resources: probeWithdrawn is not a live platform resource

Класс отказа уже назван кодом статуса и признаком полосы, поэтому в тексте он лишний и вдобавок сообщает имя внутренней переменной. Чистота текста держалась не свойством функции, а тем, что до сих пор никто не оборачивал (#1889).

Вырезается ровно то, что произвела обёртка признака, а не совпавшие слова

Наивная починка — поиск слов признака по тексту — испортила бы отказ, который называет эти слова ПО ДЕЛУ (`name: "failed precondition: x" is not a valid role name`: строку прислал сам вызывающий). Поэтому место реза берётся из ЦЕПОЧКИ: находится узел, чей собственный `Error()` начинается именем признака, и в сообщении замещается ровно его текст. Узел, которого в сообщении нет (свой `Error()`, не склеивающий вложенное), оставляет текст нетронутым.

func Wrapf

func Wrapf(sentinel error, format string, args ...any) error

Wrapf — standard fmt.Errorf-style wrapper with an explicit sentinel. Use in use-cases: `return errors.Wrapf(errors.ErrNotFound, "Account %s not found", id)`.

Types

This section is empty.

Jump to

Keyboard shortcuts

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