Files
threshold/internal/errors/error.go
T
Jim 769416c2ff refactor (controller): Use a grpc server for the grpc-gateway (#1576)
* 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
2021-10-19 17:21:41 -04:00

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