mirror of https://github.com/hashicorp/boundary
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.
341 lines
9.5 KiB
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)
|
|
}
|