pax_global_header00006660000000000000000000000064147012274610014516gustar00rootroot0000000000000052 comment=4817405e4a3caeb7aee9dac68ed55339c59cb635 errdefs-pkg-v0.3.0/000077500000000000000000000000001470122746100141155ustar00rootroot00000000000000errdefs-pkg-v0.3.0/.github/000077500000000000000000000000001470122746100154555ustar00rootroot00000000000000errdefs-pkg-v0.3.0/.github/workflows/000077500000000000000000000000001470122746100175125ustar00rootroot00000000000000errdefs-pkg-v0.3.0/.github/workflows/ci.yml000066400000000000000000000044471470122746100206410ustar00rootroot00000000000000name: CI on: push: branches: [ main ] pull_request: branches: [ main ] env: # Go version we currently use to build containerd across all CI. # Note: don't forget to update `Binaries` step, as it contains the matrix of all supported Go versions. GO_VERSION: "1.22.8" permissions: # added using https://github.com/step-security/secure-workflows contents: read jobs: # # golangci-lint # linters: permissions: contents: read # for actions/checkout to fetch code pull-requests: read # for golangci/golangci-lint-action to fetch pull requests name: Linters runs-on: ${{ matrix.os }} timeout-minutes: 10 strategy: matrix: os: [ubuntu-22.04, macos-13, windows-2022] steps: - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: false # see actions/setup-go#368 - uses: actions/checkout@v4 - uses: golangci/golangci-lint-action@v6 with: version: v1.61.0 skip-cache: true args: --timeout=5m # # Project checks # project: name: Project Checks if: github.repository == 'containerd/errdefs' runs-on: ubuntu-22.04 timeout-minutes: 5 steps: - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: false # see actions/setup-go#368 - uses: actions/checkout@v4 with: path: src/github.com/containerd/errdefs fetch-depth: 25 - uses: containerd/project-checks@v1.1.0 with: working-directory: src/github.com/containerd/errdefs repo-access-token: ${{ secrets.GITHUB_TOKEN }} tests: name: Tests runs-on: ${{ matrix.os }} timeout-minutes: 5 strategy: matrix: os: [ubuntu-22.04, macos-13, windows-2022] steps: - uses: actions/checkout@v4 with: path: src/github.com/containerd/errdefs - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} - name: Set env shell: bash run: | echo "GOPATH=${{ github.workspace }}" >> $GITHUB_ENV echo "${{ github.workspace }}/bin" >> $GITHUB_PATH - run: | go test -v -race working-directory: src/github.com/containerd/errdefs errdefs-pkg-v0.3.0/LICENSE000066400000000000000000000250151470122746100151250ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS 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 https://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. errdefs-pkg-v0.3.0/README.md000066400000000000000000000011251470122746100153730ustar00rootroot00000000000000# errdefs A Go package for defining and checking common containerd errors. ## Project details **errdefs** is a containerd sub-project, licensed under the [Apache 2.0 license](./LICENSE). As a containerd sub-project, you will find the: * [Project governance](https://github.com/containerd/project/blob/main/GOVERNANCE.md), * [Maintainers](https://github.com/containerd/project/blob/main/MAINTAINERS), * and [Contributing guidelines](https://github.com/containerd/project/blob/main/CONTRIBUTING.md) information in our [`containerd/project`](https://github.com/containerd/project) repository. errdefs-pkg-v0.3.0/errors.go000066400000000000000000000263201470122746100157630ustar00rootroot00000000000000/* 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 defines the common errors used throughout containerd // packages. // // Use with fmt.Errorf to add context to an error. // // To detect an error class, use the IsXXX functions to tell whether an error // is of a certain type. package errdefs import ( "context" "errors" ) // 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{} ) // 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 (errAlreadyExists) AlreadyExists() {} func (e errAlreadyExists) WithMessage(msg string) error { return customMessage{e, msg} } type alreadyExists interface { AlreadyExists() } // IsAlreadyExists returns true if the error is due to an already existing // metadata item func IsAlreadyExists(err error) bool { return errors.Is(err, ErrAlreadyExists) || isInterface[alreadyExists](err) } type errPermissionDenied struct{} func (errPermissionDenied) Error() string { return "permission denied" } func (errPermissionDenied) Forbidden() {} 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 (errResourceExhausted) ResourceExhausted() {} func (e errResourceExhausted) WithMessage(msg string) error { return customMessage{e, msg} } type resourceExhausted interface { ResourceExhausted() } // 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{}) || isInterface[resourceExhausted](err) } type errFailedPrecondition struct{} func (e errFailedPrecondition) Error() string { return "failed precondition" } func (errFailedPrecondition) FailedPrecondition() {} func (e errFailedPrecondition) WithMessage(msg string) error { return customMessage{e, msg} } type failedPrecondition interface { FailedPrecondition() } // 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{}) || isInterface[failedPrecondition](err) } 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 (errAborted) Aborted() {} func (e errAborted) WithMessage(msg string) error { return customMessage{e, msg} } type aborted interface { Aborted() } // IsAborted returns true if an operation was aborted. func IsAborted(err error) bool { return errors.Is(err, errAborted{}) || isInterface[aborted](err) } type errOutOfRange struct{} func (errOutOfRange) Error() string { return "out of range" } func (errOutOfRange) OutOfRange() {} func (e errOutOfRange) WithMessage(msg string) error { return customMessage{e, msg} } type outOfRange interface { OutOfRange() } // 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{}) || isInterface[outOfRange](err) } 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() } // 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 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 } } } // 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. type customMessage struct { err error msg string } 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 (c customMessage) Error() string { return c.msg } errdefs-pkg-v0.3.0/errors_test.go000066400000000000000000000116671470122746100170320ustar00rootroot00000000000000/* 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" "reflect" "testing" ) func TestInvalidArgument(t *testing.T) { for _, match := range []error{ ErrInvalidArgument, &errInvalidArgument{}, &customInvalidArgument{}, &wrappedInvalidArgument{errors.New("invalid parameter")}, } { if !IsInvalidArgument(match) { t.Errorf("error did not match invalid argument: %#v", match) } } for _, nonMatch := range []error{ ErrUnknown, context.Canceled, errors.New("invalid argument"), } { if IsInvalidArgument(nonMatch) { t.Errorf("error unexpectedly matched invalid argument: %#v", nonMatch) } } } func TestErrorEquivalence(t *testing.T) { var e1 error = ErrAborted var e2 error = ErrUnknown if e1 == e2 { t.Fatal("should not equal the same error") } if errors.Is(e1, e2) { t.Fatal("errors.Is should not return true") } var e3 error = errAborted{} if e1 != e3 { t.Fatal("new instance should be equivalent") } if !errors.Is(e1, e3) { t.Fatal("errors.Is should be true") } if !errors.Is(e3, e1) { t.Fatal("errors.Is should be true") } var aborted errAborted if !errors.As(e1, &aborted) { t.Fatal("errors.As should be true") } var e4 = ErrAborted.WithMessage("custom message") if e1 == e4 { t.Fatal("should not equal the same error") } if !errors.Is(e4, e1) { t.Fatal("errors.Is should be true, e1 is in the tree of e4") } if errors.Is(e1, e4) { t.Fatal("errors.Is should be false, e1 is not a custom message") } if !errors.As(e4, &aborted) { t.Fatal("errors.As should be true") } var custom customMessage if !errors.As(e4, &custom) { t.Fatal("errors.As should be true") } 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) } } func TestWithMessage(t *testing.T) { testErrors := []error{ErrUnknown, ErrInvalidArgument, ErrNotFound, ErrAlreadyExists, ErrPermissionDenied, ErrResourceExhausted, ErrFailedPrecondition, ErrConflict, ErrNotModified, ErrAborted, ErrOutOfRange, ErrNotImplemented, ErrInternal, ErrUnavailable, ErrDataLoss, ErrUnauthenticated, } for _, err := range testErrors { e1 := err t.Run(err.Error(), func(t *testing.T) { wm, ok := e1.(interface{ WithMessage(string) error }) if !ok { t.Fatal("WithMessage not supported") } e2 := wm.WithMessage("custom message") if e1 == e2 { t.Fatal("should not equal the same error") } if !errors.Is(e2, e1) { t.Fatal("errors.Is should return true") } if errors.Is(e1, e2) { t.Fatal("errors.Is should be false, e1 is not a custom message") } var raw = reflect.New(reflect.TypeOf(e1)).Interface() if !errors.As(e2, raw) { t.Fatal("errors.As should be true") } var custom customMessage if !errors.As(e2, &custom) { t.Fatal("errors.As should be true") } 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) } }) } } func TestInterfaceMatch(t *testing.T) { testCases := []struct { err error check func(error) bool }{ {ErrUnknown, isInterface[unknown]}, {ErrInvalidArgument, isInterface[invalidParameter]}, {ErrNotFound, isInterface[notFound]}, {ErrAlreadyExists, isInterface[alreadyExists]}, {ErrPermissionDenied, isInterface[forbidden]}, {ErrResourceExhausted, isInterface[resourceExhausted]}, {ErrFailedPrecondition, isInterface[failedPrecondition]}, {ErrConflict, isInterface[conflict]}, {ErrNotModified, isInterface[notModified]}, {ErrAborted, isInterface[aborted]}, {ErrOutOfRange, isInterface[outOfRange]}, {ErrNotImplemented, isInterface[notImplemented]}, {ErrInternal, isInterface[system]}, {ErrUnavailable, isInterface[unavailable]}, {ErrDataLoss, isInterface[dataLoss]}, {ErrUnauthenticated, isInterface[unauthorized]}, } for _, tc := range testCases { tc := tc t.Run(fmt.Sprintf("%T", tc.err), func(t *testing.T) { if !tc.check(tc.err) { t.Fatal("Error does not match interface") } }) } } type customInvalidArgument struct{} func (*customInvalidArgument) Error() string { return "my own invalid argument" } func (*customInvalidArgument) InvalidParameter() {} type wrappedInvalidArgument struct{ error } func (*wrappedInvalidArgument) InvalidParameter() {} errdefs-pkg-v0.3.0/go.mod000066400000000000000000000000561470122746100152240ustar00rootroot00000000000000module github.com/containerd/errdefs go 1.20 errdefs-pkg-v0.3.0/go.sum000066400000000000000000000000001470122746100152360ustar00rootroot00000000000000errdefs-pkg-v0.3.0/pkg/000077500000000000000000000000001470122746100146765ustar00rootroot00000000000000errdefs-pkg-v0.3.0/pkg/errgrpc/000077500000000000000000000000001470122746100163425ustar00rootroot00000000000000errdefs-pkg-v0.3.0/pkg/errgrpc/grpc.go000066400000000000000000000254211470122746100176300ustar00rootroot00000000000000/* 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 errgrpc provides utility functions for translating errors to // and from a gRPC context. // // The functions ToGRPC and ToNative can be used to map server-side and // client-side errors to the correct types. package errgrpc import ( "context" "errors" "fmt" "reflect" "strconv" "strings" spb "google.golang.org/genproto/googleapis/rpc/status" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/protoadapt" "google.golang.org/protobuf/types/known/anypb" "github.com/containerd/typeurl/v2" "github.com/containerd/errdefs" "github.com/containerd/errdefs/pkg/internal/cause" "github.com/containerd/errdefs/pkg/internal/types" ) // ToGRPC will attempt to map the error into a grpc error, from the error types // defined in the the errdefs package and attempign to preserve the original // description. Any type which does not resolve to a defined error type will // be assigned the unknown error code. // // Further information may be extracted from certain errors depending on their // type. The grpc error details will be used to attempt to preserve as much of // the error structures and types as possible. // // Errors which can be marshaled using protobuf or typeurl will be considered // for including as GRPC error details. // Additionally, use the following interfaces in errors to preserve custom types: // // WrapError(error) error - Used to wrap the previous error // JoinErrors(...error) error - Used to join all previous errors // CollapseError() - Used for errors which carry information but // should not have their error message shown. func ToGRPC(err error) error { if err == nil { return nil } if _, ok := status.FromError(err); ok { // error has already been mapped to grpc return err } st := statusFromError(err) if st != nil { if details := errorDetails(err, false); len(details) > 0 { if ds, _ := st.WithDetails(details...); ds != nil { st = ds } } err = st.Err() } return err } func statusFromError(err error) *status.Status { switch errdefs.Resolve(err) { case errdefs.ErrInvalidArgument: return status.New(codes.InvalidArgument, err.Error()) case errdefs.ErrNotFound: return status.New(codes.NotFound, err.Error()) case errdefs.ErrAlreadyExists: return status.New(codes.AlreadyExists, err.Error()) case errdefs.ErrPermissionDenied: return status.New(codes.PermissionDenied, err.Error()) case errdefs.ErrResourceExhausted: return status.New(codes.ResourceExhausted, err.Error()) case errdefs.ErrFailedPrecondition, errdefs.ErrConflict, errdefs.ErrNotModified: return status.New(codes.FailedPrecondition, err.Error()) case errdefs.ErrAborted: return status.New(codes.Aborted, err.Error()) case errdefs.ErrOutOfRange: return status.New(codes.OutOfRange, err.Error()) case errdefs.ErrNotImplemented: return status.New(codes.Unimplemented, err.Error()) case errdefs.ErrInternal: return status.New(codes.Internal, err.Error()) case errdefs.ErrUnavailable: return status.New(codes.Unavailable, err.Error()) case errdefs.ErrDataLoss: return status.New(codes.DataLoss, err.Error()) case errdefs.ErrUnauthenticated: return status.New(codes.Unauthenticated, err.Error()) case context.DeadlineExceeded: return status.New(codes.DeadlineExceeded, err.Error()) case context.Canceled: return status.New(codes.Canceled, err.Error()) case errdefs.ErrUnknown: return status.New(codes.Unknown, err.Error()) } return nil } // errorDetails returns an array of errors which make up the provided error. // If firstIncluded is true, then all encodable errors will be used, otherwise // the first error in an error list will be not be used, to account for the // the base status error which details are added to via wrap or join. // // The errors are ordered in way that they can be applied in order by either // wrapping or joining the errors to recreate an error with the same structure // when `WrapError` and `JoinErrors` interfaces are used. // // The intent is that when re-applying the errors to create a single error, the // results of calls to `Error()`, `errors.Is`, `errors.As`, and "%+v" formatting // is the same as the original error. func errorDetails(err error, firstIncluded bool) []protoadapt.MessageV1 { switch uerr := err.(type) { case interface{ Unwrap() error }: details := errorDetails(uerr.Unwrap(), firstIncluded) // If the type is able to wrap, then include if proto if _, ok := err.(interface{ WrapError(error) error }); ok { // Get proto message if protoErr := toProtoMessage(err); protoErr != nil { details = append(details, protoErr) } } return details case interface{ Unwrap() []error }: var details []protoadapt.MessageV1 for i, e := range uerr.Unwrap() { details = append(details, errorDetails(e, firstIncluded || i > 0)...) } if _, ok := err.(interface{ JoinErrors(...error) error }); ok { // Get proto message if protoErr := toProtoMessage(err); protoErr != nil { details = append(details, protoErr) } } return details } if firstIncluded { if protoErr := toProtoMessage(err); protoErr != nil { return []protoadapt.MessageV1{protoErr} } if gs, ok := status.FromError(ToGRPC(err)); ok { return []protoadapt.MessageV1{gs.Proto()} } // TODO: Else include unknown extra error type? } return nil } func toProtoMessage(err error) protoadapt.MessageV1 { // Do not double encode proto messages, otherwise use Any if pm, ok := err.(protoadapt.MessageV1); ok { return pm } if pm, ok := err.(proto.Message); ok { return protoadapt.MessageV1Of(pm) } if reflect.TypeOf(err).Kind() == reflect.Ptr { a, aerr := typeurl.MarshalAny(err) if aerr == nil { return &anypb.Any{ TypeUrl: a.GetTypeUrl(), Value: a.GetValue(), } } } return nil } // ToGRPCf maps the error to grpc error codes, assembling the formatting string // and combining it with the target error string. // // This is equivalent to grpc.ToGRPC(fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err)) func ToGRPCf(err error, format string, args ...interface{}) error { return ToGRPC(fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err)) } // ToNative returns the underlying error from a grpc service based on the grpc // error code. The grpc details are used to add wrap the error in more context // or support multiple errors. func ToNative(err error) error { if err == nil { return nil } s, isGRPC := status.FromError(err) var ( desc string code codes.Code ) if isGRPC { desc = s.Message() code = s.Code() } else { desc = err.Error() code = codes.Unknown } var cls error // divide these into error classes, becomes the cause switch code { case codes.InvalidArgument: cls = errdefs.ErrInvalidArgument case codes.AlreadyExists: cls = errdefs.ErrAlreadyExists case codes.NotFound: cls = errdefs.ErrNotFound case codes.Unavailable: cls = errdefs.ErrUnavailable case codes.FailedPrecondition: // TODO: Has suffix is not sufficient for conflict and not modified // Message should start with ": " or be at beginning of a line // Message should end with ": " or be at the end of a line // Compile a regex if desc == errdefs.ErrConflict.Error() || strings.HasSuffix(desc, ": "+errdefs.ErrConflict.Error()) { cls = errdefs.ErrConflict } else if desc == errdefs.ErrNotModified.Error() || strings.HasSuffix(desc, ": "+errdefs.ErrNotModified.Error()) { cls = errdefs.ErrNotModified } else { cls = errdefs.ErrFailedPrecondition } case codes.Unimplemented: cls = errdefs.ErrNotImplemented case codes.Canceled: cls = context.Canceled case codes.DeadlineExceeded: cls = context.DeadlineExceeded case codes.Aborted: cls = errdefs.ErrAborted case codes.Unauthenticated: cls = errdefs.ErrUnauthenticated case codes.PermissionDenied: cls = errdefs.ErrPermissionDenied case codes.Internal: cls = errdefs.ErrInternal case codes.DataLoss: cls = errdefs.ErrDataLoss case codes.OutOfRange: cls = errdefs.ErrOutOfRange case codes.ResourceExhausted: cls = errdefs.ErrResourceExhausted default: if idx := strings.LastIndex(desc, cause.UnexpectedStatusPrefix); idx > 0 { if status, uerr := strconv.Atoi(desc[idx+len(cause.UnexpectedStatusPrefix):]); uerr == nil && status >= 200 && status < 600 { cls = cause.ErrUnexpectedStatus{Status: status} } } if cls == nil { cls = errdefs.ErrUnknown } } msg := rebaseMessage(cls, desc) if msg == "" { err = cls } else if msg != desc { err = fmt.Errorf("%s: %w", msg, cls) } else if wm, ok := cls.(interface{ WithMessage(string) error }); ok { err = wm.WithMessage(msg) } else { err = fmt.Errorf("%s: %w", msg, cls) } if isGRPC { errs := []error{err} for _, a := range s.Details() { var derr error // First decode error if needed if s, ok := a.(*spb.Status); ok { derr = ToNative(status.ErrorProto(s)) } else if e, ok := a.(error); ok { derr = e } else if dany, ok := a.(typeurl.Any); ok { i, uerr := typeurl.UnmarshalAny(dany) if uerr == nil { if e, ok = i.(error); ok { derr = e } else { derr = fmt.Errorf("non-error unmarshalled detail: %v", i) } } else { derr = fmt.Errorf("error of type %q with failure to unmarshal: %v", dany.GetTypeUrl(), uerr) } } else { derr = fmt.Errorf("non-error detail: %v", a) } switch werr := derr.(type) { case interface{ WrapError(error) error }: errs[len(errs)-1] = werr.WrapError(errs[len(errs)-1]) case interface{ JoinErrors(...error) error }: // TODO: Consider whether this should support joining a subset errs[0] = werr.JoinErrors(errs...) case interface{ CollapseError() }: errs[len(errs)-1] = types.CollapsedError(errs[len(errs)-1], derr) default: errs = append(errs, derr) } } if len(errs) > 1 { err = errors.Join(errs...) } else { err = errs[0] } } return err } // rebaseMessage removes the repeats for an error at the end of an error // string. This will happen when taking an error over grpc then remapping it. // // Effectively, we just remove the string of cls from the end of err if it // appears there. func rebaseMessage(cls error, desc string) string { clss := cls.Error() if desc == clss { return "" } return strings.TrimSuffix(desc, ": "+clss) } errdefs-pkg-v0.3.0/pkg/errgrpc/grpc_test.go000066400000000000000000000154751470122746100206770ustar00rootroot00000000000000/* 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 errgrpc import ( "context" "errors" "fmt" "strings" "testing" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "github.com/containerd/typeurl/v2" "github.com/containerd/errdefs" "github.com/containerd/errdefs/pkg/errhttp" "github.com/containerd/errdefs/pkg/internal/cause" ) func TestGRPCNilInput(t *testing.T) { if err := ToGRPC(nil); err != nil { t.Fatalf("Expected nil error, got %v", err) } if err := ToNative(nil); err != nil { t.Fatalf("Expected nil error, got %v", err) } } func TestGRPCRoundTrip(t *testing.T) { errShouldLeaveAlone := errors.New("unknown to package") for _, testcase := range []struct { input error cause error str string }{ { input: errdefs.ErrInvalidArgument, cause: errdefs.ErrInvalidArgument, }, { input: errdefs.ErrAlreadyExists, cause: errdefs.ErrAlreadyExists, }, { input: errdefs.ErrNotFound, cause: errdefs.ErrNotFound, }, { input: errdefs.ErrUnavailable, cause: errdefs.ErrUnavailable, }, { input: errdefs.ErrNotImplemented, cause: errdefs.ErrNotImplemented, }, { input: errdefs.ErrUnauthenticated, cause: errdefs.ErrUnauthenticated, }, { input: errdefs.ErrPermissionDenied, cause: errdefs.ErrPermissionDenied, }, { input: errdefs.ErrInternal, cause: errdefs.ErrInternal, }, { input: errdefs.ErrDataLoss, cause: errdefs.ErrDataLoss, }, { input: errdefs.ErrAborted, cause: errdefs.ErrAborted, }, { input: errdefs.ErrOutOfRange, cause: errdefs.ErrOutOfRange, }, { input: errdefs.ErrResourceExhausted, cause: errdefs.ErrResourceExhausted, }, { input: errdefs.ErrUnknown, cause: errdefs.ErrUnknown, }, //nolint:dupword { input: fmt.Errorf("test test test: %w", errdefs.ErrFailedPrecondition), cause: errdefs.ErrFailedPrecondition, str: "test test test: failed precondition", }, { // Currently failing input: status.Errorf(codes.Unavailable, "should be not available"), cause: errdefs.ErrUnavailable, str: "should be not available", }, { input: errShouldLeaveAlone, cause: errdefs.ErrUnknown, str: errShouldLeaveAlone.Error(), }, { input: context.Canceled, cause: context.Canceled, str: "context canceled", }, { input: fmt.Errorf("this is a test cancel: %w", context.Canceled), cause: context.Canceled, str: "this is a test cancel: context canceled", }, { input: context.DeadlineExceeded, cause: context.DeadlineExceeded, str: "context deadline exceeded", }, { input: fmt.Errorf("this is a test deadline exceeded: %w", context.DeadlineExceeded), cause: context.DeadlineExceeded, str: "this is a test deadline exceeded: context deadline exceeded", }, { input: fmt.Errorf("something conflicted: %w", errdefs.ErrConflict), cause: errdefs.ErrConflict, str: "something conflicted: conflict", }, { input: fmt.Errorf("everything is the same: %w", errdefs.ErrNotModified), cause: errdefs.ErrNotModified, str: "everything is the same: not modified", }, { input: fmt.Errorf("odd HTTP response: %w", errhttp.ToNative(418)), cause: cause.ErrUnexpectedStatus{Status: 418}, str: "odd HTTP response: unexpected status 418", }, } { t.Run(testcase.input.Error(), func(t *testing.T) { t.Logf("input: %v", testcase.input) gerr := ToGRPC(testcase.input) t.Logf("grpc: %v", gerr) ferr := ToNative(gerr) t.Logf("recovered: %v", ferr) if !errors.Is(ferr, testcase.cause) { t.Fatalf("unexpected cause: !errors.Is(%v, %v)", ferr, testcase.cause) } expected := testcase.str if expected == "" { expected = testcase.cause.Error() } if ferr.Error() != expected { t.Fatalf("unexpected string: %q != %q", ferr.Error(), expected) } }) } } type TestError struct { Value string `json:"value"` } func (*TestError) Error() string { return "test error" } func TestGRPCCustomDetails(t *testing.T) { typeurl.Register(&TestError{}, t.Name()) expected := &TestError{ Value: "test 1", } err := errors.Join(errdefs.ErrInternal, expected) gerr := ToGRPC(err) s, ok := status.FromError(gerr) if !ok { t.Fatalf("Not GRPC error: %v", gerr) } if s.Code() != codes.Internal { t.Fatalf("Unexpectd GRPC code %v, expected %v", s.Code(), codes.Internal) } nerr := ToNative(gerr) if !errors.Is(nerr, errdefs.ErrInternal) { t.Fatalf("Expected internal error type, got %v", nerr) } if !errdefs.IsInternal(err) { t.Fatalf("Expected internal error type, got %v", nerr) } terr := &TestError{} if !errors.As(nerr, &terr) { t.Fatalf("TestError not preserved, got %v", nerr) } else if terr.Value != expected.Value { t.Fatalf("Value not preserved, got %v", terr.Value) } } func TestGRPCMultiError(t *testing.T) { err := errors.Join(errdefs.ErrPermissionDenied, errdefs.ErrDataLoss, errdefs.ErrConflict, fmt.Errorf("Was not changed at all!: %w", errdefs.ErrNotModified)) checkError := func(err error) { t.Helper() if !errors.Is(err, errdefs.ErrPermissionDenied) { t.Fatal("Not permission denied") } if !errors.Is(err, errdefs.ErrDataLoss) { t.Fatal("Not data loss") } if !errors.Is(err, errdefs.ErrConflict) { t.Fatal("Not conflict") } if !errors.Is(err, errdefs.ErrNotModified) { t.Fatal("Not not modified") } if errors.Is(err, errdefs.ErrFailedPrecondition) { t.Fatal("Should not be failed precondition") } if !strings.Contains(err.Error(), "Was not changed at all!") { t.Fatalf("Not modified error message missing from:\n%v", err) } } checkError(err) terr := ToNative(ToGRPC(err)) checkError(terr) // Try again with decoded error checkError(ToNative(ToGRPC(terr))) } func TestGRPCNestedError(t *testing.T) { multiErr := errors.Join(fmt.Errorf("First error: %w", errdefs.ErrNotFound), fmt.Errorf("Second error: %w", errdefs.ErrResourceExhausted)) checkError := func(err error) { t.Helper() if !errors.Is(err, errdefs.ErrNotFound) { t.Fatal("Not not found") } if !errors.Is(err, errdefs.ErrResourceExhausted) { t.Fatal("Not resource exhausted") } if errors.Is(err, errdefs.ErrFailedPrecondition) { t.Fatal("Should not be failed precondition") } } checkError(multiErr) werr := fmt.Errorf("Wrapping the error: %w", multiErr) checkError(werr) checkError(ToNative(ToGRPC(werr))) } errdefs-pkg-v0.3.0/pkg/errhttp/000077500000000000000000000000001470122746100163665ustar00rootroot00000000000000errdefs-pkg-v0.3.0/pkg/errhttp/http.go000066400000000000000000000057161470122746100177050ustar00rootroot00000000000000/* 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 errhttp provides utility functions for translating errors to // and from a HTTP context. // // The functions ToHTTP and ToNative can be used to map server-side and // client-side errors to the correct types. package errhttp import ( "errors" "net/http" "github.com/containerd/errdefs" "github.com/containerd/errdefs/pkg/internal/cause" ) // ToHTTP returns the best status code for the given error func ToHTTP(err error) int { switch { case errdefs.IsNotFound(err): return http.StatusNotFound case errdefs.IsInvalidArgument(err): return http.StatusBadRequest case errdefs.IsConflict(err): return http.StatusConflict case errdefs.IsNotModified(err): return http.StatusNotModified case errdefs.IsFailedPrecondition(err): return http.StatusPreconditionFailed case errdefs.IsUnauthorized(err): return http.StatusUnauthorized case errdefs.IsPermissionDenied(err): return http.StatusForbidden case errdefs.IsResourceExhausted(err): return http.StatusTooManyRequests case errdefs.IsInternal(err): return http.StatusInternalServerError case errdefs.IsNotImplemented(err): return http.StatusNotImplemented case errdefs.IsUnavailable(err): return http.StatusServiceUnavailable case errdefs.IsUnknown(err): var unexpected cause.ErrUnexpectedStatus if errors.As(err, &unexpected) && unexpected.Status >= 200 && unexpected.Status < 600 { return unexpected.Status } return http.StatusInternalServerError default: return http.StatusInternalServerError } } // ToNative returns the error best matching the HTTP status code func ToNative(statusCode int) error { switch statusCode { case http.StatusNotFound: return errdefs.ErrNotFound case http.StatusBadRequest: return errdefs.ErrInvalidArgument case http.StatusConflict: return errdefs.ErrConflict case http.StatusPreconditionFailed: return errdefs.ErrFailedPrecondition case http.StatusUnauthorized: return errdefs.ErrUnauthenticated case http.StatusForbidden: return errdefs.ErrPermissionDenied case http.StatusNotModified: return errdefs.ErrNotModified case http.StatusTooManyRequests: return errdefs.ErrResourceExhausted case http.StatusInternalServerError: return errdefs.ErrInternal case http.StatusNotImplemented: return errdefs.ErrNotImplemented case http.StatusServiceUnavailable: return errdefs.ErrUnavailable default: return cause.ErrUnexpectedStatus{Status: statusCode} } } errdefs-pkg-v0.3.0/pkg/errhttp/http_test.go000066400000000000000000000050151470122746100207340ustar00rootroot00000000000000/* 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 errhttp import ( "errors" "net/http" "testing" "github.com/containerd/errdefs" ) func TestHTTPNilInput(t *testing.T) { if rc := ToHTTP(nil); rc != http.StatusInternalServerError { t.Fatalf("Expected %d error, got %d", http.StatusInternalServerError, rc) } } func TestHTTPRoundTrip(t *testing.T) { errShouldLeaveAlone := errors.New("unknown to package") for _, testcase := range []struct { input error cause error str string }{ { input: errdefs.ErrInvalidArgument, cause: errdefs.ErrInvalidArgument, }, { input: errdefs.ErrNotFound, cause: errdefs.ErrNotFound, }, { input: errdefs.ErrConflict, cause: errdefs.ErrConflict, }, { input: errdefs.ErrNotModified, cause: errdefs.ErrNotModified, }, { input: errdefs.ErrFailedPrecondition, cause: errdefs.ErrFailedPrecondition, }, { input: errdefs.ErrUnauthenticated, cause: errdefs.ErrUnauthenticated, }, { input: errdefs.ErrPermissionDenied, cause: errdefs.ErrPermissionDenied, }, { input: errdefs.ErrResourceExhausted, cause: errdefs.ErrResourceExhausted, }, { input: errdefs.ErrInternal, cause: errdefs.ErrInternal, }, { input: errdefs.ErrNotImplemented, cause: errdefs.ErrNotImplemented, }, { input: errdefs.ErrUnavailable, cause: errdefs.ErrUnavailable, }, { input: errShouldLeaveAlone, cause: errdefs.ErrInternal, }, } { t.Run(testcase.input.Error(), func(t *testing.T) { t.Logf("input: %v", testcase.input) httpErr := ToHTTP(testcase.input) t.Logf("http: %v", httpErr) ferr := ToNative(httpErr) t.Logf("recovered: %v", ferr) if !errors.Is(ferr, testcase.cause) { t.Fatalf("unexpected cause: !errors.Is(%v, %v)", ferr, testcase.cause) } expected := testcase.str if expected == "" { expected = testcase.cause.Error() } if ferr.Error() != expected { t.Fatalf("unexpected string: %q != %q", ferr.Error(), expected) } }) } } errdefs-pkg-v0.3.0/pkg/go.mod000066400000000000000000000006001470122746100160000ustar00rootroot00000000000000module github.com/containerd/errdefs/pkg go 1.22 require ( github.com/containerd/errdefs v0.3.0 github.com/containerd/typeurl/v2 v2.2.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 google.golang.org/grpc v1.67.0 google.golang.org/protobuf v1.34.2 ) require ( github.com/gogo/protobuf v1.3.2 // indirect golang.org/x/sys v0.24.0 // indirect ) errdefs-pkg-v0.3.0/pkg/go.sum000066400000000000000000000114211470122746100160300ustar00rootroot00000000000000github.com/containerd/errdefs v0.2.0 h1:XllDESRfJtVrMwMmR2mCabxyvBK4UlbyyiWI3MvRw0o= github.com/containerd/errdefs v0.2.0/go.mod h1:C28ixlj3dKhQS9hsQ13b+HIb4X7+s2G4FYhbSPcRDLM= github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso= github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= errdefs-pkg-v0.3.0/pkg/internal/000077500000000000000000000000001470122746100165125ustar00rootroot00000000000000errdefs-pkg-v0.3.0/pkg/internal/cause/000077500000000000000000000000001470122746100176125ustar00rootroot00000000000000errdefs-pkg-v0.3.0/pkg/internal/cause/cause.go000066400000000000000000000017311470122746100212430ustar00rootroot00000000000000/* 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 cause is used to define root causes for errors // common to errors packages like grpc and http. package cause import "fmt" type ErrUnexpectedStatus struct { Status int } const UnexpectedStatusPrefix = "unexpected status " func (e ErrUnexpectedStatus) Error() string { return fmt.Sprintf("%s%d", UnexpectedStatusPrefix, e.Status) } func (ErrUnexpectedStatus) Unknown() {} errdefs-pkg-v0.3.0/pkg/internal/types/000077500000000000000000000000001470122746100176565ustar00rootroot00000000000000errdefs-pkg-v0.3.0/pkg/internal/types/collapsible.go000066400000000000000000000026351470122746100225040ustar00rootroot00000000000000/* 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 types import "fmt" // CollapsibleError indicates the error should be collapsed type CollapsibleError interface { CollapseError() } // CollapsedError returns a new error with the collapsed // error returned on unwrapped or when formatted with "%+v" func CollapsedError(err error, collapsed ...error) error { return collapsedError{err, collapsed} } type collapsedError struct { error collapsed []error } func (c collapsedError) Unwrap() []error { return append([]error{c.error}, c.collapsed...) } func (c collapsedError) Format(s fmt.State, verb rune) { switch verb { case 'v': if s.Flag('+') { fmt.Fprintf(s, "%+v", c.error) for _, err := range c.collapsed { fmt.Fprintf(s, "\n%+v", err) } return } fallthrough case 's': fmt.Fprint(s, c.Error()) case 'q': fmt.Fprintf(s, "%q", c.Error()) } } errdefs-pkg-v0.3.0/pkg/stack/000077500000000000000000000000001470122746100160035ustar00rootroot00000000000000errdefs-pkg-v0.3.0/pkg/stack/stack.go000066400000000000000000000153441470122746100174460ustar00rootroot00000000000000/* 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 stack import ( "context" "encoding/json" "errors" "fmt" "os" "path" "runtime" "strings" "sync/atomic" "unsafe" "github.com/containerd/typeurl/v2" "github.com/containerd/errdefs/pkg/internal/types" ) func init() { typeurl.Register((*stack)(nil), "github.com/containerd/errdefs", "stack+json") } var ( // Version is version of running process Version string = "dev" // Revision is the specific revision of the running process Revision string = "dirty" ) type stack struct { decoded *Trace callers []uintptr helpers []uintptr } // Trace is a stack trace along with process information about the source type Trace struct { Version string `json:"version,omitempty"` Revision string `json:"revision,omitempty"` Cmdline []string `json:"cmdline,omitempty"` Frames []Frame `json:"frames,omitempty"` Pid int32 `json:"pid,omitempty"` } // Frame is a single frame of the trace representing a line of code type Frame struct { Name string `json:"Name,omitempty"` File string `json:"File,omitempty"` Line int32 `json:"Line,omitempty"` } func (f Frame) Format(s fmt.State, verb rune) { switch verb { case 'v': switch { case s.Flag('+'): fmt.Fprintf(s, "%s\n\t%s:%d\n", f.Name, f.File, f.Line) default: fmt.Fprint(s, f.Name) } case 's': fmt.Fprint(s, path.Base(f.Name)) case 'q': fmt.Fprintf(s, "%q", path.Base(f.Name)) } } // callers returns the current stack, skipping over the number of frames mentioned // Frames with skip=0: // // frame[0] runtime.Callers // frame[1] github.com/containerd/errdefs/stack.callers // frame[2] (Use skip=2 to have this be first frame) func callers(skip int) *stack { const depth = 32 var pcs [depth]uintptr n := runtime.Callers(skip, pcs[:]) return &stack{ callers: pcs[0:n], } } func (s *stack) getDecoded() *Trace { if s.decoded == nil { var unsafeDecoded = (*unsafe.Pointer)(unsafe.Pointer(&s.decoded)) var helpers map[string]struct{} if len(s.helpers) > 0 { helpers = make(map[string]struct{}) frames := runtime.CallersFrames(s.helpers) for { frame, more := frames.Next() helpers[frame.Function] = struct{}{} if !more { break } } } f := make([]Frame, 0, len(s.callers)) if len(s.callers) > 0 { frames := runtime.CallersFrames(s.callers) for { frame, more := frames.Next() if _, ok := helpers[frame.Function]; !ok { f = append(f, Frame{ Name: frame.Function, File: frame.File, Line: int32(frame.Line), }) } if !more { break } } } t := Trace{ Version: Version, Revision: Revision, Cmdline: os.Args, Frames: f, Pid: int32(os.Getpid()), } atomic.StorePointer(unsafeDecoded, unsafe.Pointer(&t)) } return s.decoded } func (s *stack) Error() string { return fmt.Sprintf("%+v", s.getDecoded()) } func (s *stack) MarshalJSON() ([]byte, error) { return json.Marshal(s.getDecoded()) } func (s *stack) UnmarshalJSON(b []byte) error { var unsafeDecoded = (*unsafe.Pointer)(unsafe.Pointer(&s.decoded)) var t Trace if err := json.Unmarshal(b, &t); err != nil { return err } atomic.StorePointer(unsafeDecoded, unsafe.Pointer(&t)) return nil } func (s *stack) Format(st fmt.State, verb rune) { switch verb { case 'v': if st.Flag('+') { t := s.getDecoded() fmt.Fprintf(st, "%d %s %s\n", t.Pid, t.Version, strings.Join(t.Cmdline, " ")) for _, f := range t.Frames { f.Format(st, verb) } fmt.Fprintln(st) return } } } func (s *stack) StackTrace() Trace { return *s.getDecoded() } func (s *stack) CollapseError() {} // ErrStack returns a new error for the callers stack, // this can be wrapped or joined into an existing error. // NOTE: When joined with errors.Join, the stack // will show up in the error string output. // Use with `stack.Join` to force addition of the // error stack. func ErrStack() error { return callers(3) } // Join adds a stack if there is no stack included to the errors // and returns a joined error with the stack hidden from the error // output. The stack error shows up when Unwrapped or formatted // with `%+v`. func Join(errs ...error) error { return joinErrors(nil, errs) } // WithStack will check if the error already has a stack otherwise // return a new error with the error joined with a stack error // Any helpers will be skipped. func WithStack(ctx context.Context, errs ...error) error { return joinErrors(ctx.Value(helperKey{}), errs) } func joinErrors(helperVal any, errs []error) error { var filtered []error var collapsible []error var hasStack bool for _, err := range errs { if err != nil { if !hasStack && hasLocalStackTrace(err) { hasStack = true } if _, ok := err.(types.CollapsibleError); ok { collapsible = append(collapsible, err) } else { filtered = append(filtered, err) } } } if len(filtered) == 0 { return nil } if !hasStack { s := callers(4) if helpers, ok := helperVal.([]uintptr); ok { s.helpers = helpers } collapsible = append(collapsible, s) } var err error if len(filtered) > 1 { err = errors.Join(filtered...) } else { err = filtered[0] } if len(collapsible) == 0 { return err } return types.CollapsedError(err, collapsible...) } func hasLocalStackTrace(err error) bool { switch e := err.(type) { case *stack: return true case interface{ Unwrap() error }: if hasLocalStackTrace(e.Unwrap()) { return true } case interface{ Unwrap() []error }: for _, ue := range e.Unwrap() { if hasLocalStackTrace(ue) { return true } } } // TODO: Consider if pkg/errors compatibility is needed // NOTE: This was implemented before the standard error package // so it may unwrap and have this interface. //if _, ok := err.(interface{ StackTrace() pkgerrors.StackTrace }); ok { // return true //} return false } type helperKey struct{} // WithHelper marks the context as from a helper function // This will add an additional skip to the error stack trace func WithHelper(ctx context.Context) context.Context { helpers, _ := ctx.Value(helperKey{}).([]uintptr) var pcs [1]uintptr n := runtime.Callers(2, pcs[:]) if n == 1 { ctx = context.WithValue(ctx, helperKey{}, append(helpers, pcs[0])) } return ctx } errdefs-pkg-v0.3.0/pkg/stack/stack_test.go000066400000000000000000000061641470122746100205050ustar00rootroot00000000000000/* 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 stack import ( "context" "errors" "fmt" "strings" "testing" ) func TestStack(t *testing.T) { s := callers(2) if len(s.callers) == 0 { t.Fatalf("expected callers, got:\n%v", s) } tr := s.getDecoded() if len(tr.Frames) != len(s.callers) { t.Fatalf("expected 1 frame, got %d", len(tr.Frames)) } if name := tr.Frames[0].Name; !strings.HasSuffix(name, "."+t.Name()) { t.Fatalf("unexpected frame: %s\n%v", name, s) } } func TestCollapsed(t *testing.T) { checkError := func(err error, expected string) { t.Helper() if err.Error() != expected { t.Fatalf("unexpected error string %q, expected %q", err.Error(), expected) } if printed := fmt.Sprintf("%v", err); printed != expected { t.Fatalf("unexpected error string %q, expected %q", printed, expected) } if printed := fmt.Sprintf("%+v", err); !strings.HasPrefix(printed, expected) || !strings.Contains(printed, t.Name()) { t.Fatalf("unexpected error string %q, expected %q with stack containing %q", printed, expected, t.Name()) } } expected := "some error" checkError(Join(errors.New(expected)), expected) checkError(Join(errors.New(expected), ErrStack()), expected) checkError(WithStack(context.Background(), errors.New(expected)), expected) } func TestHelpers(t *testing.T) { checkError := func(err error, expected string, withHelper bool) { t.Helper() if err.Error() != expected { t.Fatalf("unexpected error string %q, expected %q", err.Error(), expected) } if printed := fmt.Sprintf("%v", err); printed != expected { t.Fatalf("unexpected error string %q, expected %q", printed, expected) } printed := fmt.Sprintf("%+v", err) if !strings.HasPrefix(printed, expected) || !strings.Contains(printed, t.Name()) { t.Fatalf("unexpected error string %q, expected %q with stack containing %q", printed, expected, t.Name()) } if withHelper { if !strings.Contains(printed, "testHelper") { t.Fatalf("unexpected error string, expected stack containing testHelper:\n%s", printed) } } else if strings.Contains(printed, "testHelper") { t.Fatalf("unexpected error string, expected stack with no containing testHelper:\n%s", printed) } } expected := "some error" checkError(Join(errors.New(expected)), expected, false) checkError(testHelper(expected, false), expected, true) checkError(testHelper(expected, true), expected, false) } func testHelper(msg string, withHelper bool) error { if withHelper { return WithStack(WithHelper(context.Background()), errors.New(msg)) } else { return WithStack(context.Background(), errors.New(msg)) } } errdefs-pkg-v0.3.0/resolve.go000066400000000000000000000070771470122746100161360ustar00rootroot00000000000000/* 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 customMessage: err = e.err case unknown: return ErrUnknown case invalidParameter: return ErrInvalidArgument case notFound: return ErrNotFound case alreadyExists: return ErrAlreadyExists case forbidden: return ErrPermissionDenied case resourceExhausted: return ErrResourceExhausted case failedPrecondition: return ErrFailedPrecondition case conflict: return ErrConflict case notModified: return ErrNotModified case aborted: return ErrAborted case errOutOfRange: return ErrOutOfRange 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 case interface{ Is(error) bool }: for _, target := range []error{ErrUnknown, ErrInvalidArgument, ErrNotFound, ErrAlreadyExists, ErrPermissionDenied, ErrResourceExhausted, ErrFailedPrecondition, ErrConflict, ErrNotModified, ErrAborted, ErrOutOfRange, ErrNotImplemented, ErrInternal, ErrUnavailable, ErrDataLoss, ErrUnauthenticated, context.DeadlineExceeded, context.Canceled} { if e.Is(target) { return target } } return nil default: return nil } } } errdefs-pkg-v0.3.0/resolve_test.go000066400000000000000000000061421470122746100171650ustar00rootroot00000000000000/* 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}, {ErrNotFound.WithMessage("something else"), ErrNotFound}, {wrap(ErrNotFound.WithMessage("something else")), ErrNotFound}, {errors.Join(ErrNotFound.WithMessage("something else"), ErrPermissionDenied), ErrNotFound}, } { 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() {}