Skip to content

Commit

Permalink
Split gRPC and HTTP error utility into seperate packages
Browse files Browse the repository at this point in the history
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 <macedonv@amazon.com>
  • Loading branch information
austinvazquez committed Apr 25, 2024
1 parent 98ae5ec commit 334de29
Show file tree
Hide file tree
Showing 6 changed files with 319 additions and 121 deletions.
16 changes: 0 additions & 16 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
90 changes: 49 additions & 41 deletions grpc.go → grpc/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 grpc 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 grpc

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
91 changes: 73 additions & 18 deletions grpc_test.go → grpc/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 grpc

import (
"context"
Expand All @@ -24,8 +24,20 @@ import (

"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/containerd/errdefs"
"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 +47,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 +135,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)),

Check failure on line 148 in grpc/grpc_test.go

View workflow job for this annotation

GitHub Actions / Linters (ubuntu-22.04)

undefined: errhttp (typecheck)
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 +170,4 @@ func TestGRPCRoundTrip(t *testing.T) {
}
})
}

}
Loading

0 comments on commit 334de29

Please sign in to comment.