From acf19c15667a65a6e73402658b74e8b312e45485 Mon Sep 17 00:00:00 2001 From: "Jonathan A. Sternberg" Date: Mon, 22 Jul 2024 09:59:44 -0500 Subject: [PATCH] Consolidate error types into a single error type This consolidates the various error types to a single error type with a code that designates the type of error. This reduces the amount and complexity of the overall code. The individual methods like `IsInvalidArgument` that support both errors from this package and moby errors are now moved to the `compat.go` file to organize them together. Signed-off-by: Jonathan A. Sternberg --- compat.go | 213 ++++++++++++++++++++++++ errors.go | 432 +++++++++---------------------------------------- errors_test.go | 22 +-- resolve.go | 24 +-- 4 files changed, 300 insertions(+), 391 deletions(-) create mode 100644 compat.go diff --git a/compat.go b/compat.go new file mode 100644 index 0000000..48d68ea --- /dev/null +++ b/compat.go @@ -0,0 +1,213 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package errdefs + +import ( + "context" + "errors" +) + +// IsCanceled returns true if the error is due to `context.Canceled`. +func IsCanceled(err error) bool { + return errors.Is(err, context.Canceled) || isInterface[cancelled](err) +} + +// IsUnknown returns true if the error is due to an unknown error, +// unhandled condition or unexpected response. +func IsUnknown(err error) bool { + return errors.Is(err, ErrUnknown) || isInterface[unknown](err) +} + +// IsInvalidArgument returns true if the error is due to an invalid argument +func IsInvalidArgument(err error) bool { + return errors.Is(err, ErrInvalidArgument) || isInterface[invalidParameter](err) +} + +// IsDeadlineExceeded returns true if the error is due to +// `context.DeadlineExceeded`. +func IsDeadlineExceeded(err error) bool { + return errors.Is(err, context.DeadlineExceeded) || isInterface[deadlineExceeded](err) +} + +// IsNotFound returns true if the error is due to a missing object +func IsNotFound(err error) bool { + return errors.Is(err, ErrNotFound) || isInterface[notFound](err) +} + +// IsAlreadyExists returns true if the error is due to an already existing +// metadata item +func IsAlreadyExists(err error) bool { + return errors.Is(err, ErrAlreadyExists) +} + +// IsPermissionDenied returns true if the error is due to permission denied +// or forbidden (403) response +func IsPermissionDenied(err error) bool { + return errors.Is(err, ErrPermissionDenied) || isInterface[forbidden](err) +} + +// IsResourceExhausted returns true if the error is due to +// a lack of resources or too many attempts. +func IsResourceExhausted(err error) bool { + return errors.Is(err, ErrResourceExhausted) +} + +// IsFailedPrecondition returns true if an operation could not proceed due to +// the lack of a particular condition +func IsFailedPrecondition(err error) bool { + return errors.Is(err, ErrFailedPrecondition) +} + +// IsConflict returns true if an operation could not proceed due to +// a conflict. +func IsConflict(err error) bool { + return errors.Is(err, ErrConflict) || isInterface[conflict](err) +} + +// IsNotModified returns true if an operation could not proceed due +// to an object not modified from a previous state. +func IsNotModified(err error) bool { + return errors.Is(err, ErrNotModified) || isInterface[notModified](err) +} + +// IsAborted returns true if an operation was aborted. +func IsAborted(err error) bool { + return errors.Is(err, ErrAborted) +} + +// IsOutOfRange returns true if an operation could not proceed due +// to data being out of the expected range. +func IsOutOfRange(err error) bool { + return errors.Is(err, ErrOutOfRange) +} + +// IsNotImplemented returns true if the error is due to not being implemented +func IsNotImplemented(err error) bool { + return errors.Is(err, ErrNotImplemented) || isInterface[notImplemented](err) +} + +// IsInternal returns true if the error returns to an internal or system error +func IsInternal(err error) bool { + return errors.Is(err, ErrInternal) || isInterface[system](err) +} + +// IsUnavailable returns true if the error is due to a resource being unavailable +func IsUnavailable(err error) bool { + return errors.Is(err, ErrUnavailable) || isInterface[unavailable](err) +} + +// IsDataLoss returns true if data during an operation was lost or corrupted +func IsDataLoss(err error) bool { + return errors.Is(err, ErrDataLoss) || isInterface[dataLoss](err) +} + +// IsUnauthorized returns true if the error indicates that the user was +// unauthenticated or unauthorized. +func IsUnauthorized(err error) bool { + // Intentional change. Old name was Unauthorized, but the grpc error + // code is named Unauthenticated. The name is changing to Unauthenticated + // but the old function name was IsUnauthorized. + return errors.Is(err, ErrUnauthenticated) || isInterface[unauthorized](err) +} + +// cancelled maps to Moby's "ErrCancelled" +type cancelled interface { + Cancelled() +} + +// unknown maps to Moby's "ErrUnknown" +type unknown interface { + Unknown() +} + +// invalidParameter maps to Moby's "ErrInvalidParameter" +type invalidParameter interface { + InvalidParameter() +} + +// deadlineExceed maps to Moby's "ErrDeadline" +type deadlineExceeded interface { + DeadlineExceeded() +} + +// notFound maps to Moby's "ErrNotFound" +type notFound interface { + NotFound() +} + +// forbidden maps to Moby's "ErrForbidden" +type forbidden interface { + Forbidden() +} + +// conflict maps to Moby's "ErrConflict" +type conflict interface { + Conflict() +} + +// notModified maps to Moby's "ErrNotModified" +type notModified interface { + NotModified() +} + +// notImplemented maps to Moby's "ErrNotImplemented" +type notImplemented interface { + NotImplemented() +} + +// system maps to Moby's "ErrSystem" +type system interface { + System() +} + +// unavailable maps to Moby's "ErrUnavailable" +type unavailable interface { + Unavailable() +} + +// dataLoss maps to Moby's "ErrDataLoss" +type dataLoss interface { + DataLoss() +} + +// unauthorized maps to Moby's "ErrUnauthorized" +type unauthorized interface { + Unauthorized() +} + +func isInterface[T any](err error) bool { + for { + switch x := err.(type) { + case T: + return true + case interface{ Unwrap() error }: + err = x.Unwrap() + if err == nil { + return false + } + case interface{ Unwrap() []error }: + for _, err := range x.Unwrap() { + if isInterface[T](err) { + return true + } + } + return false + default: + return false + } + } +} diff --git a/errors.go b/errors.go index 4827d8c..f5ec23f 100644 --- a/errors.go +++ b/errors.go @@ -25,387 +25,99 @@ package errdefs import ( "context" - "errors" + "fmt" ) +type Error int8 + // Definitions of common error types used throughout containerd. All containerd // errors returned by most packages will map into one of these errors classes. // Packages should return errors of these types when they want to instruct a // client to take a particular action. // // These errors map closely to grpc errors. -var ( - ErrUnknown = errUnknown{} - ErrInvalidArgument = errInvalidArgument{} - ErrNotFound = errNotFound{} - ErrAlreadyExists = errAlreadyExists{} - ErrPermissionDenied = errPermissionDenied{} - ErrResourceExhausted = errResourceExhausted{} - ErrFailedPrecondition = errFailedPrecondition{} - ErrConflict = errConflict{} - ErrNotModified = errNotModified{} - ErrAborted = errAborted{} - ErrOutOfRange = errOutOfRange{} - ErrNotImplemented = errNotImplemented{} - ErrInternal = errInternal{} - ErrUnavailable = errUnavailable{} - ErrDataLoss = errDataLoss{} - ErrUnauthenticated = errUnauthorized{} +const ( + ErrUnknown Error = iota + ErrInvalidArgument + ErrNotFound + ErrAlreadyExists + ErrPermissionDenied + ErrResourceExhausted + ErrFailedPrecondition + ErrConflict + ErrNotModified + ErrAborted + ErrOutOfRange + ErrNotImplemented + ErrInternal + ErrUnavailable + ErrDataLoss + ErrUnauthenticated ) -// cancelled maps to Moby's "ErrCancelled" -type cancelled interface { - Cancelled() -} - -// IsCanceled returns true if the error is due to `context.Canceled`. -func IsCanceled(err error) bool { - return errors.Is(err, context.Canceled) || isInterface[cancelled](err) -} - -type errUnknown struct{} - -func (errUnknown) Error() string { return "unknown" } - -func (errUnknown) Unknown() {} - -func (e errUnknown) WithMessage(msg string) error { - return customMessage{e, msg} -} - -// unknown maps to Moby's "ErrUnknown" -type unknown interface { - Unknown() -} - -// IsUnknown returns true if the error is due to an unknown error, -// unhandled condition or unexpected response. -func IsUnknown(err error) bool { - return errors.Is(err, errUnknown{}) || isInterface[unknown](err) -} - -type errInvalidArgument struct{} - -func (errInvalidArgument) Error() string { return "invalid argument" } - -func (errInvalidArgument) InvalidParameter() {} - -func (e errInvalidArgument) WithMessage(msg string) error { - return customMessage{e, msg} -} - -// invalidParameter maps to Moby's "ErrInvalidParameter" -type invalidParameter interface { - InvalidParameter() -} - -// IsInvalidArgument returns true if the error is due to an invalid argument -func IsInvalidArgument(err error) bool { - return errors.Is(err, ErrInvalidArgument) || isInterface[invalidParameter](err) -} - -// deadlineExceed maps to Moby's "ErrDeadline" -type deadlineExceeded interface { - DeadlineExceeded() -} - -// IsDeadlineExceeded returns true if the error is due to -// `context.DeadlineExceeded`. -func IsDeadlineExceeded(err error) bool { - return errors.Is(err, context.DeadlineExceeded) || isInterface[deadlineExceeded](err) -} - -type errNotFound struct{} - -func (errNotFound) Error() string { return "not found" } - -func (errNotFound) NotFound() {} - -func (e errNotFound) WithMessage(msg string) error { - return customMessage{e, msg} -} - -// notFound maps to Moby's "ErrNotFound" -type notFound interface { - NotFound() -} - -// IsNotFound returns true if the error is due to a missing object -func IsNotFound(err error) bool { - return errors.Is(err, ErrNotFound) || isInterface[notFound](err) -} - -type errAlreadyExists struct{} - -func (errAlreadyExists) Error() string { return "already exists" } - -func (e errAlreadyExists) WithMessage(msg string) error { - return customMessage{e, msg} -} - -// IsAlreadyExists returns true if the error is due to an already existing -// metadata item -func IsAlreadyExists(err error) bool { - return errors.Is(err, ErrAlreadyExists) -} - -type errPermissionDenied struct{} - -func (errPermissionDenied) Error() string { return "permission denied" } - -func (e errPermissionDenied) WithMessage(msg string) error { - return customMessage{e, msg} -} - -// forbidden maps to Moby's "ErrForbidden" -type forbidden interface { - Forbidden() -} - -// IsPermissionDenied returns true if the error is due to permission denied -// or forbidden (403) response -func IsPermissionDenied(err error) bool { - return errors.Is(err, ErrPermissionDenied) || isInterface[forbidden](err) -} - -type errResourceExhausted struct{} - -func (errResourceExhausted) Error() string { return "resource exhausted" } - -func (e errResourceExhausted) WithMessage(msg string) error { - return customMessage{e, msg} -} - -// IsResourceExhausted returns true if the error is due to -// a lack of resources or too many attempts. -func IsResourceExhausted(err error) bool { - return errors.Is(err, errResourceExhausted{}) -} - -type errFailedPrecondition struct{} - -func (e errFailedPrecondition) Error() string { return "failed precondition" } - -func (e errFailedPrecondition) WithMessage(msg string) error { - return customMessage{e, msg} -} - -// IsFailedPrecondition returns true if an operation could not proceed due to -// the lack of a particular condition -func IsFailedPrecondition(err error) bool { - return errors.Is(err, errFailedPrecondition{}) -} - -type errConflict struct{} - -func (errConflict) Error() string { return "conflict" } - -func (errConflict) Conflict() {} - -func (e errConflict) WithMessage(msg string) error { - return customMessage{e, msg} -} - -// conflict maps to Moby's "ErrConflict" -type conflict interface { - Conflict() -} - -// IsConflict returns true if an operation could not proceed due to -// a conflict. -func IsConflict(err error) bool { - return errors.Is(err, errConflict{}) || isInterface[conflict](err) -} - -type errNotModified struct{} - -func (errNotModified) Error() string { return "not modified" } - -func (errNotModified) NotModified() {} - -func (e errNotModified) WithMessage(msg string) error { - return customMessage{e, msg} -} - -// notModified maps to Moby's "ErrNotModified" -type notModified interface { - NotModified() -} - -// IsNotModified returns true if an operation could not proceed due -// to an object not modified from a previous state. -func IsNotModified(err error) bool { - return errors.Is(err, errNotModified{}) || isInterface[notModified](err) -} - -type errAborted struct{} - -func (errAborted) Error() string { return "aborted" } - -func (e errAborted) WithMessage(msg string) error { - return customMessage{e, msg} -} - -// IsAborted returns true if an operation was aborted. -func IsAborted(err error) bool { - return errors.Is(err, errAborted{}) -} - -type errOutOfRange struct{} - -func (errOutOfRange) Error() string { return "out of range" } - -func (e errOutOfRange) WithMessage(msg string) error { - return customMessage{e, msg} -} - -// IsOutOfRange returns true if an operation could not proceed due -// to data being out of the expected range. -func IsOutOfRange(err error) bool { - return errors.Is(err, errOutOfRange{}) -} - -type errNotImplemented struct{} - -func (errNotImplemented) Error() string { return "not implemented" } - -func (errNotImplemented) NotImplemented() {} - -func (e errNotImplemented) WithMessage(msg string) error { - return customMessage{e, msg} -} - -// notImplemented maps to Moby's "ErrNotImplemented" -type notImplemented interface { - NotImplemented() -} - -// IsNotImplemented returns true if the error is due to not being implemented -func IsNotImplemented(err error) bool { - return errors.Is(err, errNotImplemented{}) || isInterface[notImplemented](err) -} - -type errInternal struct{} - -func (errInternal) Error() string { return "internal" } - -func (errInternal) System() {} - -func (e errInternal) WithMessage(msg string) error { - return customMessage{e, msg} -} - -// system maps to Moby's "ErrSystem" -type system interface { - System() -} - -// IsInternal returns true if the error returns to an internal or system error -func IsInternal(err error) bool { - return errors.Is(err, errInternal{}) || isInterface[system](err) -} - -type errUnavailable struct{} - -func (errUnavailable) Error() string { return "unavailable" } - -func (errUnavailable) Unavailable() {} - -func (e errUnavailable) WithMessage(msg string) error { - return customMessage{e, msg} -} - -// unavailable maps to Moby's "ErrUnavailable" -type unavailable interface { - Unavailable() -} - -// IsUnavailable returns true if the error is due to a resource being unavailable -func IsUnavailable(err error) bool { - return errors.Is(err, errUnavailable{}) || isInterface[unavailable](err) -} - -type errDataLoss struct{} - -func (errDataLoss) Error() string { return "data loss" } - -func (errDataLoss) DataLoss() {} - -func (e errDataLoss) WithMessage(msg string) error { - return customMessage{e, msg} -} - -// dataLoss maps to Moby's "ErrDataLoss" -type dataLoss interface { - DataLoss() -} - -// IsDataLoss returns true if data during an operation was lost or corrupted -func IsDataLoss(err error) bool { - return errors.Is(err, errDataLoss{}) || isInterface[dataLoss](err) -} - -type errUnauthorized struct{} - -func (errUnauthorized) Error() string { return "unauthorized" } - -func (errUnauthorized) Unauthorized() {} - -func (e errUnauthorized) WithMessage(msg string) error { - return customMessage{e, msg} -} - -// unauthorized maps to Moby's "ErrUnauthorized" -type unauthorized interface { - Unauthorized() +var ErrCanceled = context.Canceled + +func (e Error) Error() string { + switch e { + case ErrInvalidArgument: + return "invalid argument" + case ErrNotFound: + return "not found" + case ErrAlreadyExists: + return "already exists" + case ErrPermissionDenied: + return "permission denied" + case ErrResourceExhausted: + return "resource exhausted" + case ErrFailedPrecondition: + return "failed precondition" + case ErrConflict: + return "conflict" + case ErrNotModified: + return "not modified" + case ErrAborted: + return "aborted" + case ErrOutOfRange: + return "out of range" + case ErrNotImplemented: + return "not implemented" + case ErrInternal: + return "internal" + case ErrUnavailable: + return "unavailable" + case ErrDataLoss: + return "unauthenticated" + case ErrUnauthenticated: + return "unauthenticated" + default: + return "unknown" + } } -// IsUnauthorized returns true if the error indicates that the user was -// unauthenticated or unauthorized. -func IsUnauthorized(err error) bool { - return errors.Is(err, errUnauthorized{}) || isInterface[unauthorized](err) +func (e Error) WithMessage(msg string) error { + return customMessage{ + msg: msg, + err: e, + } } -func isInterface[T any](err error) bool { - for { - switch x := err.(type) { - case T: - return true - case customMessage: - err = x.err - case interface{ Unwrap() error }: - err = x.Unwrap() - if err == nil { - return false - } - case interface{ Unwrap() []error }: - for _, err := range x.Unwrap() { - if isInterface[T](err) { - return true - } - } - return false - default: - return false - } +func (e Error) WithMessagef(format string, args ...any) error { + return customMessage{ + msg: fmt.Sprintf(format, args...), + err: e, } } -// customMessage is used to provide a defined error with a custom message. -// The message is not wrapped but can be compared by the `Is(error) bool` interface. +// customMessage wraps an underlying code with a custom message. type customMessage struct { - err error msg string + err Error } -func (c customMessage) Is(err error) bool { - return c.err == err -} - -func (c customMessage) As(target any) bool { - return errors.As(c.err, target) +func (m customMessage) Error() string { + return m.msg } -func (c customMessage) Error() string { - return c.msg +func (m customMessage) Unwrap() error { + return m.err } diff --git a/errors_test.go b/errors_test.go index 40e564f..2c49210 100644 --- a/errors_test.go +++ b/errors_test.go @@ -26,7 +26,7 @@ import ( func TestInvalidArgument(t *testing.T) { for _, match := range []error{ ErrInvalidArgument, - &errInvalidArgument{}, + customMessage{err: ErrInvalidArgument}, &customInvalidArgument{}, &wrappedInvalidArgument{errors.New("invalid parameter")}, } { @@ -55,7 +55,7 @@ func TestErrorEquivalence(t *testing.T) { t.Fatal("errors.Is should not return true") } - var e3 error = errAborted{} + var e3 error = ErrAborted if e1 != e3 { t.Fatal("new instance should be equivalent") } @@ -65,12 +65,12 @@ func TestErrorEquivalence(t *testing.T) { if !errors.Is(e3, e1) { t.Fatal("errors.Is should be true") } - var aborted errAborted + var aborted Error if !errors.As(e1, &aborted) { t.Fatal("errors.As should be true") } - var e4 = ErrAborted.WithMessage("custom message") + e4 := ErrAborted.WithMessage("custom message") if e1 == e4 { t.Fatal("should not equal the same error") } @@ -94,13 +94,14 @@ func TestErrorEquivalence(t *testing.T) { if custom.msg != "custom message" { t.Fatalf("unexpected custom message: %q", custom.msg) } - if custom.err != e1 { - t.Fatalf("unexpected custom message error: %v", custom.err) + if !errors.Is(custom, e1) { + t.Fatal("errors.Is should be true") } } func TestWithMessage(t *testing.T) { - testErrors := []error{ErrUnknown, + testErrors := []error{ + ErrUnknown, ErrInvalidArgument, ErrNotFound, ErrAlreadyExists, @@ -138,7 +139,7 @@ func TestWithMessage(t *testing.T) { t.Fatal("errors.Is should be false, e1 is not a custom message") } - var raw = reflect.New(reflect.TypeOf(e1)).Interface() + raw := reflect.New(reflect.TypeOf(e1)).Interface() if !errors.As(e2, raw) { t.Fatal("errors.As should be true") } @@ -150,10 +151,9 @@ func TestWithMessage(t *testing.T) { if custom.msg != "custom message" { t.Fatalf("unexpected custom message: %q", custom.msg) } - if custom.err != e1 { - t.Fatalf("unexpected custom message error: %v", custom.err) + if !errors.Is(custom, e1) { + t.Fatal("errors.Is should be true") } - }) } } diff --git a/resolve.go b/resolve.go index ea049da..64c752e 100644 --- a/resolve.go +++ b/resolve.go @@ -45,29 +45,13 @@ func Resolve(err error) error { func firstError(err error) error { for { switch err { - case ErrUnknown, - ErrInvalidArgument, - ErrNotFound, - ErrAlreadyExists, - ErrPermissionDenied, - ErrResourceExhausted, - ErrFailedPrecondition, - ErrConflict, - ErrNotModified, - ErrAborted, - ErrOutOfRange, - ErrNotImplemented, - ErrInternal, - ErrUnavailable, - ErrDataLoss, - ErrUnauthenticated, - context.DeadlineExceeded, - context.Canceled: + case context.Canceled, context.DeadlineExceeded: return err } + switch e := err.(type) { - case customMessage: - err = e.err + case Error: + return e case unknown: return ErrUnknown case invalidParameter: