@@ -23,71 +23,175 @@ package errgrpc
2323
2424import (
2525 "context"
26+ "errors"
2627 "fmt"
28+ "reflect"
2729 "strconv"
2830 "strings"
2931
32+ spb "google.golang.org/genproto/googleapis/rpc/status"
3033 "google.golang.org/grpc/codes"
3134 "google.golang.org/grpc/status"
35+ "google.golang.org/protobuf/proto"
36+ "google.golang.org/protobuf/protoadapt"
37+ "google.golang.org/protobuf/types/known/anypb"
38+
39+ "github.com/containerd/typeurl/v2"
3240
3341 "github.com/containerd/errdefs"
3442 "github.com/containerd/errdefs/internal/cause"
43+ "github.com/containerd/errdefs/internal/types"
3544)
3645
37- // ToGRPC will attempt to map the backend containerd error into a grpc error,
38- // using the original error message as a description.
46+ // ToGRPC will attempt to map the error into a grpc error, from the error types
47+ // defined in the the errdefs package and attempign to preserve the original
48+ // description. Any type which does not resolve to a defined error type will
49+ // be assigned the unknown error code.
3950//
4051// Further information may be extracted from certain errors depending on their
41- // type.
52+ // type. The grpc error details will be used to attempt to preserve as much of
53+ // the error structures and types as possible.
54+ //
55+ // Errors which can be marshaled using protobuf or typeurl will be considered
56+ // for including as GRPC error details.
57+ // Additionally, use the following interfaces in errors to preserve custom types:
4258//
43- // If the error is unmapped, the original error will be returned to be handled
44- // by the regular grpc error handling stack.
59+ // WrapError(error) error - Used to wrap the previous error
60+ // JoinErrors(...error) error - Used to join all previous errors
61+ // CollapseError() - Used for errors which carry information but
62+ // should not have their error message shown.
4563func ToGRPC (err error ) error {
4664 if err == nil {
4765 return nil
4866 }
4967
50- if isGRPCError (err ) {
68+ if _ , ok := status . FromError (err ); ok {
5169 // error has already been mapped to grpc
5270 return err
5371 }
72+ st := statusFromError (err )
73+ if st != nil {
74+ if details := errorDetails (err , false ); len (details ) > 0 {
75+ if ds , _ := st .WithDetails (details ... ); ds != nil {
76+ st = ds
77+ }
78+ }
79+ err = st .Err ()
80+ }
81+ return err
82+ }
5483
55- switch {
56- case errdefs .IsInvalidArgument (err ):
57- return status .Error (codes .InvalidArgument , err .Error ())
58- case errdefs .IsNotFound (err ):
59- return status .Error (codes .NotFound , err .Error ())
60- case errdefs .IsAlreadyExists (err ):
61- return status .Error (codes .AlreadyExists , err .Error ())
62- case errdefs .IsFailedPrecondition (err ) || errdefs .IsConflict (err ) || errdefs .IsNotModified (err ):
63- return status .Error (codes .FailedPrecondition , err .Error ())
64- case errdefs .IsUnavailable (err ):
65- return status .Error (codes .Unavailable , err .Error ())
66- case errdefs .IsNotImplemented (err ):
67- return status .Error (codes .Unimplemented , err .Error ())
68- case errdefs .IsCanceled (err ):
69- return status .Error (codes .Canceled , err .Error ())
70- case errdefs .IsDeadlineExceeded (err ):
71- return status .Error (codes .DeadlineExceeded , err .Error ())
72- case errdefs .IsUnauthorized (err ):
73- return status .Error (codes .Unauthenticated , err .Error ())
74- case errdefs .IsPermissionDenied (err ):
75- return status .Error (codes .PermissionDenied , err .Error ())
76- case errdefs .IsInternal (err ):
77- return status .Error (codes .Internal , err .Error ())
78- case errdefs .IsDataLoss (err ):
79- return status .Error (codes .DataLoss , err .Error ())
80- case errdefs .IsAborted (err ):
81- return status .Error (codes .Aborted , err .Error ())
82- case errdefs .IsOutOfRange (err ):
83- return status .Error (codes .OutOfRange , err .Error ())
84- case errdefs .IsResourceExhausted (err ):
85- return status .Error (codes .ResourceExhausted , err .Error ())
86- case errdefs .IsUnknown (err ):
87- return status .Error (codes .Unknown , err .Error ())
84+ func statusFromError (err error ) * status.Status {
85+ switch errdefs .Resolve (err ) {
86+ case errdefs .ErrInvalidArgument :
87+ return status .New (codes .InvalidArgument , err .Error ())
88+ case errdefs .ErrNotFound :
89+ return status .New (codes .NotFound , err .Error ())
90+ case errdefs .ErrAlreadyExists :
91+ return status .New (codes .AlreadyExists , err .Error ())
92+ case errdefs .ErrPermissionDenied :
93+ return status .New (codes .PermissionDenied , err .Error ())
94+ case errdefs .ErrResourceExhausted :
95+ return status .New (codes .ResourceExhausted , err .Error ())
96+ case errdefs .ErrFailedPrecondition , errdefs .ErrConflict , errdefs .ErrNotModified :
97+ return status .New (codes .FailedPrecondition , err .Error ())
98+ case errdefs .ErrAborted :
99+ return status .New (codes .Aborted , err .Error ())
100+ case errdefs .ErrOutOfRange :
101+ return status .New (codes .OutOfRange , err .Error ())
102+ case errdefs .ErrNotImplemented :
103+ return status .New (codes .Unimplemented , err .Error ())
104+ case errdefs .ErrInternal :
105+ return status .New (codes .Internal , err .Error ())
106+ case errdefs .ErrUnavailable :
107+ return status .New (codes .Unavailable , err .Error ())
108+ case errdefs .ErrDataLoss :
109+ return status .New (codes .DataLoss , err .Error ())
110+ case errdefs .ErrUnauthenticated :
111+ return status .New (codes .Unauthenticated , err .Error ())
112+ case context .DeadlineExceeded :
113+ return status .New (codes .DeadlineExceeded , err .Error ())
114+ case context .Canceled :
115+ return status .New (codes .Canceled , err .Error ())
116+ case errdefs .ErrUnknown :
117+ return status .New (codes .Unknown , err .Error ())
88118 }
119+ return nil
120+ }
89121
90- return err
122+ // errorDetails returns an array of errors which make up the provided error.
123+ // If firstIncluded is true, then all encodable errors will be used, otherwise
124+ // the first error in an error list will be not be used, to account for the
125+ // the base status error which details are added to via wrap or join.
126+ //
127+ // The errors are ordered in way that they can be applied in order by either
128+ // wrapping or joining the errors to recreate an error with the same structure
129+ // when `WrapError` and `JoinErrors` interfaces are used.
130+ //
131+ // The intent is that when re-applying the errors to create a single error, the
132+ // results of calls to `Error()`, `errors.Is`, `errors.As`, and "%+v" formatting
133+ // is the same as the original error.
134+ func errorDetails (err error , firstIncluded bool ) []protoadapt.MessageV1 {
135+ switch uerr := err .(type ) {
136+ case interface { Unwrap () error }:
137+ details := errorDetails (uerr .Unwrap (), firstIncluded )
138+
139+ // If the type is able to wrap, then include if proto
140+ if _ , ok := err .(interface { WrapError (error ) error }); ok {
141+ // Get proto message
142+ if protoErr := toProtoMessage (err ); protoErr != nil {
143+ details = append (details , protoErr )
144+ }
145+ }
146+
147+ return details
148+ case interface { Unwrap () []error }:
149+ var details []protoadapt.MessageV1
150+ for i , e := range uerr .Unwrap () {
151+ details = append (details , errorDetails (e , firstIncluded || i > 0 )... )
152+ }
153+
154+ if _ , ok := err .(interface { JoinErrors (... error ) error }); ok {
155+ // Get proto message
156+ if protoErr := toProtoMessage (err ); protoErr != nil {
157+ details = append (details , protoErr )
158+ }
159+ }
160+ return details
161+ }
162+
163+ if firstIncluded {
164+ if protoErr := toProtoMessage (err ); protoErr != nil {
165+ return []protoadapt.MessageV1 {protoErr }
166+ }
167+ if gs , ok := status .FromError (ToGRPC (err )); ok {
168+ return []protoadapt.MessageV1 {gs .Proto ()}
169+ }
170+ // TODO: Else include unknown extra error type?
171+ }
172+
173+ return nil
174+ }
175+
176+ func toProtoMessage (err error ) protoadapt.MessageV1 {
177+ // Do not double encode proto messages, otherwise use Any
178+ if pm , ok := err .(protoadapt.MessageV1 ); ok {
179+ return pm
180+ }
181+ if pm , ok := err .(proto.Message ); ok {
182+ return protoadapt .MessageV1Of (pm )
183+ }
184+
185+ if reflect .TypeOf (err ).Kind () == reflect .Ptr {
186+ a , aerr := typeurl .MarshalAny (err )
187+ if aerr == nil {
188+ return & anypb.Any {
189+ TypeUrl : a .GetTypeUrl (),
190+ Value : a .GetValue (),
191+ }
192+ }
193+ }
194+ return nil
91195}
92196
93197// ToGRPCf maps the error to grpc error codes, assembling the formatting string
@@ -98,17 +202,32 @@ func ToGRPCf(err error, format string, args ...interface{}) error {
98202 return ToGRPC (fmt .Errorf ("%s: %w" , fmt .Sprintf (format , args ... ), err ))
99203}
100204
101- // ToNative returns the underlying error from a grpc service based on the grpc error code
205+ // ToNative returns the underlying error from a grpc service based on the grpc
206+ // error code. The grpc details are used to add wrap the error in more context
207+ // or support multiple errors.
102208func ToNative (err error ) error {
103209 if err == nil {
104210 return nil
105211 }
106212
107- desc := errDesc (err )
213+ s , isGRPC := status .FromError (err )
214+
215+ var (
216+ desc string
217+ code codes.Code
218+ )
219+
220+ if isGRPC {
221+ desc = s .Message ()
222+ code = s .Code ()
223+ } else {
224+ desc = err .Error ()
225+ code = codes .Unknown
226+ }
108227
109228 var cls error // divide these into error classes, becomes the cause
110229
111- switch code ( err ) {
230+ switch code {
112231 case codes .InvalidArgument :
113232 cls = errdefs .ErrInvalidArgument
114233 case codes .AlreadyExists :
@@ -118,6 +237,10 @@ func ToNative(err error) error {
118237 case codes .Unavailable :
119238 cls = errdefs .ErrUnavailable
120239 case codes .FailedPrecondition :
240+ // TODO: Has suffix is not sufficient for conflict and not modified
241+ // Message should start with ": " or be at beginning of a line
242+ // Message should end with ": " or be at the end of a line
243+ // Compile a regex
121244 if desc == errdefs .ErrConflict .Error () || strings .HasSuffix (desc , ": " + errdefs .ErrConflict .Error ()) {
122245 cls = errdefs .ErrConflict
123246 } else if desc == errdefs .ErrNotModified .Error () || strings .HasSuffix (desc , ": " + errdefs .ErrNotModified .Error ()) {
@@ -147,7 +270,7 @@ func ToNative(err error) error {
147270 cls = errdefs .ErrResourceExhausted
148271 default :
149272 if idx := strings .LastIndex (desc , cause .UnexpectedStatusPrefix ); idx > 0 {
150- if status , err := strconv .Atoi (desc [idx + len (cause .UnexpectedStatusPrefix ):]); err == nil && status >= 200 && status < 600 {
273+ if status , uerr := strconv .Atoi (desc [idx + len (cause .UnexpectedStatusPrefix ):]); uerr == nil && status >= 200 && status < 600 {
151274 cls = cause.ErrUnexpectedStatus {Status : status }
152275 }
153276 }
@@ -157,10 +280,59 @@ func ToNative(err error) error {
157280 }
158281
159282 msg := rebaseMessage (cls , desc )
160- if msg != "" {
283+ if msg == "" {
284+ err = cls
285+ } else if msg != desc {
161286 err = fmt .Errorf ("%s: %w" , msg , cls )
287+ } else if wm , ok := cls .(interface { WithMessage (string ) error }); ok {
288+ err = wm .WithMessage (msg )
162289 } else {
163- err = cls
290+ err = fmt .Errorf ("%s: %w" , msg , cls )
291+ }
292+
293+ if isGRPC {
294+ errs := []error {err }
295+ for _ , a := range s .Details () {
296+ var derr error
297+
298+ // First decode error if needed
299+ if s , ok := a .(* spb.Status ); ok {
300+ derr = ToNative (status .ErrorProto (s ))
301+ } else if e , ok := a .(error ); ok {
302+ derr = e
303+ } else if dany , ok := a .(typeurl.Any ); ok {
304+ i , uerr := typeurl .UnmarshalAny (dany )
305+ if uerr == nil {
306+ if e , ok = i .(error ); ok {
307+ derr = e
308+ } else {
309+ derr = fmt .Errorf ("non-error unmarshalled detail: %v" , i )
310+ }
311+ } else {
312+ derr = fmt .Errorf ("error of type %q with failure to unmarshal: %v" , dany .GetTypeUrl (), uerr )
313+ }
314+ } else {
315+ derr = fmt .Errorf ("non-error detail: %v" , a )
316+ }
317+
318+ switch werr := derr .(type ) {
319+ case interface { WrapError (error ) error }:
320+ errs [len (errs )- 1 ] = werr .WrapError (errs [len (errs )- 1 ])
321+ case interface { JoinErrors (... error ) error }:
322+ // TODO: Consider whether this should support joining a subset
323+ errs [0 ] = werr .JoinErrors (errs ... )
324+ case interface { CollapseError () }:
325+ errs [len (errs )- 1 ] = types .CollapsedError (errs [len (errs )- 1 ], derr )
326+ default :
327+ errs = append (errs , derr )
328+ }
329+
330+ }
331+ if len (errs ) > 1 {
332+ err = errors .Join (errs ... )
333+ } else {
334+ err = errs [0 ]
335+ }
164336 }
165337
166338 return err
@@ -179,22 +351,3 @@ func rebaseMessage(cls error, desc string) string {
179351
180352 return strings .TrimSuffix (desc , ": " + clss )
181353}
182-
183- func isGRPCError (err error ) bool {
184- _ , ok := status .FromError (err )
185- return ok
186- }
187-
188- func code (err error ) codes.Code {
189- if s , ok := status .FromError (err ); ok {
190- return s .Code ()
191- }
192- return codes .Unknown
193- }
194-
195- func errDesc (err error ) string {
196- if s , ok := status .FromError (err ); ok {
197- return s .Message ()
198- }
199- return err .Error ()
200- }
0 commit comments