diff --git a/resolve.go b/resolve.go new file mode 100644 index 0000000..663338a --- /dev/null +++ b/resolve.go @@ -0,0 +1,116 @@ +/* + 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" + +// Resolve returns the first error found in the error chain which matches an +// error defined in this package or context error. A raw, unwrapped error is +// returned or ErrUnknown if no matching error is found. +// +// This is useful for determining a response code based on the outermost wrapped +// error rather than the original cause. For example, a not found error deep +// in the code may be wrapped as an invalid argument. When determining status +// code from Is* functions, the depth or ordering of the error is not +// considered. +// +// The search order is depth first, a wrapped error returned from any part of +// the chain from `Unwrap() error` will be returned before any joined errors +// as returned by `Unwrap() []error`. +func Resolve(err error) error { + if err == nil { + return nil + } + err = firstError(err) + if err == nil { + err = ErrUnknown + } + return err +} + +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: + return err + } + switch e := err.(type) { + case unknown: + return ErrUnknown + case invalidParameter: + return ErrInvalidArgument + case notFound: + return ErrNotFound + // Skip ErrAlreadyExists, no interface defined + case forbidden: + return ErrPermissionDenied + // Skip ErrResourceExhasuted, no interface defined + // Skip ErrFailedPrecondition, no interface defined + case conflict: + return ErrConflict + case notModified: + return ErrNotModified + // Skip ErrAborted, no interface defined + // Skip ErrOutOfRange, no interface defined + case notImplemented: + return ErrNotImplemented + case system: + return ErrInternal + case unavailable: + return ErrUnavailable + case dataLoss: + return ErrDataLoss + case unauthorized: + return ErrUnauthenticated + case deadlineExceeded: + return context.DeadlineExceeded + case cancelled: + return context.Canceled + case interface{ Unwrap() error }: + err = e.Unwrap() + if err == nil { + return nil + } + case interface{ Unwrap() []error }: + for _, ue := range e.Unwrap() { + if fe := firstError(ue); fe != nil { + return fe + } + } + return nil + default: + return nil + } + } +} diff --git a/resolve_test.go b/resolve_test.go new file mode 100644 index 0000000..c05cae1 --- /dev/null +++ b/resolve_test.go @@ -0,0 +1,91 @@ +/* + 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" + "fmt" + "testing" +) + +func TestResolve(t *testing.T) { + for i, tc := range []struct { + err error + resolved error + }{ + {nil, nil}, + {wrap(ErrUnknown), ErrUnknown}, + {wrap(ErrNotFound), ErrNotFound}, + {wrap(ErrInvalidArgument), ErrInvalidArgument}, + {wrap(ErrNotFound), ErrNotFound}, + {wrap(ErrAlreadyExists), ErrAlreadyExists}, + {wrap(ErrPermissionDenied), ErrPermissionDenied}, + {wrap(ErrResourceExhausted), ErrResourceExhausted}, + {wrap(ErrFailedPrecondition), ErrFailedPrecondition}, + {wrap(ErrConflict), ErrConflict}, + {wrap(ErrNotModified), ErrNotModified}, + {wrap(ErrAborted), ErrAborted}, + {wrap(ErrOutOfRange), ErrOutOfRange}, + {wrap(ErrNotImplemented), ErrNotImplemented}, + {wrap(ErrInternal), ErrInternal}, + {wrap(ErrUnavailable), ErrUnavailable}, + {wrap(ErrDataLoss), ErrDataLoss}, + {wrap(ErrUnauthenticated), ErrUnauthenticated}, + {wrap(context.DeadlineExceeded), context.DeadlineExceeded}, + {wrap(context.Canceled), context.Canceled}, + {errors.Join(errors.New("untyped"), wrap(ErrInvalidArgument)), ErrInvalidArgument}, + {errors.Join(ErrConflict, ErrNotFound), ErrConflict}, + {errors.New("untyped"), ErrUnknown}, + {errors.Join(wrap(ErrUnauthenticated), ErrNotModified), ErrUnauthenticated}, + {ErrDataLoss, ErrDataLoss}, + {errors.Join(ErrOutOfRange), ErrOutOfRange}, + {errors.Join(ErrNotImplemented, ErrInternal), ErrNotImplemented}, + {context.Canceled, context.Canceled}, + {testUnavailable{}, ErrUnavailable}, + {wrap(testUnavailable{}), ErrUnavailable}, + {errors.Join(testUnavailable{}, ErrPermissionDenied), ErrUnavailable}, + {errors.Join(errors.New("untyped join")), ErrUnknown}, + {errors.Join(errors.New("untyped1"), errors.New("untyped2")), ErrUnknown}, + } { + name := fmt.Sprintf("%d-%s", i, errorString(tc.resolved)) + tc := tc + t.Run(name, func(t *testing.T) { + resolved := Resolve(tc.err) + if resolved != tc.resolved { + t.Errorf("Expected %s, got %s", tc.resolved, resolved) + } + }) + } +} + +func wrap(err error) error { + err = fmt.Errorf("wrapped error: %w", err) + return fmt.Errorf("%w and also %w", err, ErrUnknown) +} + +func errorString(err error) string { + if err == nil { + return "nil" + } + return err.Error() +} + +type testUnavailable struct{} + +func (testUnavailable) Error() string { return "" } +func (testUnavailable) Unavailable() {}