* refactor (controller): Use a grpc server for the grpc-gateway This starts a grpc server for the grpc-gateway. The new gateway server uses an in-memory listener which also supports "dialing", so the http proxy can connect to the in-memory listener. This change allows us to use all grpc server features and was in part motivated by a need for an interceptor of inbound requests. With that said, having a "proper" grpc server for the gateway will provide a lot more flexibility and capabilities moving forward. The request context will still be verified via middleware in the http proxy (see wrapHandlerWithCommonFuncs). This verified context will be serialized into a protobuf and sent along to the grpc-gateway via metadata where an interceptor will add the verified request info to the request's context (see requestCtxInterceptor). The interceptor will verify the serialized request info via a shared ticket with it's companion in-memory http proxy. FYI: the ticket is simply random base62 string (see db.NewPrivateId). This change also necessitated a minor change to services. We need to return an empty response instead of nil, nil when there are no errors and the response is empty. * refactor (handlers): Rename gRPC Gateway filter Originally, this was called an OutgoingInterceptor, which is an unfortunate name since it's not an interceptor. Now that we're refactoring the gateway stuff and have proper interceptors, renaming this should remove ambiguity * refactor: Rework how 204 with empty msg is handled Recent changes to grpc gateway has necessitated a change to how 204s are handled by the gateway * tests (bats): Check status codes for delete operations
341 lines
9.5 KiB
Go
341 lines
9.5 KiB
Go
package errors
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"runtime"
|
|
"strings"
|
|
|
|
pberrors "github.com/hashicorp/boundary/internal/gen/errors"
|
|
"github.com/hashicorp/boundary/internal/observability/event"
|
|
"github.com/jackc/pgconn"
|
|
)
|
|
|
|
// Op represents an operation (package.function).
|
|
// For example iam.CreateRole
|
|
type Op string
|
|
|
|
// Err provides the ability to specify a Msg, Op, Code and Wrapped error.
|
|
// We've chosen Err over Error for the identifier to support the easy embedding of Errs.
|
|
// Errs can be embedded without a conflict between the embedded Err and Err.Error().
|
|
type Err struct {
|
|
// Code is the error's code, which can be used to get the error's
|
|
// errorCodeInfo, which contains the error's Kind and Message
|
|
Code Code
|
|
|
|
// Msg for the error
|
|
Msg string
|
|
|
|
// Op represents the operation raising/propagating an error and is optional.
|
|
// Op should be formatted as "package.func" for functions, while methods should
|
|
// include the receiver type in parentheses "package.(type).func"
|
|
Op Op
|
|
|
|
// Wrapped is the error which this Err wraps and will be nil if there's no
|
|
// error to wrap.
|
|
Wrapped error
|
|
}
|
|
|
|
// E creates a new Err with provided code and supports the options of:
|
|
//
|
|
// * WithoutEvent - allows you to specify that an error event should not be
|
|
// emitted.
|
|
//
|
|
// * WithOp() - allows you to specify an optional Op (operation).
|
|
//
|
|
// * WithMsg() - allows you to specify an optional error msg, if the default
|
|
// msg for the error Code is not sufficient.
|
|
//
|
|
// * WithWrap() - allows you to specify an error to wrap.
|
|
// If the wrapped error is a boundary domain error, the wrapped error code
|
|
// will be used as the returned error's code.
|
|
//
|
|
// * WithCode() - allows you to specify an optional Code, this code will be prioritized
|
|
// over a code used from WithWrap().
|
|
func E(ctx context.Context, opt ...Option) error {
|
|
// nil ctx is allowed and tested for in unit tests
|
|
opts := GetOpts(opt...)
|
|
var code Code
|
|
|
|
// check if options includes a wrapped error to take code from
|
|
var err *Err
|
|
if As(opts.withErrWrapped, &err) {
|
|
code = err.Code
|
|
}
|
|
|
|
// if options include withCode prioritize using that code
|
|
// even if one was set via wrapped error above
|
|
if opts.withCode != Unknown {
|
|
code = opts.withCode
|
|
}
|
|
|
|
err = &Err{
|
|
Code: code,
|
|
Op: opts.withOp,
|
|
Wrapped: opts.withErrWrapped,
|
|
Msg: opts.withErrMsg,
|
|
}
|
|
if opts.withoutEvent {
|
|
return err
|
|
}
|
|
|
|
{
|
|
// events require an Op, but we don't want to change the error specified
|
|
// by the caller. So we'll build a new event and conditionally set a
|
|
// reasonable Op based on the call stack.
|
|
eventErr := &Err{
|
|
Code: err.Code,
|
|
Op: err.Op,
|
|
Wrapped: err.Wrapped,
|
|
Msg: err.Msg,
|
|
}
|
|
if eventErr.Op == "" {
|
|
const has = "github.com/hashicorp/boundary/internal/errors."
|
|
const trim = "github.com/hashicorp/boundary/"
|
|
for i := 0; i < 5; i++ {
|
|
pc, _, _, ok := runtime.Caller(i)
|
|
details := runtime.FuncForPC(pc)
|
|
if ok && details != nil {
|
|
if strings.HasPrefix(details.Name(), has) {
|
|
continue
|
|
}
|
|
eventErr.Op = Op(strings.TrimPrefix(details.Name(), trim))
|
|
break
|
|
}
|
|
}
|
|
if eventErr.Op == "" {
|
|
eventErr.Op = "unknown operation"
|
|
}
|
|
}
|
|
event.WriteError(ctx, event.Op(eventErr.Op), eventErr)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// New creates a new Err with provided code, op and msg
|
|
// It supports the options of:
|
|
//
|
|
// * WithWrap() - allows you to specify an error to wrap
|
|
func New(ctx context.Context, c Code, op Op, msg string, opt ...Option) error {
|
|
if c != Unknown {
|
|
opt = append(opt, WithCode(c))
|
|
}
|
|
if op != "" {
|
|
opt = append(opt, WithOp(op))
|
|
}
|
|
if msg != "" {
|
|
opt = append(opt, WithMsg(msg))
|
|
}
|
|
return E(ctx, opt...)
|
|
}
|
|
|
|
// Wrap creates a new Err from the provided err and op,
|
|
// preserving the code from the originating error.
|
|
// It supports the options of:
|
|
//
|
|
// * WithMsg() - allows you to specify an optional error msg, if the default
|
|
// msg for the error Code is not sufficient.
|
|
func Wrap(ctx context.Context, e error, op Op, opt ...Option) error {
|
|
if op != "" {
|
|
opt = append(opt, WithOp(op))
|
|
}
|
|
if e != nil {
|
|
// TODO: once db package has been refactored to only return domain errors,
|
|
// this convert can be removed
|
|
err := Convert(e)
|
|
if err != nil {
|
|
// wrap the converted error
|
|
e = err
|
|
}
|
|
opt = append(opt, WithWrap(e))
|
|
}
|
|
|
|
return E(ctx, opt...)
|
|
}
|
|
|
|
// EDeprecated is the legacy version of E which does not
|
|
// create an event. Please refrain from using this.
|
|
// When all calls are moved from EDeprecated to
|
|
// E, please update ICU-1883
|
|
func EDeprecated(opt ...Option) error {
|
|
return E(context.TODO(), opt...)
|
|
}
|
|
|
|
// NewDeprecated is the legacy version of New which does not
|
|
// create an event. Please refrain from using this.
|
|
// When all calls are moved from NewDeprecated to
|
|
// New, please update ICU-1883
|
|
func NewDeprecated(c Code, op Op, msg string, opt ...Option) error {
|
|
return New(context.TODO(), c, op, msg, opt...)
|
|
}
|
|
|
|
// WrapDeprecated is the legacy version of New which does not
|
|
// create an event. Please refrain from using this.
|
|
// When all calls are moved from WrapDeprecated to
|
|
// New, please update ICU-1884
|
|
func WrapDeprecated(e error, op Op, opt ...Option) error {
|
|
return Wrap(context.TODO(), e, op, opt...)
|
|
}
|
|
|
|
// Convert will convert the error to a Boundary *Err (returning it as an error)
|
|
// and attempt to add a helpful error msg as well. If that's not possible, it
|
|
// will return nil
|
|
func Convert(e error) *Err {
|
|
ctx := context.TODO()
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
// TODO instead of casting the error here, we should do an As.
|
|
// Currently doing an As loses any additional context added by non-refactored packages
|
|
// that are still wrapping with stdlib
|
|
if err, ok := e.(*Err); ok {
|
|
return err
|
|
}
|
|
var pgxError *pgconn.PgError
|
|
if As(e, &pgxError) {
|
|
if pgxError.Code[0:2] == "23" { // class of integrity constraint violations
|
|
switch pgxError.Code {
|
|
case "23505": // unique_violation
|
|
return E(ctx, WithoutEvent(), WithMsg(pgxError.Message), WithWrap(E(ctx, WithoutEvent(), WithCode(NotUnique), WithMsg("unique constraint violation")))).(*Err)
|
|
case "23502": // not_null_violation
|
|
msg := fmt.Sprintf("%s must not be empty", pgxError.ColumnName)
|
|
return E(ctx, WithoutEvent(), WithMsg(msg), WithWrap(E(ctx, WithoutEvent(), WithCode(NotNull), WithMsg("not null constraint violated")))).(*Err)
|
|
case "23514": // check_violation
|
|
msg := fmt.Sprintf("%s constraint failed", pgxError.ConstraintName)
|
|
return E(ctx, WithoutEvent(), WithMsg(msg), WithWrap(E(ctx, WithoutEvent(), WithCode(CheckConstraint), WithMsg("check constraint violated")))).(*Err)
|
|
default:
|
|
return E(ctx, WithoutEvent(), WithCode(NotSpecificIntegrity), WithMsg(pgxError.Message)).(*Err)
|
|
}
|
|
}
|
|
switch pgxError.Code {
|
|
case "42P01":
|
|
return E(ctx, WithoutEvent(), WithCode(MissingTable), WithMsg(pgxError.Message)).(*Err)
|
|
case "42703":
|
|
return E(ctx, WithoutEvent(), WithCode(ColumnNotFound), WithMsg(pgxError.Message)).(*Err)
|
|
case "P0001":
|
|
return E(ctx, WithoutEvent(), WithCode(Exception), WithMsg(pgxError.Message)).(*Err)
|
|
}
|
|
}
|
|
// unfortunately, we can't help.
|
|
return nil
|
|
}
|
|
|
|
// Info about the Err
|
|
func (e *Err) Info() Info {
|
|
if e == nil {
|
|
return errorCodeInfo[Unknown]
|
|
}
|
|
return e.Code.Info()
|
|
}
|
|
|
|
// Error satisfies the error interface and returns a string representation of
|
|
// the Err
|
|
func (e *Err) Error() string {
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
var s strings.Builder
|
|
if e.Op != "" {
|
|
join(&s, ": ", string(e.Op))
|
|
}
|
|
if e.Msg != "" {
|
|
join(&s, ": ", e.Msg)
|
|
}
|
|
|
|
var skipInfo bool
|
|
var wrapped *Err
|
|
if As(e.Wrapped, &wrapped) {
|
|
// if wrapped error code is the same as this error, don't print redundant info
|
|
skipInfo = wrapped.Code == e.Code
|
|
}
|
|
|
|
if info, ok := errorCodeInfo[e.Code]; ok && !skipInfo {
|
|
if e.Msg == "" {
|
|
join(&s, ": ", info.Message) // provide a default.
|
|
join(&s, ", ", info.Kind.String())
|
|
} else {
|
|
join(&s, ": ", info.Kind.String())
|
|
}
|
|
join(&s, ": ", fmt.Sprintf("error #%d", e.Code))
|
|
}
|
|
|
|
if e.Wrapped != nil {
|
|
join(&s, ": ", e.Wrapped.Error())
|
|
}
|
|
return s.String()
|
|
}
|
|
|
|
// ToPbErrors will convert to an Err protobuf
|
|
func ToPbErrors(err *Err) *pberrors.Err {
|
|
pbErr := &pberrors.Err{
|
|
Code: uint32(err.Code),
|
|
Msg: err.Msg,
|
|
Op: string(err.Op),
|
|
}
|
|
|
|
var wrappedErr *Err
|
|
isWrappedErr := As(err.Wrapped, &wrappedErr)
|
|
switch {
|
|
case err.Wrapped == nil:
|
|
pbErr.Wrapped = &pberrors.Err_None{
|
|
None: false,
|
|
}
|
|
case isWrappedErr:
|
|
pbErr.Wrapped = &pberrors.Err_Err{
|
|
Err: ToPbErrors(wrappedErr),
|
|
}
|
|
default:
|
|
pbErr.Wrapped = &pberrors.Err_StdError{
|
|
StdError: err.Wrapped.Error(),
|
|
}
|
|
}
|
|
return pbErr
|
|
}
|
|
|
|
// FromPbErrors will convert from Err protobuf
|
|
func FromPbErrors(pbErr *pberrors.Err) *Err {
|
|
err := &Err{
|
|
Code: Code(pbErr.Code),
|
|
Msg: pbErr.Msg,
|
|
Op: Op(pbErr.Op),
|
|
}
|
|
switch w := pbErr.Wrapped.(type) {
|
|
case *pberrors.Err_Err:
|
|
err.Wrapped = FromPbErrors(w.Err)
|
|
case *pberrors.Err_StdError:
|
|
err.Wrapped = errors.New(w.StdError)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func join(str *strings.Builder, delim string, s string) {
|
|
if str.Len() == 0 {
|
|
_, _ = str.WriteString(s)
|
|
return
|
|
}
|
|
_, _ = str.WriteString(delim + s)
|
|
}
|
|
|
|
// Unwrap implements the errors.Unwrap interface and allows callers to use the
|
|
// errors.Is() and errors.As() functions effectively for any wrapped errors.
|
|
func (e *Err) Unwrap() error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return e.Wrapped
|
|
}
|
|
|
|
// Is the equivalent of the std errors.Is, but allows Devs to only import
|
|
// this package for the capability.
|
|
func Is(err, target error) bool {
|
|
return errors.Is(err, target)
|
|
}
|
|
|
|
// As is the equivalent of the std errors.As, and allows devs to only import
|
|
// this package for the capability.
|
|
func As(err error, target interface{}) bool {
|
|
return errors.As(err, target)
|
|
}
|