diff --git a/channel.go b/channel.go index aa8c954..9810695 100644 --- a/channel.go +++ b/channel.go @@ -23,9 +23,9 @@ import ( "net" "sync" + "github.com/containerd/ttrpc/codes" + "github.com/containerd/ttrpc/status" "github.com/pkg/errors" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" ) const ( diff --git a/channel_test.go b/channel_test.go index 2242336..c7b4dcb 100644 --- a/channel_test.go +++ b/channel_test.go @@ -23,9 +23,9 @@ import ( "reflect" "testing" + "github.com/containerd/ttrpc/codes" + "github.com/containerd/ttrpc/status" "github.com/pkg/errors" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" ) func TestReadWriteMessage(t *testing.T) { diff --git a/client.go b/client.go index e816941..c195d11 100644 --- a/client.go +++ b/client.go @@ -26,11 +26,11 @@ import ( "syscall" "time" + "github.com/containerd/ttrpc/codes" + "github.com/containerd/ttrpc/status" "github.com/gogo/protobuf/proto" "github.com/pkg/errors" "github.com/sirupsen/logrus" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" ) // ErrClosed is returned by client methods when the underlying connection is diff --git a/codes/code_string.go b/codes/code_string.go new file mode 100644 index 0000000..27b12a6 --- /dev/null +++ b/codes/code_string.go @@ -0,0 +1,78 @@ +/* + 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. +*/ + +/* + * + * Copyright 2017 gRPC 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 codes + +import "strconv" + +func (c Code) String() string { + switch c { + case OK: + return "OK" + case Canceled: + return "Canceled" + case Unknown: + return "Unknown" + case InvalidArgument: + return "InvalidArgument" + case DeadlineExceeded: + return "DeadlineExceeded" + case NotFound: + return "NotFound" + case AlreadyExists: + return "AlreadyExists" + case PermissionDenied: + return "PermissionDenied" + case ResourceExhausted: + return "ResourceExhausted" + case FailedPrecondition: + return "FailedPrecondition" + case Aborted: + return "Aborted" + case OutOfRange: + return "OutOfRange" + case Unimplemented: + return "Unimplemented" + case Internal: + return "Internal" + case Unavailable: + return "Unavailable" + case DataLoss: + return "DataLoss" + case Unauthenticated: + return "Unauthenticated" + default: + return "Code(" + strconv.FormatInt(int64(c), 10) + ")" + } +} diff --git a/codes/codes.go b/codes/codes.go new file mode 100644 index 0000000..91300c9 --- /dev/null +++ b/codes/codes.go @@ -0,0 +1,214 @@ +/* + 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. +*/ + +/* + * + * Copyright 2014 gRPC 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 codes defines the canonical error codes used by gRPC. It is +// consistent across various languages. +package codes + +import ( + "fmt" + "strconv" +) + +// A Code is an unsigned 32-bit error code as defined in the gRPC spec. +type Code uint32 + +const ( + // OK is returned on success. + OK Code = 0 + + // Canceled indicates the operation was canceled (typically by the caller). + Canceled Code = 1 + + // Unknown error. An example of where this error may be returned is + // if a Status value received from another address space belongs to + // an error-space that is not known in this address space. Also + // errors raised by APIs that do not return enough error information + // may be converted to this error. + Unknown Code = 2 + + // InvalidArgument indicates client specified an invalid argument. + // Note that this differs from FailedPrecondition. It indicates arguments + // that are problematic regardless of the state of the system + // (e.g., a malformed file name). + InvalidArgument Code = 3 + + // DeadlineExceeded means operation expired before completion. + // For operations that change the state of the system, this error may be + // returned even if the operation has completed successfully. For + // example, a successful response from a server could have been delayed + // long enough for the deadline to expire. + DeadlineExceeded Code = 4 + + // NotFound means some requested entity (e.g., file or directory) was + // not found. + NotFound Code = 5 + + // AlreadyExists means an attempt to create an entity failed because one + // already exists. + AlreadyExists Code = 6 + + // PermissionDenied indicates the caller does not have permission to + // execute the specified operation. It must not be used for rejections + // caused by exhausting some resource (use ResourceExhausted + // instead for those errors). It must not be + // used if the caller cannot be identified (use Unauthenticated + // instead for those errors). + PermissionDenied Code = 7 + + // ResourceExhausted indicates some resource has been exhausted, perhaps + // a per-user quota, or perhaps the entire file system is out of space. + ResourceExhausted Code = 8 + + // FailedPrecondition indicates operation was rejected because the + // system is not in a state required for the operation's execution. + // For example, directory to be deleted may be non-empty, an rmdir + // operation is applied to a non-directory, etc. + // + // A litmus test that may help a service implementor in deciding + // between FailedPrecondition, Aborted, and Unavailable: + // (a) Use Unavailable if the client can retry just the failing call. + // (b) Use Aborted if the client should retry at a higher-level + // (e.g., restarting a read-modify-write sequence). + // (c) Use FailedPrecondition if the client should not retry until + // the system state has been explicitly fixed. E.g., if an "rmdir" + // fails because the directory is non-empty, FailedPrecondition + // should be returned since the client should not retry unless + // they have first fixed up the directory by deleting files from it. + // (d) Use FailedPrecondition if the client performs conditional + // REST Get/Update/Delete on a resource and the resource on the + // server does not match the condition. E.g., conflicting + // read-modify-write on the same resource. + FailedPrecondition Code = 9 + + // Aborted indicates the operation was aborted, typically due to a + // concurrency issue like sequencer check failures, transaction aborts, + // etc. + // + // See litmus test above for deciding between FailedPrecondition, + // Aborted, and Unavailable. + Aborted Code = 10 + + // OutOfRange means operation was attempted past the valid range. + // E.g., seeking or reading past end of file. + // + // Unlike InvalidArgument, this error indicates a problem that may + // be fixed if the system state changes. For example, a 32-bit file + // system will generate InvalidArgument if asked to read at an + // offset that is not in the range [0,2^32-1], but it will generate + // OutOfRange if asked to read from an offset past the current + // file size. + // + // There is a fair bit of overlap between FailedPrecondition and + // OutOfRange. We recommend using OutOfRange (the more specific + // error) when it applies so that callers who are iterating through + // a space can easily look for an OutOfRange error to detect when + // they are done. + OutOfRange Code = 11 + + // Unimplemented indicates operation is not implemented or not + // supported/enabled in this service. + Unimplemented Code = 12 + + // Internal errors. Means some invariants expected by underlying + // system has been broken. If you see one of these errors, + // something is very broken. + Internal Code = 13 + + // Unavailable indicates the service is currently unavailable. + // This is a most likely a transient condition and may be corrected + // by retrying with a backoff. Note that it is not always safe to retry + // non-idempotent operations. + // + // See litmus test above for deciding between FailedPrecondition, + // Aborted, and Unavailable. + Unavailable Code = 14 + + // DataLoss indicates unrecoverable data loss or corruption. + DataLoss Code = 15 + + // Unauthenticated indicates the request does not have valid + // authentication credentials for the operation. + Unauthenticated Code = 16 + + _maxCode = 17 +) + +var strToCode = map[string]Code{ + `"OK"`: OK, + `"CANCELLED"`:/* [sic] */ Canceled, + `"UNKNOWN"`: Unknown, + `"INVALID_ARGUMENT"`: InvalidArgument, + `"DEADLINE_EXCEEDED"`: DeadlineExceeded, + `"NOT_FOUND"`: NotFound, + `"ALREADY_EXISTS"`: AlreadyExists, + `"PERMISSION_DENIED"`: PermissionDenied, + `"RESOURCE_EXHAUSTED"`: ResourceExhausted, + `"FAILED_PRECONDITION"`: FailedPrecondition, + `"ABORTED"`: Aborted, + `"OUT_OF_RANGE"`: OutOfRange, + `"UNIMPLEMENTED"`: Unimplemented, + `"INTERNAL"`: Internal, + `"UNAVAILABLE"`: Unavailable, + `"DATA_LOSS"`: DataLoss, + `"UNAUTHENTICATED"`: Unauthenticated, +} + +// UnmarshalJSON unmarshals b into the Code. +func (c *Code) UnmarshalJSON(b []byte) error { + // From json.Unmarshaler: By convention, to approximate the behavior of + // Unmarshal itself, Unmarshalers implement UnmarshalJSON([]byte("null")) as + // a no-op. + if string(b) == "null" { + return nil + } + if c == nil { + return fmt.Errorf("nil receiver passed to UnmarshalJSON") + } + + if ci, err := strconv.ParseUint(string(b), 10, 32); err == nil { + if ci >= _maxCode { + return fmt.Errorf("invalid code: %q", ci) + } + + *c = Code(ci) + return nil + } + + if jc, ok := strToCode[string(b)]; ok { + *c = jc + return nil + } + return fmt.Errorf("invalid code: %q", string(b)) +} diff --git a/codes/codes_test.go b/codes/codes_test.go new file mode 100644 index 0000000..0f1a416 --- /dev/null +++ b/codes/codes_test.go @@ -0,0 +1,100 @@ +/* + 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. +*/ + +/* + * + * Copyright 2017 gRPC 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 codes + +import ( + "encoding/json" + "reflect" + "testing" + + cpb "google.golang.org/genproto/googleapis/rpc/code" +) + +func TestUnmarshalJSON(t *testing.T) { + for s, v := range cpb.Code_value { + want := Code(v) + var got Code + if err := got.UnmarshalJSON([]byte(`"` + s + `"`)); err != nil || got != want { + t.Errorf("got.UnmarshalJSON(%q) = %v; want . got=%v; want %v", s, err, got, want) + } + } +} + +func TestJSONUnmarshal(t *testing.T) { + var got []Code + want := []Code{OK, NotFound, Internal, Canceled} + in := `["OK", "NOT_FOUND", "INTERNAL", "CANCELLED"]` + err := json.Unmarshal([]byte(in), &got) + if err != nil || !reflect.DeepEqual(got, want) { + t.Fatalf("json.Unmarshal(%q, &got) = %v; want . got=%v; want %v", in, err, got, want) + } +} + +func TestUnmarshalJSON_NilReceiver(t *testing.T) { + var got *Code + in := OK.String() + if err := got.UnmarshalJSON([]byte(in)); err == nil { + t.Errorf("got.UnmarshalJSON(%q) = nil; want . got=%v", in, got) + } +} + +func TestUnmarshalJSON_UnknownInput(t *testing.T) { + var got Code + for _, in := range [][]byte{[]byte(""), []byte("xxx"), []byte("Code(17)"), nil} { + if err := got.UnmarshalJSON([]byte(in)); err == nil { + t.Errorf("got.UnmarshalJSON(%q) = nil; want . got=%v", in, got) + } + } +} + +func TestUnmarshalJSON_MarshalUnmarshal(t *testing.T) { + for i := 0; i < _maxCode; i++ { + var cUnMarshaled Code + c := Code(i) + + cJSON, err := json.Marshal(c) + if err != nil { + t.Errorf("marshalling %q failed: %v", c, err) + } + + if err := json.Unmarshal(cJSON, &cUnMarshaled); err != nil { + t.Errorf("unmarshalling code failed: %s", err) + } + + if c != cUnMarshaled { + t.Errorf("code is %q after marshalling/unmarshalling, expected %q", cUnMarshaled, c) + } + } +} diff --git a/codes/doc.go b/codes/doc.go new file mode 100644 index 0000000..01b0266 --- /dev/null +++ b/codes/doc.go @@ -0,0 +1,20 @@ +/* + 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. +*/ + +// This package has been copied over from the v1.23.1 release of go grpc +// There are stability issues with point releases of grpc and ttrpc +// only uses the codes and status package from grpc +package codes diff --git a/go.mod b/go.mod index 2b662a7..036247c 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.12 require ( github.com/gogo/protobuf v1.2.1 + github.com/golang/protobuf v1.3.1 github.com/pkg/errors v0.8.1 github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1 github.com/sirupsen/logrus v1.4.2 diff --git a/go.sum b/go.sum index 66aa0b1..ebccedd 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -34,6 +35,7 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -44,6 +46,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5 h1:f005F/Jl5JLP036x7QIvUVhNTqxvSYwFIiyOh2q12iU= golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/server.go b/server.go index 1d4f1df..3f72493 100644 --- a/server.go +++ b/server.go @@ -25,10 +25,10 @@ import ( "sync/atomic" "time" + "github.com/containerd/ttrpc/codes" + "github.com/containerd/ttrpc/status" "github.com/pkg/errors" "github.com/sirupsen/logrus" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" ) var ( diff --git a/server_test.go b/server_test.go index 1358b08..7164b98 100644 --- a/server_test.go +++ b/server_test.go @@ -26,11 +26,11 @@ import ( "testing" "time" + "github.com/containerd/ttrpc/codes" + "github.com/containerd/ttrpc/status" "github.com/gogo/protobuf/proto" "github.com/pkg/errors" "github.com/prometheus/procfs" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" ) const serviceName = "testService" diff --git a/services.go b/services.go index 2a83ba8..c1e7776 100644 --- a/services.go +++ b/services.go @@ -23,10 +23,10 @@ import ( "path" "unsafe" + "github.com/containerd/ttrpc/codes" + "github.com/containerd/ttrpc/status" "github.com/gogo/protobuf/proto" "github.com/pkg/errors" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" ) type Method func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) diff --git a/status/doc.go b/status/doc.go new file mode 100644 index 0000000..4b091ec --- /dev/null +++ b/status/doc.go @@ -0,0 +1,20 @@ +/* + 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. +*/ + +// This package has been copied over from the v1.23.1 release of go grpc +// There are stability issues with point releases of grpc and ttrpc +// only uses the codes and status package from grpc +package status diff --git a/status/status.go b/status/status.go new file mode 100644 index 0000000..a78674c --- /dev/null +++ b/status/status.go @@ -0,0 +1,237 @@ +/* + 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. +*/ + +/* + * + * Copyright 2017 gRPC 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 status implements errors returned by gRPC. These errors are +// serialized and transmitted on the wire between server and client, and allow +// for additional data to be transmitted via the Details field in the status +// proto. gRPC service handlers should return an error created by this +// package, and gRPC clients should expect a corresponding error to be +// returned from the RPC call. +// +// This package upholds the invariants that a non-nil error may not +// contain an OK code, and an OK code must result in a nil error. +package status + +import ( + "context" + "errors" + "fmt" + + "github.com/containerd/ttrpc/codes" + "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/ptypes" + spb "google.golang.org/genproto/googleapis/rpc/status" +) + +// statusError is an alias of a status proto. It implements error and Status, +// and a nil statusError should never be returned by this package. +type statusError spb.Status + +func (se *statusError) Error() string { + p := (*spb.Status)(se) + return fmt.Sprintf("rpc error: code = %s desc = %s", codes.Code(p.GetCode()), p.GetMessage()) +} + +func (se *statusError) GRPCStatus() *Status { + return &Status{s: (*spb.Status)(se)} +} + +// Is implements future error.Is functionality. +// A statusError is equivalent if the code and message are identical. +func (se *statusError) Is(target error) bool { + tse, ok := target.(*statusError) + if !ok { + return false + } + + return proto.Equal((*spb.Status)(se), (*spb.Status)(tse)) +} + +// Status represents an RPC status code, message, and details. It is immutable +// and should be created with New, Newf, or FromProto. +type Status struct { + s *spb.Status +} + +// Code returns the status code contained in s. +func (s *Status) Code() codes.Code { + if s == nil || s.s == nil { + return codes.OK + } + return codes.Code(s.s.Code) +} + +// Message returns the message contained in s. +func (s *Status) Message() string { + if s == nil || s.s == nil { + return "" + } + return s.s.Message +} + +// Proto returns s's status as an spb.Status proto message. +func (s *Status) Proto() *spb.Status { + if s == nil { + return nil + } + return proto.Clone(s.s).(*spb.Status) +} + +// Err returns an immutable error representing s; returns nil if s.Code() is +// OK. +func (s *Status) Err() error { + if s.Code() == codes.OK { + return nil + } + return (*statusError)(s.s) +} + +// New returns a Status representing c and msg. +func New(c codes.Code, msg string) *Status { + return &Status{s: &spb.Status{Code: int32(c), Message: msg}} +} + +// Newf returns New(c, fmt.Sprintf(format, a...)). +func Newf(c codes.Code, format string, a ...interface{}) *Status { + return New(c, fmt.Sprintf(format, a...)) +} + +// Error returns an error representing c and msg. If c is OK, returns nil. +func Error(c codes.Code, msg string) error { + return New(c, msg).Err() +} + +// Errorf returns Error(c, fmt.Sprintf(format, a...)). +func Errorf(c codes.Code, format string, a ...interface{}) error { + return Error(c, fmt.Sprintf(format, a...)) +} + +// ErrorProto returns an error representing s. If s.Code is OK, returns nil. +func ErrorProto(s *spb.Status) error { + return FromProto(s).Err() +} + +// FromProto returns a Status representing s. +func FromProto(s *spb.Status) *Status { + return &Status{s: proto.Clone(s).(*spb.Status)} +} + +// FromError returns a Status representing err if it was produced from this +// package or has a method `GRPCStatus() *Status`. Otherwise, ok is false and a +// Status is returned with codes.Unknown and the original error message. +func FromError(err error) (s *Status, ok bool) { + if err == nil { + return nil, true + } + if se, ok := err.(interface { + GRPCStatus() *Status + }); ok { + return se.GRPCStatus(), true + } + return New(codes.Unknown, err.Error()), false +} + +// Convert is a convenience function which removes the need to handle the +// boolean return value from FromError. +func Convert(err error) *Status { + s, _ := FromError(err) + return s +} + +// WithDetails returns a new status with the provided details messages appended to the status. +// If any errors are encountered, it returns nil and the first error encountered. +func (s *Status) WithDetails(details ...proto.Message) (*Status, error) { + if s.Code() == codes.OK { + return nil, errors.New("no error details for status with code OK") + } + // s.Code() != OK implies that s.Proto() != nil. + p := s.Proto() + for _, detail := range details { + any, err := ptypes.MarshalAny(detail) + if err != nil { + return nil, err + } + p.Details = append(p.Details, any) + } + return &Status{s: p}, nil +} + +// Details returns a slice of details messages attached to the status. +// If a detail cannot be decoded, the error is returned in place of the detail. +func (s *Status) Details() []interface{} { + if s == nil || s.s == nil { + return nil + } + details := make([]interface{}, 0, len(s.s.Details)) + for _, any := range s.s.Details { + detail := &ptypes.DynamicAny{} + if err := ptypes.UnmarshalAny(any, detail); err != nil { + details = append(details, err) + continue + } + details = append(details, detail.Message) + } + return details +} + +// Code returns the Code of the error if it is a Status error, codes.OK if err +// is nil, or codes.Unknown otherwise. +func Code(err error) codes.Code { + // Don't use FromError to avoid allocation of OK status. + if err == nil { + return codes.OK + } + if se, ok := err.(interface { + GRPCStatus() *Status + }); ok { + return se.GRPCStatus().Code() + } + return codes.Unknown +} + +// FromContextError converts a context error into a Status. It returns a +// Status with codes.OK if err is nil, or a Status with codes.Unknown if err is +// non-nil and not a context error. +func FromContextError(err error) *Status { + switch err { + case nil: + return nil + case context.DeadlineExceeded: + return New(codes.DeadlineExceeded, err.Error()) + case context.Canceled: + return New(codes.Canceled, err.Error()) + default: + return New(codes.Unknown, err.Error()) + } +} diff --git a/status/status_ext_test.go b/status/status_ext_test.go new file mode 100644 index 0000000..b183d62 --- /dev/null +++ b/status/status_ext_test.go @@ -0,0 +1,88 @@ +/* + 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. +*/ + +/* + * + * Copyright 2019 gRPC 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 status_test + +import ( + "errors" + "testing" + + "github.com/containerd/ttrpc/codes" + "github.com/containerd/ttrpc/status" + "github.com/golang/protobuf/proto" + "google.golang.org/grpc/test/grpc_testing" +) + +func errWithDetails(t *testing.T, s *status.Status, details ...proto.Message) error { + t.Helper() + res, err := s.WithDetails(details...) + if err != nil { + t.Fatalf("(%v).WithDetails(%v) = %v, %v; want _, ", s, details, res, err) + } + return res.Err() +} + +func TestErrorIs(t *testing.T) { + // Test errors. + testErr := status.Error(codes.Internal, "internal server error") + testErrWithDetails := errWithDetails(t, status.New(codes.Internal, "internal server error"), &grpc_testing.Empty{}) + + // Test cases. + testCases := []struct { + err1, err2 error + want bool + }{ + {err1: testErr, err2: nil, want: false}, + {err1: testErr, err2: status.Error(codes.Internal, "internal server error"), want: true}, + {err1: testErr, err2: status.Error(codes.Internal, "internal error"), want: false}, + {err1: testErr, err2: status.Error(codes.Unknown, "internal server error"), want: false}, + {err1: testErr, err2: errors.New("non-grpc error"), want: false}, + {err1: testErrWithDetails, err2: status.Error(codes.Internal, "internal server error"), want: false}, + {err1: testErrWithDetails, err2: errWithDetails(t, status.New(codes.Internal, "internal server error"), &grpc_testing.Empty{}), want: true}, + {err1: testErrWithDetails, err2: errWithDetails(t, status.New(codes.Internal, "internal server error"), &grpc_testing.Empty{}, &grpc_testing.Empty{}), want: false}, + } + + for _, tc := range testCases { + isError, ok := tc.err1.(interface{ Is(target error) bool }) + if !ok { + t.Errorf("(%v) does not implement is", tc.err1) + continue + } + + is := isError.Is(tc.err2) + if is != tc.want { + t.Errorf("(%v).Is(%v) = %t; want %t", tc.err1, tc.err2, is, tc.want) + } + } +} diff --git a/status/status_test.go b/status/status_test.go new file mode 100644 index 0000000..4f3d3d1 --- /dev/null +++ b/status/status_test.go @@ -0,0 +1,378 @@ +/* + 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. +*/ + +/* + * + * Copyright 2017 gRPC 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 status + +import ( + "context" + "errors" + "fmt" + "reflect" + "testing" + + "github.com/containerd/ttrpc/codes" + "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/ptypes" + apb "github.com/golang/protobuf/ptypes/any" + dpb "github.com/golang/protobuf/ptypes/duration" + cpb "google.golang.org/genproto/googleapis/rpc/code" + epb "google.golang.org/genproto/googleapis/rpc/errdetails" + spb "google.golang.org/genproto/googleapis/rpc/status" +) + +// errEqual is essentially a copy of testutils.StatusErrEqual(), to avoid a +// cyclic dependency. +func errEqual(err1, err2 error) bool { + status1, ok := FromError(err1) + if !ok { + return false + } + status2, ok := FromError(err2) + if !ok { + return false + } + return proto.Equal(status1.Proto(), status2.Proto()) +} + +func TestErrorsWithSameParameters(t *testing.T) { + const description = "some description" + e1 := Errorf(codes.AlreadyExists, description) + e2 := Errorf(codes.AlreadyExists, description) + if e1 == e2 || !errEqual(e1, e2) { + t.Fatalf("Errors should be equivalent but unique - e1: %v, %v e2: %p, %v", e1.(*statusError), e1, e2.(*statusError), e2) + } +} + +func TestFromToProto(t *testing.T) { + s := &spb.Status{ + Code: int32(codes.Internal), + Message: "test test test", + Details: []*apb.Any{{TypeUrl: "foo", Value: []byte{3, 2, 1}}}, + } + + err := FromProto(s) + if got := err.Proto(); !proto.Equal(s, got) { + t.Fatalf("Expected errors to be identical - s: %v got: %v", s, got) + } +} + +func TestFromNilProto(t *testing.T) { + tests := []*Status{nil, FromProto(nil)} + for _, s := range tests { + if c := s.Code(); c != codes.OK { + t.Errorf("s: %v - Expected s.Code() = OK; got %v", s, c) + } + if m := s.Message(); m != "" { + t.Errorf("s: %v - Expected s.Message() = \"\"; got %q", s, m) + } + if p := s.Proto(); p != nil { + t.Errorf("s: %v - Expected s.Proto() = nil; got %q", s, p) + } + if e := s.Err(); e != nil { + t.Errorf("s: %v - Expected s.Err() = nil; got %v", s, e) + } + } +} + +func TestError(t *testing.T) { + err := Error(codes.Internal, "test description") + if got, want := err.Error(), "rpc error: code = Internal desc = test description"; got != want { + t.Fatalf("err.Error() = %q; want %q", got, want) + } + s, _ := FromError(err) + if got, want := s.Code(), codes.Internal; got != want { + t.Fatalf("err.Code() = %s; want %s", got, want) + } + if got, want := s.Message(), "test description"; got != want { + t.Fatalf("err.Message() = %s; want %s", got, want) + } +} + +func TestErrorOK(t *testing.T) { + err := Error(codes.OK, "foo") + if err != nil { + t.Fatalf("Error(codes.OK, _) = %p; want nil", err.(*statusError)) + } +} + +func TestErrorProtoOK(t *testing.T) { + s := &spb.Status{Code: int32(codes.OK)} + if got := ErrorProto(s); got != nil { + t.Fatalf("ErrorProto(%v) = %v; want nil", s, got) + } +} + +func TestFromError(t *testing.T) { + code, message := codes.Internal, "test description" + err := Error(code, message) + s, ok := FromError(err) + if !ok || s.Code() != code || s.Message() != message || s.Err() == nil { + t.Fatalf("FromError(%v) = %v, %v; want , true", err, s, ok, code, message) + } +} + +func TestFromErrorOK(t *testing.T) { + code, message := codes.OK, "" + s, ok := FromError(nil) + if !ok || s.Code() != code || s.Message() != message || s.Err() != nil { + t.Fatalf("FromError(nil) = %v, %v; want , true", s, ok, code, message) + } +} + +type customError struct { + Code codes.Code + Message string + Details []*apb.Any +} + +func (c customError) Error() string { + return fmt.Sprintf("rpc error: code = %s desc = %s", c.Code, c.Message) +} + +func (c customError) GRPCStatus() *Status { + return &Status{ + s: &spb.Status{ + Code: int32(c.Code), + Message: c.Message, + Details: c.Details, + }, + } +} + +func TestFromErrorImplementsInterface(t *testing.T) { + code, message := codes.Internal, "test description" + details := []*apb.Any{{ + TypeUrl: "testUrl", + Value: []byte("testValue"), + }} + err := customError{ + Code: code, + Message: message, + Details: details, + } + s, ok := FromError(err) + if !ok || s.Code() != code || s.Message() != message || s.Err() == nil { + t.Fatalf("FromError(%v) = %v, %v; want , true", err, s, ok, code, message) + } + pd := s.Proto().GetDetails() + if len(pd) != 1 || !proto.Equal(pd[0], details[0]) { + t.Fatalf("s.Proto.GetDetails() = %v; want ", pd, details) + } +} + +func TestFromErrorUnknownError(t *testing.T) { + code, message := codes.Unknown, "unknown error" + err := errors.New("unknown error") + s, ok := FromError(err) + if ok || s.Code() != code || s.Message() != message { + t.Fatalf("FromError(%v) = %v, %v; want , false", err, s, ok, code, message) + } +} + +func TestConvertKnownError(t *testing.T) { + code, message := codes.Internal, "test description" + err := Error(code, message) + s := Convert(err) + if s.Code() != code || s.Message() != message { + t.Fatalf("Convert(%v) = %v; want ", err, s, code, message) + } +} + +func TestConvertUnknownError(t *testing.T) { + code, message := codes.Unknown, "unknown error" + err := errors.New("unknown error") + s := Convert(err) + if s.Code() != code || s.Message() != message { + t.Fatalf("Convert(%v) = %v; want ", err, s, code, message) + } +} + +func TestStatus_ErrorDetails(t *testing.T) { + tests := []struct { + code codes.Code + details []proto.Message + }{ + { + code: codes.NotFound, + details: nil, + }, + { + code: codes.NotFound, + details: []proto.Message{ + &epb.ResourceInfo{ + ResourceType: "book", + ResourceName: "projects/1234/books/5678", + Owner: "User", + }, + }, + }, + { + code: codes.Internal, + details: []proto.Message{ + &epb.DebugInfo{ + StackEntries: []string{ + "first stack", + "second stack", + }, + }, + }, + }, + { + code: codes.Unavailable, + details: []proto.Message{ + &epb.RetryInfo{ + RetryDelay: &dpb.Duration{Seconds: 60}, + }, + &epb.ResourceInfo{ + ResourceType: "book", + ResourceName: "projects/1234/books/5678", + Owner: "User", + }, + }, + }, + } + + for _, tc := range tests { + s, err := New(tc.code, "").WithDetails(tc.details...) + if err != nil { + t.Fatalf("(%v).WithDetails(%+v) failed: %v", str(s), tc.details, err) + } + details := s.Details() + for i := range details { + if !proto.Equal(details[i].(proto.Message), tc.details[i]) { + t.Fatalf("(%v).Details()[%d] = %+v, want %+v", str(s), i, details[i], tc.details[i]) + } + } + } +} + +func TestStatus_WithDetails_Fail(t *testing.T) { + tests := []*Status{ + nil, + FromProto(nil), + New(codes.OK, ""), + } + for _, s := range tests { + if s, err := s.WithDetails(); err == nil || s != nil { + t.Fatalf("(%v).WithDetails(%+v) = %v, %v; want nil, non-nil", str(s), []proto.Message{}, s, err) + } + } +} + +func TestStatus_ErrorDetails_Fail(t *testing.T) { + tests := []struct { + s *Status + i []interface{} + }{ + { + nil, + nil, + }, + { + FromProto(nil), + nil, + }, + { + New(codes.OK, ""), + []interface{}{}, + }, + { + FromProto(&spb.Status{ + Code: int32(cpb.Code_CANCELLED), + Details: []*apb.Any{ + { + TypeUrl: "", + Value: []byte{}, + }, + mustMarshalAny(&epb.ResourceInfo{ + ResourceType: "book", + ResourceName: "projects/1234/books/5678", + Owner: "User", + }), + }, + }), + []interface{}{ + errors.New(`message type url "" is invalid`), + &epb.ResourceInfo{ + ResourceType: "book", + ResourceName: "projects/1234/books/5678", + Owner: "User", + }, + }, + }, + } + for _, tc := range tests { + got := tc.s.Details() + if !reflect.DeepEqual(got, tc.i) { + t.Errorf("(%v).Details() = %+v, want %+v", str(tc.s), got, tc.i) + } + } +} + +func str(s *Status) string { + if s == nil { + return "nil" + } + if s.s == nil { + return "" + } + return fmt.Sprintf("", codes.Code(s.s.GetCode()), s.s.GetMessage(), s.s.GetDetails()) +} + +// mustMarshalAny converts a protobuf message to an any. +func mustMarshalAny(msg proto.Message) *apb.Any { + any, err := ptypes.MarshalAny(msg) + if err != nil { + panic(fmt.Sprintf("ptypes.MarshalAny(%+v) failed: %v", msg, err)) + } + return any +} + +func TestFromContextError(t *testing.T) { + testCases := []struct { + in error + want *Status + }{ + {in: nil, want: New(codes.OK, "")}, + {in: context.DeadlineExceeded, want: New(codes.DeadlineExceeded, context.DeadlineExceeded.Error())}, + {in: context.Canceled, want: New(codes.Canceled, context.Canceled.Error())}, + {in: errors.New("other"), want: New(codes.Unknown, "other")}, + } + for _, tc := range testCases { + got := FromContextError(tc.in) + if got.Code() != tc.want.Code() || got.Message() != tc.want.Message() { + t.Errorf("FromContextError(%v) = %v; want %v", tc.in, got, tc.want) + } + } +}