
Now that we have most of the services required for use with containerd, it was found that common patterns were used throughout services. By defining a central `errdefs` package, we ensure that services will map errors to and from grpc consistently and cleanly. One can decorate an error with as much context as necessary, using `pkg/errors` and still have the error mapped correctly via grpc. We make a few sacrifices. At this point, the common errors we use across the repository all map directly to grpc error codes. While this seems positively crazy, it actually works out quite well. The error conditions that were specific weren't super necessary and the ones that were necessary now simply have better context information. We lose the ability to add new codes, but this constraint may not be a bad thing. Effectively, as long as one uses the errors defined in `errdefs`, the error class will be mapped correctly across the grpc boundary and everything will be good. If you don't use those definitions, the error maps to "unknown" and the error message is preserved. Signed-off-by: Stephen J Day <stephen.day@docker.com>
107 lines
2.5 KiB
Go
107 lines
2.5 KiB
Go
package errdefs
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
// ToGRPC will attempt to map the backend containerd error into a grpc error,
|
|
// using the original error message as a description.
|
|
//
|
|
// Further information may be extracted from certain errors depending on their
|
|
// type.
|
|
//
|
|
// If the error is unmapped, the original error will be returned to be handled
|
|
// by the regular grpc error handling stack.
|
|
func ToGRPC(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
if isGRPCError(err) {
|
|
// error has already been mapped to grpc
|
|
return err
|
|
}
|
|
|
|
switch {
|
|
case IsInvalidArgument(err):
|
|
return grpc.Errorf(codes.InvalidArgument, err.Error())
|
|
case IsNotFound(err):
|
|
return grpc.Errorf(codes.NotFound, err.Error())
|
|
case IsAlreadyExists(err):
|
|
return grpc.Errorf(codes.AlreadyExists, err.Error())
|
|
case IsFailedPrecondition(err):
|
|
return grpc.Errorf(codes.FailedPrecondition, err.Error())
|
|
case IsUnavailable(err):
|
|
return grpc.Errorf(codes.Unavailable, err.Error())
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// ToGRPCf maps the error to grpc error codes, assembling the formatting string
|
|
// and combining it with the target error string.
|
|
//
|
|
// This is equivalent to errors.ToGRPC(errors.Wrapf(err, format, args...))
|
|
func ToGRPCf(err error, format string, args ...interface{}) error {
|
|
return ToGRPC(errors.Wrapf(err, format, args...))
|
|
}
|
|
|
|
func FromGRPC(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
var cls error // divide these into error classes, becomes the cause
|
|
|
|
switch grpc.Code(err) {
|
|
case codes.InvalidArgument:
|
|
cls = ErrInvalidArgument
|
|
case codes.AlreadyExists:
|
|
cls = ErrAlreadyExists
|
|
case codes.NotFound:
|
|
cls = ErrNotFound
|
|
case codes.Unavailable:
|
|
cls = ErrUnavailable
|
|
case codes.FailedPrecondition:
|
|
cls = ErrFailedPrecondition
|
|
default:
|
|
cls = ErrUnknown
|
|
}
|
|
|
|
if cls != nil {
|
|
msg := rebaseMessage(cls, err)
|
|
if msg != "" {
|
|
err = errors.Wrapf(cls, msg)
|
|
} else {
|
|
err = cls
|
|
}
|
|
}
|
|
|
|
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, err error) string {
|
|
desc := grpc.ErrorDesc(err)
|
|
clss := cls.Error()
|
|
if desc == clss {
|
|
return ""
|
|
}
|
|
|
|
return strings.TrimSuffix(desc, ": "+clss)
|
|
}
|
|
|
|
func isGRPCError(err error) bool {
|
|
_, ok := status.FromError(err)
|
|
return ok
|
|
}
|