github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/plugin/grpc_error.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package plugin
     5  
     6  import (
     7  	"fmt"
     8  	"path"
     9  	"runtime"
    10  
    11  	"github.com/terramate-io/tf/tfdiags"
    12  	"google.golang.org/grpc/codes"
    13  	"google.golang.org/grpc/status"
    14  )
    15  
    16  // grpcErr extracts some known error types and formats them into better
    17  // representations for core. This must only be called from plugin methods.
    18  // Since we don't use RPC status errors for the plugin protocol, these do not
    19  // contain any useful details, and we can return some text that at least
    20  // indicates the plugin call and possible error condition.
    21  func grpcErr(err error) (diags tfdiags.Diagnostics) {
    22  	if err == nil {
    23  		return
    24  	}
    25  
    26  	// extract the method name from the caller.
    27  	pc, _, _, ok := runtime.Caller(1)
    28  	if !ok {
    29  		logger.Error("unknown grpc call", "error", err)
    30  		return diags.Append(err)
    31  	}
    32  
    33  	f := runtime.FuncForPC(pc)
    34  
    35  	// Function names will contain the full import path. Take the last
    36  	// segment, which will let users know which method was being called.
    37  	_, requestName := path.Split(f.Name())
    38  
    39  	// Here we can at least correlate the error in the logs to a particular binary.
    40  	logger.Error(requestName, "error", err)
    41  
    42  	// TODO: while this expands the error codes into somewhat better messages,
    43  	// this still does not easily link the error to an actual user-recognizable
    44  	// plugin. The grpc plugin does not know its configured name, and the
    45  	// errors are in a list of diagnostics, making it hard for the caller to
    46  	// annotate the returned errors.
    47  	switch status.Code(err) {
    48  	case codes.Unavailable:
    49  		// This case is when the plugin has stopped running for some reason,
    50  		// and is usually the result of a crash.
    51  		diags = diags.Append(tfdiags.WholeContainingBody(
    52  			tfdiags.Error,
    53  			"Plugin did not respond",
    54  			fmt.Sprintf("The plugin encountered an error, and failed to respond to the %s call. "+
    55  				"The plugin logs may contain more details.", requestName),
    56  		))
    57  	case codes.Canceled:
    58  		diags = diags.Append(tfdiags.WholeContainingBody(
    59  			tfdiags.Error,
    60  			"Request cancelled",
    61  			fmt.Sprintf("The %s request was cancelled.", requestName),
    62  		))
    63  	case codes.Unimplemented:
    64  		diags = diags.Append(tfdiags.WholeContainingBody(
    65  			tfdiags.Error,
    66  			"Unsupported plugin method",
    67  			fmt.Sprintf("The %s method is not supported by this plugin.", requestName),
    68  		))
    69  	default:
    70  		diags = diags.Append(tfdiags.WholeContainingBody(
    71  			tfdiags.Error,
    72  			"Plugin error",
    73  			fmt.Sprintf("The plugin returned an unexpected error from %s: %v", requestName, err),
    74  		))
    75  	}
    76  	return
    77  }