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 ¶
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") // 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 ¶
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()`, не склеивающий вложенное), оставляет текст нетронутым.
Types ¶
This section is empty.