You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
boundary/internal/errors/error.go

341 lines
9.5 KiB

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