make grpcErr work for either plugin type

Extract a better function name and make the errors generic for different
plugin types.
This commit is contained in:
James Bardin 2020-10-25 11:19:09 -04:00
parent 3225d9ac11
commit 5f063ae94a
3 changed files with 78 additions and 74 deletions

74
plugin/grpc_error.go Normal file
View File

@ -0,0 +1,74 @@
package plugin
import (
"fmt"
"path"
"runtime"
"github.com/hashicorp/terraform/tfdiags"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// grpcErr extracts some known error types and formats them into better
// representations for core. This must only be called from plugin methods.
// Since we don't use RPC status errors for the plugin protocol, these do not
// contain any useful details, and we can return some text that at least
// indicates the plugin call and possible error condition.
func grpcErr(err error) (diags tfdiags.Diagnostics) {
if err == nil {
return
}
// extract the method name from the caller.
pc, _, _, ok := runtime.Caller(1)
if !ok {
logger.Error("unknown grpc call", "error", err)
return diags.Append(err)
}
f := runtime.FuncForPC(pc)
// Function names will contain the full import path. Take the last
// segment, which will let users know which method was being called.
_, requestName := path.Split(f.Name())
// Here we can at least correlate the error in the logs to a particular binary.
logger.Error(requestName, "error", err)
// TODO: while this expands the error codes into somewhat better messages,
// this still does not easily link the error to an actual user-recognizable
// plugin. The grpc plugin does not know its configured name, and the
// errors are in a list of diagnostics, making it hard for the caller to
// annotate the returned errors.
switch status.Code(err) {
case codes.Unavailable:
// This case is when the plugin has stopped running for some reason,
// and is usually the result of a crash.
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Plugin did not respond",
fmt.Sprintf("The plugin encountered an error, and failed to respond to the %s call. "+
"The plugin logs may contain more details.", requestName),
))
case codes.Canceled:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Request cancelled",
fmt.Sprintf("The %s request was cancelled.", requestName),
))
case codes.Unimplemented:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unsupported plugin method",
fmt.Sprintf("The %s method is not supported by this plugin.", requestName),
))
default:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Plugin error",
fmt.Sprintf("The plugin returned an unexpected error from %s: %v", requestName, err),
))
}
return
}

View File

@ -3,9 +3,6 @@ package plugin
import (
"context"
"errors"
"fmt"
"runtime"
"strings"
"sync"
"github.com/zclconf/go-cty/cty"
@ -15,12 +12,9 @@ import (
proto "github.com/hashicorp/terraform/internal/tfplugin5"
"github.com/hashicorp/terraform/plugin/convert"
"github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/tfdiags"
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/zclconf/go-cty/cty/msgpack"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var logger = logging.HCLogger()
@ -625,66 +619,3 @@ func decodeDynamicValue(v *proto.DynamicValue, ty cty.Type) (cty.Value, error) {
}
return res, err
}
// grpcErr extracts some known error types and formats them into better
// representations for core. This must only be called from plugin methods.
// Since we don't use RPC status errors for the plugin protocol, these do not
// contain any useful details, and we can return some text that at least
// indicates the plugin call and possible error condition.
func grpcErr(err error) (diags tfdiags.Diagnostics) {
if err == nil {
return
}
// extract the method name from the caller.
pc, _, _, ok := runtime.Caller(1)
if !ok {
return diags.Append(err)
}
f := runtime.FuncForPC(pc)
requestName := f.Name()
dot := strings.LastIndex(requestName, ".")
if dot > 0 {
requestName = requestName[dot+1:]
}
// Here we can at least correlate the error in the logs to a particular binary.
logger.Error("GRPCProvider."+requestName, "error", err)
// TODO: while this expands the error codes into somewhat better messages,
// this still does not easily link the error to the an actual
// user-recognizable provider. The GRPCProvider does not know its
// configured name, and the errors are in a list of diagnostics, making it
// hard for the the caller to annotate the returned errors.
switch status.Code(err) {
case codes.Unavailable:
// This case is when the provider has stopped running for some reason,
// and is usually the result of a crash.
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Provider did not respond",
fmt.Sprintf("The provider encountered an error, and failed to respond to the %s call. "+
"The provider logs may contain more details", requestName),
))
case codes.Canceled:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Request cancelled",
fmt.Sprintf("The %s request was cancelled.", requestName),
))
case codes.Unimplemented:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unsupported plugin method",
fmt.Sprintf("The %s method is not supported by this provider", requestName),
))
default:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Provider error",
fmt.Sprintf("The provider returned an unexpected error from %s: %v", requestName, err),
))
}
return
}

View File

@ -4,7 +4,6 @@ import (
"context"
"errors"
"io"
"log"
"sync"
plugin "github.com/hashicorp/go-plugin"
@ -61,7 +60,7 @@ func (p *GRPCProvisioner) GetSchema() (resp provisioners.GetSchemaResponse) {
protoResp, err := p.client.GetSchema(p.ctx, new(proto.GetProvisionerSchema_Request))
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
return resp
}
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
@ -96,7 +95,7 @@ func (p *GRPCProvisioner) ValidateProvisionerConfig(r provisioners.ValidateProvi
}
protoResp, err := p.client.ValidateProvisionerConfig(p.ctx, protoReq)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
return resp
}
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
@ -130,7 +129,7 @@ func (p *GRPCProvisioner) ProvisionResource(r provisioners.ProvisionResourceRequ
outputClient, err := p.client.ProvisionResource(p.ctx, protoReq)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
return resp
}
@ -169,7 +168,7 @@ func (p *GRPCProvisioner) Stop() error {
func (p *GRPCProvisioner) Close() error {
// check this since it's not automatically inserted during plugin creation
if p.PluginClient == nil {
log.Println("[DEBUG] provider has no plugin.Client")
logger.Debug("provisioner has no plugin.Client")
return nil
}