From fd0e4826e7aee061a8d584680686a8fade5bccc9 Mon Sep 17 00:00:00 2001 From: Austin Vazquez Date: Fri, 9 Feb 2024 06:02:45 +0000 Subject: [PATCH] Split gRPC and HTTP error utility into seperate packages By having seperate packages, users can consume base package without pulling gRPC or HTTP as a dependency if not required. Signed-off-by: Austin Vazquez --- grpc.go => errgrpc/grpc.go | 90 ++++++++++++---------- grpc_test.go => errgrpc/grpc_test.go | 92 +++++++++++++++++----- http.go => errhttp/http.go | 100 +++++++++++++----------- errhttp/http_test.go | 110 +++++++++++++++++++++++++++ errors.go | 16 ---- internal/cause/cause.go | 33 ++++++++ 6 files changed, 320 insertions(+), 121 deletions(-) rename grpc.go => errgrpc/grpc.go (65%) rename grpc_test.go => errgrpc/grpc_test.go (55%) rename http.go => errhttp/http.go (52%) create mode 100644 errhttp/http_test.go create mode 100644 internal/cause/cause.go diff --git a/grpc.go b/errgrpc/grpc.go similarity index 65% rename from grpc.go rename to errgrpc/grpc.go index ef885d8..e9cfce7 100644 --- a/grpc.go +++ b/errgrpc/grpc.go @@ -14,7 +14,12 @@ limitations under the License. */ -package errdefs +// 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" @@ -24,6 +29,9 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + + "github.com/containerd/errdefs" + "github.com/containerd/errdefs/internal/cause" ) // ToGRPC will attempt to map the backend containerd error into a grpc error, @@ -45,37 +53,37 @@ func ToGRPC(err error) error { } switch { - case IsInvalidArgument(err): + case errdefs.IsInvalidArgument(err): return status.Error(codes.InvalidArgument, err.Error()) - case IsNotFound(err): + case errdefs.IsNotFound(err): return status.Error(codes.NotFound, err.Error()) - case IsAlreadyExists(err): + case errdefs.IsAlreadyExists(err): return status.Error(codes.AlreadyExists, err.Error()) - case IsFailedPrecondition(err) || IsConflict(err) || IsNotModified(err): + case errdefs.IsFailedPrecondition(err) || errdefs.IsConflict(err) || errdefs.IsNotModified(err): return status.Error(codes.FailedPrecondition, err.Error()) - case IsUnavailable(err): + case errdefs.IsUnavailable(err): return status.Error(codes.Unavailable, err.Error()) - case IsNotImplemented(err): + case errdefs.IsNotImplemented(err): return status.Error(codes.Unimplemented, err.Error()) - case IsCanceled(err): + case errdefs.IsCanceled(err): return status.Error(codes.Canceled, err.Error()) - case IsDeadlineExceeded(err): + case errdefs.IsDeadlineExceeded(err): return status.Error(codes.DeadlineExceeded, err.Error()) - case IsUnauthorized(err): + case errdefs.IsUnauthorized(err): return status.Error(codes.Unauthenticated, err.Error()) - case IsPermissionDenied(err): + case errdefs.IsPermissionDenied(err): return status.Error(codes.PermissionDenied, err.Error()) - case IsInternal(err): + case errdefs.IsInternal(err): return status.Error(codes.Internal, err.Error()) - case IsDataLoss(err): + case errdefs.IsDataLoss(err): return status.Error(codes.DataLoss, err.Error()) - case IsAborted(err): + case errdefs.IsAborted(err): return status.Error(codes.Aborted, err.Error()) - case IsOutOfRange(err): + case errdefs.IsOutOfRange(err): return status.Error(codes.OutOfRange, err.Error()) - case IsResourceExhausted(err): + case errdefs.IsResourceExhausted(err): return status.Error(codes.ResourceExhausted, err.Error()) - case IsUnknown(err): + case errdefs.IsUnknown(err): return status.Error(codes.Unknown, err.Error()) } @@ -85,13 +93,13 @@ func ToGRPC(err error) error { // ToGRPCf maps the error to grpc error codes, assembling the formatting string // and combining it with the target error string. // -// This is equivalent to errdefs.ToGRPC(fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err)) +// 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)) } -// FromGRPC returns the underlying error from a grpc service based on the grpc error code -func FromGRPC(err error) error { +// ToNative returns the underlying error from a grpc service based on the grpc error code +func ToNative(err error) error { if err == nil { return nil } @@ -102,49 +110,49 @@ func FromGRPC(err error) error { switch code(err) { case codes.InvalidArgument: - cls = ErrInvalidArgument + cls = errdefs.ErrInvalidArgument case codes.AlreadyExists: - cls = ErrAlreadyExists + cls = errdefs.ErrAlreadyExists case codes.NotFound: - cls = ErrNotFound + cls = errdefs.ErrNotFound case codes.Unavailable: - cls = ErrUnavailable + cls = errdefs.ErrUnavailable case codes.FailedPrecondition: - if desc == ErrConflict.Error() || strings.HasSuffix(desc, ": "+ErrConflict.Error()) { - cls = ErrConflict - } else if desc == ErrNotModified.Error() || strings.HasSuffix(desc, ": "+ErrNotModified.Error()) { - cls = ErrNotModified + 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 = ErrFailedPrecondition + cls = errdefs.ErrFailedPrecondition } case codes.Unimplemented: - cls = ErrNotImplemented + cls = errdefs.ErrNotImplemented case codes.Canceled: cls = context.Canceled case codes.DeadlineExceeded: cls = context.DeadlineExceeded case codes.Aborted: - cls = ErrAborted + cls = errdefs.ErrAborted case codes.Unauthenticated: - cls = ErrUnauthenticated + cls = errdefs.ErrUnauthenticated case codes.PermissionDenied: - cls = ErrPermissionDenied + cls = errdefs.ErrPermissionDenied case codes.Internal: - cls = ErrInternal + cls = errdefs.ErrInternal case codes.DataLoss: - cls = ErrDataLoss + cls = errdefs.ErrDataLoss case codes.OutOfRange: - cls = ErrOutOfRange + cls = errdefs.ErrOutOfRange case codes.ResourceExhausted: - cls = ErrResourceExhausted + cls = errdefs.ErrResourceExhausted default: - if idx := strings.LastIndex(desc, unexpectedStatusPrefix); idx > 0 { - if status, err := strconv.Atoi(desc[idx+len(unexpectedStatusPrefix):]); err == nil && status >= 200 && status < 600 { - cls = errUnexpectedStatus{status} + if idx := strings.LastIndex(desc, cause.UnexpectedStatusPrefix); idx > 0 { + if status, err := strconv.Atoi(desc[idx+len(cause.UnexpectedStatusPrefix):]); err == nil && status >= 200 && status < 600 { + cls = cause.ErrUnexpectedStatus{Status: status} } } if cls == nil { - cls = ErrUnknown + cls = errdefs.ErrUnknown } } diff --git a/grpc_test.go b/errgrpc/grpc_test.go similarity index 55% rename from grpc_test.go rename to errgrpc/grpc_test.go index c25d410..7a3778c 100644 --- a/grpc_test.go +++ b/errgrpc/grpc_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package errdefs +package errgrpc import ( "context" @@ -24,8 +24,21 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + + "github.com/containerd/errdefs" + "github.com/containerd/errdefs/errhttp" + "github.com/containerd/errdefs/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") @@ -35,28 +48,72 @@ func TestGRPCRoundTrip(t *testing.T) { str string }{ { - input: ErrAlreadyExists, - cause: ErrAlreadyExists, + 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: ErrNotFound, - cause: ErrNotFound, + 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", ErrFailedPrecondition), - cause: ErrFailedPrecondition, + input: fmt.Errorf("test test test: %w", errdefs.ErrFailedPrecondition), + cause: errdefs.ErrFailedPrecondition, str: "test test test: failed precondition", }, { input: status.Errorf(codes.Unavailable, "should be not available"), - cause: ErrUnavailable, + cause: errdefs.ErrUnavailable, str: "should be not available: unavailable", }, { input: errShouldLeaveAlone, - cause: ErrUnknown, - str: errShouldLeaveAlone.Error() + ": " + ErrUnknown.Error(), + cause: errdefs.ErrUnknown, + str: errShouldLeaveAlone.Error() + ": " + errdefs.ErrUnknown.Error(), }, { input: context.Canceled, @@ -79,18 +136,18 @@ func TestGRPCRoundTrip(t *testing.T) { str: "this is a test deadline exceeded: context deadline exceeded", }, { - input: fmt.Errorf("something conflicted: %w", ErrConflict), - cause: ErrConflict, + input: fmt.Errorf("something conflicted: %w", errdefs.ErrConflict), + cause: errdefs.ErrConflict, str: "something conflicted: conflict", }, { - input: fmt.Errorf("everything is the same: %w", ErrNotModified), - cause: ErrNotModified, + 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", FromHTTP(418)), - cause: errUnexpectedStatus{418}, + input: fmt.Errorf("odd HTTP response: %w", errhttp.ToNative(418)), + cause: cause.ErrUnexpectedStatus{Status: 418}, str: "odd HTTP response: unexpected status 418", }, } { @@ -98,7 +155,7 @@ func TestGRPCRoundTrip(t *testing.T) { t.Logf("input: %v", testcase.input) gerr := ToGRPC(testcase.input) t.Logf("grpc: %v", gerr) - ferr := FromGRPC(gerr) + ferr := ToNative(gerr) t.Logf("recovered: %v", ferr) if !errors.Is(ferr, testcase.cause) { @@ -114,5 +171,4 @@ func TestGRPCRoundTrip(t *testing.T) { } }) } - } diff --git a/http.go b/errhttp/http.go similarity index 52% rename from http.go rename to errhttp/http.go index 90ca12f..dfdf5c8 100644 --- a/http.go +++ b/errhttp/http.go @@ -14,75 +14,83 @@ limitations under the License. */ -package errdefs +// 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" -) -// FromHTTP returns the error best matching the HTTP status code -func FromHTTP(statusCode int) error { - switch statusCode { - case http.StatusNotFound: - return ErrNotFound - case http.StatusBadRequest: - return ErrInvalidArgument - case http.StatusConflict: - return ErrConflict - case http.StatusPreconditionFailed: - return ErrFailedPrecondition - case http.StatusUnauthorized: - return ErrUnauthenticated - case http.StatusForbidden: - return ErrPermissionDenied - case http.StatusNotModified: - return ErrNotModified - case http.StatusTooManyRequests: - return ErrResourceExhausted - case http.StatusInternalServerError: - return ErrInternal - case http.StatusNotImplemented: - return ErrNotImplemented - case http.StatusServiceUnavailable: - return ErrUnavailable - default: - return errUnexpectedStatus{statusCode} - } -} + "github.com/containerd/errdefs" + "github.com/containerd/errdefs/internal/cause" +) // ToHTTP returns the best status code for the given error func ToHTTP(err error) int { switch { - case IsNotFound(err): + case errdefs.IsNotFound(err): return http.StatusNotFound - case IsInvalidArgument(err): + case errdefs.IsInvalidArgument(err): return http.StatusBadRequest - case IsConflict(err): + case errdefs.IsConflict(err): return http.StatusConflict - case IsNotModified(err): + case errdefs.IsNotModified(err): return http.StatusNotModified - case IsFailedPrecondition(err): + case errdefs.IsFailedPrecondition(err): return http.StatusPreconditionFailed - case IsUnauthorized(err): + case errdefs.IsUnauthorized(err): return http.StatusUnauthorized - case IsPermissionDenied(err): + case errdefs.IsPermissionDenied(err): return http.StatusForbidden - case IsResourceExhausted(err): + case errdefs.IsResourceExhausted(err): return http.StatusTooManyRequests - case IsInternal(err): + case errdefs.IsInternal(err): return http.StatusInternalServerError - case IsNotImplemented(err): + case errdefs.IsNotImplemented(err): return http.StatusNotImplemented - case IsUnavailable(err): + case errdefs.IsUnavailable(err): return http.StatusServiceUnavailable - case IsUnknown(err): - var unexpected errUnexpectedStatus - if errors.As(err, &unexpected) && unexpected.status >= 200 && unexpected.status < 600 { - return unexpected.status + 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} + } +} diff --git a/errhttp/http_test.go b/errhttp/http_test.go new file mode 100644 index 0000000..a6ea320 --- /dev/null +++ b/errhttp/http_test.go @@ -0,0 +1,110 @@ +/* + 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) + } + }) + } +} diff --git a/errors.go b/errors.go index 0bcc2a6..69525e5 100644 --- a/errors.go +++ b/errors.go @@ -21,15 +21,11 @@ // // To detect an error class, use the IsXXX functions to tell whether an error // is of a certain type. -// -// The functions ToGRPC and FromGRPC can be used to map server-side and -// client-side errors to the correct types. package errdefs import ( "context" "errors" - "fmt" ) // Definitions of common error types used throughout containerd. All containerd @@ -73,18 +69,6 @@ func (errUnknown) Error() string { return "unknown" } func (errUnknown) Unknown() {} -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() {} - // unknown maps to Moby's "ErrUnknown" type unknown interface { Unknown() diff --git a/internal/cause/cause.go b/internal/cause/cause.go new file mode 100644 index 0000000..d88756b --- /dev/null +++ b/internal/cause/cause.go @@ -0,0 +1,33 @@ +/* + 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() {}