pulumi/pkg/util/rpcutil/rpcerror/rpcerror.go
2018-10-29 18:09:00 -05:00

220 lines
7.4 KiB
Go

// Copyright 2016-2018, Pulumi Corporation.
//
// 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 rpcerror provides helper types and functions for dealing with errors
// that cross gRPC boundaries.
//
// gRPC best practices dictate that the only error that should ever be returned
// by RPC server endpoints is `status.Status`. If an RPC server does not do this,
// gRPC will wrap it in a `status.Status` with an error code of Unknown, which is
// not useful to clients. This package provides a few functions, namely
// `New`, `Newf`, `Wrap`, and `Wrapf`, which provide RPC servers an easy way to wrap
// up existing errors or create new errors to return from RPC endpoints.
//
// For the client side, this package provides functions `FromError` and `Convert`,
// as well as types `Error` and `ErrorCause`, which allows RPC clients to inspect
// the error that occurred (including its error status) and, if one was provided,
// the cause of the error (i.e. the one that was wrapped via `Wrap` or `Wrapf`).
package rpcerror
import (
"fmt"
"github.com/golang/protobuf/proto"
"github.com/pkg/errors"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/pulumi/pulumi/pkg/util/contract"
pulumirpc "github.com/pulumi/pulumi/sdk/proto/go"
)
// Error represents an error response from an RPC server endpoint.
// It contains a gRPC error code, a message, and a chain of "wrapped"
// errors that led to the final dispatch of this particular error message.
type Error struct {
code codes.Code
message string
cause *ErrorCause
details []interface{}
}
var _ error = (*Error)(nil)
func (r *Error) Error() string {
if r.cause != nil {
return fmt.Sprintf("%s: %s", r.message, r.cause.Message())
}
return r.message
}
// Code returns the gRPC error code for this error.
func (r *Error) Code() codes.Code {
return r.code
}
// Message returns the message associated with this error cause.
func (r *Error) Message() string {
return r.message
}
// Cause returns the error that was the root cause of this error,
// or nil if one wasn't provided.
func (r *Error) Cause() *ErrorCause {
return r.cause
}
// Details returns the list of all auxiliary protobuf objects that were
// attached to this error by the server. It's up to the caller to try and
// downcast them to look for the one they are interested in.
func (r *Error) Details() []interface{} {
return r.details
}
// ErrorCause represents a root cause of an error that ultimately caused
// an RPC endpoint to issue an error. ErrorCauses are optionally attached
// to Errors.
//
// All ErrorCauses have messages, but only a subset of them have stack traces.
// Notably, the pkg/errors package will affix stack traces to errors created through
// the errors.New and errors.Wrap.
type ErrorCause struct {
message string
stackTrace string
}
// Message returns the message associated with this error cause.
func (r *ErrorCause) Message() string {
return r.message
}
// StackTrace returns the stack trace associated with this error, or
// the empty string if one wasn't provided.
func (r *ErrorCause) StackTrace() string {
return r.stackTrace
}
// New creates a new gRPC-compatible `error` with the given error code
// and message.
func New(code codes.Code, message string) error {
status := status.New(code, message)
return status.Err()
}
// Newf creates a new gRPC-compatible `error` with the given code and
// formatted message.
func Newf(code codes.Code, messageFormat string, args ...interface{}) error {
status := status.Newf(code, messageFormat, args...)
return status.Err()
}
// Wrap wraps an `error` into a gRPC-compatible `error`, recording the
// warpped error as the "cause" of the returned error.
//
// It is a logic error to call this function on an error previously
// returned by `rpcerrors.Wrap`.
func Wrap(code codes.Code, err error, message string) error {
status := status.New(code, message)
cause := serializeErrorCause(err)
status, newErr := status.WithDetails(cause)
contract.AssertNoError(newErr)
return status.Err()
}
// Wrapf wraps an `error` into a gRPC-compatible `error`, plus a formatted message,
// recording the wrapped error as the "cause" of the returned error.
//
// It is a logic error to call this function on an error previously
// returned by `rpcerrors.Wrap`.
func Wrapf(code codes.Code, err error, messageFormat string, args ...interface{}) error {
status := status.Newf(code, messageFormat, args...)
cause := serializeErrorCause(err)
status, newErr := status.WithDetails(cause)
contract.AssertNoError(newErr)
return status.Err()
}
// WithDetails adds arbitrary protobuf payloads to errors created by this package.
// These errors will be accessible by calling `Details` on `Error` instances created
// by `FromError`.
func WithDetails(err error, details ...proto.Message) error {
status, ok := status.FromError(err)
contract.Assertf(ok, "WithDetails called on error not created by rpcerror")
status, conversionError := status.WithDetails(details...)
contract.AssertNoError(conversionError)
return status.Err()
}
// FromError "unwraps" an error created by functions in the `rpcerror` package and produces
// an `Error` structure from them.
//
// This function is designed to be used by clients interacting with gRPC servers.
// If the gRPC server issued an error using one of the error creation functions in `rpcerror`,
// this function will produce a non-null `Error`.
//
// Returns false if the given error is not a gRPC Status error.
func FromError(err error) (*Error, bool) {
status, ok := status.FromError(err)
if !ok {
rpcError, ok := err.(*Error)
return rpcError, ok
}
var rpcError Error
rpcError.code = status.Code()
rpcError.message = status.Message()
rpcError.details = status.Details()
for _, details := range status.Details() {
if errorCause, ok := details.(*pulumirpc.ErrorCause); ok {
contract.Assertf(rpcError.cause == nil, "RPC endpoint sent more than one ErrorCause")
rpcError.cause = &ErrorCause{
message: errorCause.Message,
stackTrace: errorCause.StackTrace,
}
}
}
return &rpcError, true
}
// Convert converts an error to an Error using `FromError`, but panics if the conversion
// fails.
func Convert(err error) *Error {
converted, ok := FromError(err)
contract.Assertf(ok, "failed to convert err %v to Error, did this come from an RPC endpoint?", err)
return converted
}
func serializeErrorCause(err error) *pulumirpc.ErrorCause {
// Go is a surprising language that lets you do wacky stuff like this
// to get at implementation details of private structs.
//
// The pkg/errors documentation actually encourages this pattern (!) so
// that's what we're doing here to get at the error's stack trace.
type stackTracer interface {
StackTrace() errors.StackTrace
}
message := err.Error()
var stackTrace string
if errWithStack, ok := err.(stackTracer); ok {
stackTrace = fmt.Sprintf("%+v", errWithStack.StackTrace())
}
return &pulumirpc.ErrorCause{
Message: message,
StackTrace: stackTrace,
}
}