* Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * Update copyright file headers to BUS-1.1 * Rerun make gen This will pick up the last of the license changes * Revert "Rerun make gen" This reverts commit dbe0968bffd92745d8663c6940debae098395a6b. * Remove copywrite from generation script The current version of copywrite is not sophisticated enough to only apply the copywrite changes to certain subdirectories, so to avoid accidentally relicensing our api and sdk modules, we will stop running the copywrite script until such time that it can do the right thing. * License protobuf API as MPL The protobuf API files are used to generate the SDK, since we want the SDK to be MPL, the protobuf files must also be MPL. This extends to all protobuf files whose sources end up in the SDK directory, including: - internal/proto/controller/api - internal/proto/controller/custom_options - internal/proto/plugin It also includes the generated service directory in internal/gen/controller/api/services, since they are generated from the protobuf sources used for the SDK. * Change website license file name This makes the naming scheme consistent with the rest of the repository. * Add note on licensing to README * Set copywrite license to BUSL * Revert "Add note on licensing to README" This reverts commit 4d865a91a7a4f11605f0dc4609355d7e3c3cfffe. --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com> Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
323 lines
9.0 KiB
Go
323 lines
9.0 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
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/pgx/v5/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: fmt.Sprintf(opts.withErrMsg, opts.withErrMsgArgs...),
|
|
}
|
|
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...)
|
|
}
|
|
|
|
// 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)
|
|
case "23602": // set_once_column
|
|
msg := fmt.Sprintf("%s.%s can only be set once", pgxError.TableName, pgxError.ColumnName)
|
|
return E(ctx, WithoutEvent(), WithMsg(msg), WithWrap(E(ctx, WithoutEvent(), WithCode(ImmutableColumn), WithMsg("set_once_column 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), WithWrap(e)).(*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 any) bool {
|
|
return errors.As(err, target)
|
|
}
|