Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split gRPC and HTTP error utility into seperate packages #5

Merged
merged 1 commit into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 49 additions & 41 deletions grpc.go → errgrpc/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand All @@ -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())
}

Expand All @@ -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
}
Expand All @@ -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
}
}

Expand Down
92 changes: 74 additions & 18 deletions grpc_test.go → errgrpc/grpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
limitations under the License.
*/

package errdefs
package errgrpc

import (
"context"
Expand All @@ -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")

Expand All @@ -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,
Expand All @@ -79,26 +136,26 @@ 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",
},
} {
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 := FromGRPC(gerr)
ferr := ToNative(gerr)
t.Logf("recovered: %v", ferr)

if !errors.Is(ferr, testcase.cause) {
Expand All @@ -114,5 +171,4 @@ func TestGRPCRoundTrip(t *testing.T) {
}
})
}

}
Loading
Loading