Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/google/uuid v1.6.0
github.com/klauspost/connect-compress/v2 v2.1.1
github.com/stretchr/testify v1.11.1
google.golang.org/grpc v1.82.0
google.golang.org/protobuf v1.36.11
)

Expand All @@ -28,6 +29,7 @@ require (
github.com/stretchr/objx v0.5.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.38.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/cel-go v0.29.1 h1:8Gx3S5HPop5XPOHtZI8vkaGk+DBW/nn9fkpNPbh5dBk=
github.com/google/cel-go v0.29.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
Expand Down Expand Up @@ -53,12 +55,18 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M=
golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU=
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 h1:eM/YSd5bBFagF51o1E745Ta7RwzpW0h+z+QDNZOgmQ8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU=
google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
244 changes: 244 additions & 0 deletions go/errorutil/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package errorutil

import (
"errors"
"fmt"
"strings"

"connectrpc.com/connect"
"google.golang.org/grpc/status"

"github.com/google/go-cmp/cmp"
)

// Convert compares the error and maps it to an appropriate connect.Error.
// If there is no backend-specific wrapped error given to this function, it will return an internal error.
// If there are multiple errors wrapped, this function only cares about the first found error in the error tree.
func Convert(err error) *connect.Error {
// Ipam or other connect errors
if connectErr, ok := errors.AsType[*connect.Error](err); ok {
// when the connect error is wrapped deeper a tree, connect.Error() calls the string function on
// the error and adds things like "internal: ..."
// so we replace the wrapped error message with the direct message
cleaned := strings.Replace(err.Error(), connectErr.Error(), connectErr.Message(), 1)
return connect.NewError(connectErr.Code(), errors.New(cleaned))
}

// for other pure grpc apis
if _, ok := status.FromError(err); ok {
// when the grpc error is wrapped deeper a tree, status.FromError calls the string function on
// the error and adds "rpc error: ..."
// so we unwrap the grpc status on our own and replace the error message with the direct message
type grpcstatus interface{ GRPCStatus() *status.Status }
var (
iterErr = err
)

for {
st, ok := iterErr.(grpcstatus)
if ok {
cleaned := strings.Replace(err.Error(), st.GRPCStatus().String(), st.GRPCStatus().Message(), 1)
return connect.NewError(connect.Code(st.GRPCStatus().Code()), errors.New(cleaned))
}

iterErr = errors.Unwrap(iterErr)
if iterErr == nil {
// just a theoretical case
return connect.NewError(connect.CodeUnknown, err)
}
}
}

return connect.NewError(connect.CodeInternal, err)
}

// NotFound creates a new Not Found error (Connect Code 5) with a formatted error message.
func NotFound(format string, args ...any) error {
return connect.NewError(connect.CodeNotFound, fmt.Errorf(format, args...))
}

// Conflict creates a new Already Exists error (Connect Code 6) with a formatted error message.
func Conflict(format string, args ...any) error {
return connect.NewError(connect.CodeAlreadyExists, fmt.Errorf(format, args...))
}

// Internal creates a new Internal error (Connect Code 13) with a formatted error message.
func Internal(format string, args ...any) error {
return connect.NewError(connect.CodeInternal, fmt.Errorf(format, args...))
}

// Aborted creates a new Aborted error (Connect Code 10) with a formatted error message.
func Aborted(format string, args ...any) error {
return connect.NewError(connect.CodeAborted, fmt.Errorf(format, args...))
}

// InvalidArgument creates a new Invalid Argument error (Connect Code 3) with a formatted error message.
func InvalidArgument(format string, args ...any) error {
return connect.NewError(connect.CodeInvalidArgument, fmt.Errorf(format, args...))
}

// FailedPrecondition creates a new Failed Precondition error (Connect Code 9) with a formatted error message.
func FailedPrecondition(format string, args ...any) error {
return connect.NewError(connect.CodeFailedPrecondition, fmt.Errorf(format, args...))
}

// Unauthenticated creates a new Unauthenticated error (Connect Code 16) with a formatted error message.
func Unauthenticated(format string, args ...any) error {
return connect.NewError(connect.CodeUnauthenticated, fmt.Errorf(format, args...))
}

// ResourceExhausted creates a new Resource Exhausted error (Connect Code 8) with a formatted error message.
func ResourceExhausted(format string, args ...any) error {
return connect.NewError(connect.CodeResourceExhausted, fmt.Errorf(format, args...))
}

// PermissionDenied creates a new Permission Denied error (Connect Code 7) with a formatted error message.
func PermissionDenied(format string, args ...any) error {
return connect.NewError(connect.CodePermissionDenied, fmt.Errorf(format, args...))
}

// NewNotFound creates a new Not Found error (Connect Code 5) from an underlying error.
func NewNotFound(err error) error {
return connect.NewError(connect.CodeNotFound, err)
}

// NewConflict creates a new Already Exists error (Connect Code 6) from an underlying error.
func NewConflict(err error) error {
return connect.NewError(connect.CodeAlreadyExists, err)
}

// NewInternal creates a new Internal error (Connect Code 13) from an underlying error.
func NewInternal(err error) error {
return connect.NewError(connect.CodeInternal, err)
}

// NewAborted creates a new Aborted error (Connect Code 10) from an underlying error.
func NewAborted(err error) error {
return connect.NewError(connect.CodeAborted, err)
}

// NewInvalidArgument creates a new Invalid Argument error (Connect Code 3) from an underlying error.
func NewInvalidArgument(err error) error {
return connect.NewError(connect.CodeInvalidArgument, err)
}

// NewFailedPrecondition creates a new Failed Precondition error (Connect Code 9) from an underlying error.
func NewFailedPrecondition(err error) error {
return connect.NewError(connect.CodeFailedPrecondition, err)
}

// NewUnauthenticated creates a new Unauthenticated error (Connect Code 16) from an underlying error.
func NewUnauthenticated(err error) error {
return connect.NewError(connect.CodeUnauthenticated, err)
}

// NewResourceExhausted creates a new Resource Exhausted error (Connect Code 8) from an underlying error.
func NewResourceExhausted(err error) error {
return connect.NewError(connect.CodeResourceExhausted, err)
}

// NewPermissionDenied creates a new Permission Denied error (Connect Code 7) from an underlying error.
func NewPermissionDenied(err error) error {
return connect.NewError(connect.CodePermissionDenied, err)
}

// IsNotFound reports whether the error is a Not Found error (Connect Code 5).
func IsNotFound(err error) bool {
connectErr := Convert(err)
return connectErr.Code() == connect.CodeNotFound
}

// IsConflict reports whether the error is an Already Exists error (Connect Code 6).
func IsConflict(err error) bool {
connectErr := Convert(err)
return connectErr.Code() == connect.CodeAlreadyExists
}

// IsInternal reports whether the error is an Internal error (Connect Code 13).
func IsInternal(err error) bool {
connectErr := Convert(err)
return connectErr.Code() == connect.CodeInternal
}

// IsAborted reports whether the error is an Aborted error (Connect Code 10).
func IsAborted(err error) bool {
connectErr := Convert(err)
return connectErr.Code() == connect.CodeAborted
}

// IsInvalidArgument reports whether the error is an Invalid Argument error (Connect Code 3).
func IsInvalidArgument(err error) bool {
connectErr := Convert(err)
return connectErr.Code() == connect.CodeInvalidArgument
}

// IsFailedPrecondition reports whether the error is a Failed Precondition error (Connect Code 9).
func IsFailedPrecondition(err error) bool {
connectErr := Convert(err)
return connectErr.Code() == connect.CodeFailedPrecondition
}

// IsUnauthenticated reports whether the error is an Unauthenticated error (Connect Code 16).
func IsUnauthenticated(err error) bool {
connectErr := Convert(err)
return connectErr.Code() == connect.CodeUnauthenticated
}

// IsResourceExhausted reports whether the error is a Resource Exhausted error (Connect Code 8).
func IsResourceExhausted(err error) bool {
connectErr := Convert(err)
return connectErr.Code() == connect.CodeResourceExhausted
}

// IsPermissionDenied reports whether the error is a Permission Denied error (Connect Code 7).
func IsPermissionDenied(err error) bool {
connectErr := Convert(err)
return connectErr.Code() == connect.CodePermissionDenied
}

// WrapConnectErr wraps the error into a connect.Error with the given code if it is not already a connect.Error.
// If the error is already a *connect.Error, it returns it unchanged.
func WrapConnectErr(c connect.Code, underlying error) *connect.Error {
if connectErr, ok := errors.AsType[*connect.Error](underlying); ok {
return connectErr
}

return connect.NewError(c, underlying)
}

// ConnectErrorComparer returns a go-cmp Option for comparing *connect.Error values.
// Two errors are considered equal if they have the same error string and the same Connect code.
func ConnectErrorComparer() cmp.Option {
return cmp.Comparer(func(x, y *connect.Error) bool {
if x == nil && y == nil {
return true
}
if x == nil && y != nil {
return false
}
if x != nil && y == nil {
return false
}
if x.Error() != y.Error() {
return false
}
return x.Code() == y.Code()
})
}

// ErrorStringComparer returns a go-cmp Option for comparing error values.
// Two errors are considered equal if their Error() strings are identical.
func ErrorStringComparer() cmp.Option {
return cmp.Comparer(func(x, y error) bool {
if x == nil && y == nil {
return true
}
if x == nil && y != nil {
return false
}
if x != nil && y == nil {
return false
}
return x.Error() == y.Error()
})
}
Loading
Loading