provider functions return an error

The call site for language functions doesn't currently have a way to
handle complex diagnostics, so rather than appear to support them in the
protocol we remove the concepts of diagnostics for now. We do however
retain the argument index fields, which we can wrap in a
function.ArgError and get a little more precise hcl diagnostic from
expression.
pull/34603/head
James Bardin 2 years ago
parent f44e743f90
commit a8701f6ddd

@ -41,10 +41,13 @@ message Diagnostic {
string summary = 2;
string detail = 3;
AttributePath attribute = 4;
}
// function_argument is the positional function argument for aligning
// configuration source.
optional int64 function_argument = 5;
message FunctionError {
string text = 1;
// The optional function_argument records the index position of the
// argument which caused the error.
optional int64 function_argument = 2;
}
message AttributePath {
@ -569,6 +572,6 @@ message CallFunction {
}
message Response {
DynamicValue result = 1;
repeated Diagnostic diagnostics = 2;
FunctionError error = 2;
}
}

@ -41,10 +41,13 @@ message Diagnostic {
string summary = 2;
string detail = 3;
AttributePath attribute = 4;
}
// function_argument is the positional function argument for aligning
// configuration source.
optional int64 function_argument = 5;
message FunctionError {
string text = 1;
// The optional function_argument records the index position of the
// argument which caused the error.
optional int64 function_argument = 2;
}
message AttributePath {
@ -549,6 +552,6 @@ message CallFunction {
}
message Response {
DynamicValue result = 1;
repeated Diagnostic diagnostics = 2;
FunctionError error = 2;
}
}

@ -5,9 +5,9 @@ package grpcwrap
import (
"context"
"fmt"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/zclconf/go-cty/cty/msgpack"
"google.golang.org/grpc/codes"
@ -444,15 +444,18 @@ func (p *provider) CallFunction(_ context.Context, req *tfplugin5.CallFunction_R
if len(req.Arguments) != 0 {
args = make([]cty.Value, len(req.Arguments))
for i, rawArg := range req.Arguments {
idx := int64(i)
var argTy cty.Type
if i < len(funcSchema.Parameters) {
argTy = funcSchema.Parameters[i].Type
} else {
if funcSchema.VariadicParameter == nil {
resp.Diagnostics = convert.AppendProtoDiag(
resp.Diagnostics, fmt.Errorf("too many arguments for non-variadic function"),
)
resp.Error = &tfplugin5.FunctionError{
Text: "too many arguments for non-variadic function",
FunctionArgument: &idx,
}
return resp, nil
}
argTy = funcSchema.VariadicParameter.Type
@ -460,9 +463,13 @@ func (p *provider) CallFunction(_ context.Context, req *tfplugin5.CallFunction_R
argVal, err := decodeDynamicValue(rawArg, argTy)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
resp.Error = &tfplugin5.FunctionError{
Text: err.Error(),
FunctionArgument: &idx,
}
return resp, nil
}
args[i] = argVal
}
}
@ -471,14 +478,26 @@ func (p *provider) CallFunction(_ context.Context, req *tfplugin5.CallFunction_R
FunctionName: req.Name,
Arguments: args,
})
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, callResp.Diagnostics)
if callResp.Diagnostics.HasErrors() {
if callResp.Err != nil {
resp.Error = &tfplugin5.FunctionError{
Text: callResp.Err.Error(),
}
if argErr, ok := callResp.Err.(function.ArgError); ok {
idx := int64(argErr.Index)
resp.Error.FunctionArgument = &idx
}
return resp, nil
}
resp.Result, err = encodeDynamicValue(callResp.Result, funcSchema.ReturnType)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
resp.Error = &tfplugin5.FunctionError{
Text: err.Error(),
}
return resp, nil
}

@ -5,9 +5,9 @@ package grpcwrap
import (
"context"
"fmt"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/zclconf/go-cty/cty/msgpack"
"google.golang.org/grpc/codes"
@ -445,15 +445,17 @@ func (p *provider6) CallFunction(_ context.Context, req *tfplugin6.CallFunction_
if len(req.Arguments) != 0 {
args = make([]cty.Value, len(req.Arguments))
for i, rawArg := range req.Arguments {
idx := int64(i)
var argTy cty.Type
if i < len(funcSchema.Parameters) {
argTy = funcSchema.Parameters[i].Type
} else {
if funcSchema.VariadicParameter == nil {
resp.Diagnostics = convert.AppendProtoDiag(
resp.Diagnostics, fmt.Errorf("too many arguments for non-variadic function"),
)
resp.Error = &tfplugin6.FunctionError{
Text: "too many arguments for non-variadic function",
FunctionArgument: &idx,
}
return resp, nil
}
argTy = funcSchema.VariadicParameter.Type
@ -461,9 +463,13 @@ func (p *provider6) CallFunction(_ context.Context, req *tfplugin6.CallFunction_
argVal, err := decodeDynamicValue6(rawArg, argTy)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
resp.Error = &tfplugin6.FunctionError{
Text: err.Error(),
FunctionArgument: &idx,
}
return resp, nil
}
args[i] = argVal
}
}
@ -472,14 +478,25 @@ func (p *provider6) CallFunction(_ context.Context, req *tfplugin6.CallFunction_
FunctionName: req.Name,
Arguments: args,
})
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, callResp.Diagnostics)
if callResp.Diagnostics.HasErrors() {
if callResp.Err != nil {
resp.Error = &tfplugin6.FunctionError{
Text: callResp.Err.Error(),
}
if argErr, ok := callResp.Err.(function.ArgError); ok {
idx := int64(argErr.Index)
resp.Error.FunctionArgument = &idx
}
return resp, nil
}
resp.Result, err = encodeDynamicValue6(callResp.Result, funcSchema.ReturnType)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
resp.Error = &tfplugin6.FunctionError{
Text: err.Error(),
}
return resp, nil
}

@ -12,6 +12,7 @@ import (
"github.com/zclconf/go-cty/cty"
plugin "github.com/hashicorp/go-plugin"
"github.com/zclconf/go-cty/cty/function"
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/zclconf/go-cty/cty/msgpack"
"google.golang.org/grpc"
@ -741,7 +742,7 @@ func (p *GRPCProvider) CallFunction(r providers.CallFunctionRequest) (resp provi
schema := p.GetProviderSchema()
if schema.Diagnostics.HasErrors() {
resp.Diagnostics = schema.Diagnostics
resp.Err = schema.Diagnostics.Err()
return resp
}
@ -755,15 +756,15 @@ func (p *GRPCProvider) CallFunction(r providers.CallFunctionRequest) (resp provi
// Should only get here if the caller has a bug, because we should
// have detected earlier any attempt to call a function that the
// provider didn't declare.
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("provider has no function named %q", r.FunctionName))
resp.Err = fmt.Errorf("provider has no function named %q", r.FunctionName)
return resp
}
if len(r.Arguments) < len(funcDecl.Parameters) {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("not enough arguments for function %q", r.FunctionName))
resp.Err = fmt.Errorf("not enough arguments for function %q", r.FunctionName)
return resp
}
if funcDecl.VariadicParameter == nil && len(r.Arguments) > len(funcDecl.Parameters) {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("too many arguments for function %q", r.FunctionName))
resp.Err = fmt.Errorf("too many arguments for function %q", r.FunctionName)
return resp
}
args := make([]*proto.DynamicValue, len(r.Arguments))
@ -777,7 +778,7 @@ func (p *GRPCProvider) CallFunction(r providers.CallFunctionRequest) (resp provi
argValRaw, err := msgpack.Marshal(argVal, paramDecl.Type)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
resp.Err = err
return resp
}
args[i] = &proto.DynamicValue{
@ -790,17 +791,28 @@ func (p *GRPCProvider) CallFunction(r providers.CallFunctionRequest) (resp provi
Arguments: args,
})
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
// functions can only support simple errors, but use our grpcError
// diagnostic function to format common problems is a more
// user-friendly manner.
resp.Err = grpcErr(err).Err()
return resp
}
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
if resp.Diagnostics.HasErrors() {
if protoResp.Error != nil {
resp.Err = errors.New(protoResp.Error.Text)
// If this is a problem with a specific argument, we can wrap the error
// in a function.ArgError
if protoResp.Error.FunctionArgument != nil {
resp.Err = function.NewArgError(int(*protoResp.Error.FunctionArgument), resp.Err)
}
return resp
}
resultVal, err := decodeDynamicValue(protoResp.Result, funcDecl.ReturnType)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
resp.Err = err
return resp
}

@ -12,6 +12,7 @@ import (
"github.com/zclconf/go-cty/cty"
plugin "github.com/hashicorp/go-plugin"
"github.com/zclconf/go-cty/cty/function"
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/zclconf/go-cty/cty/msgpack"
"google.golang.org/grpc"
@ -730,7 +731,7 @@ func (p *GRPCProvider) CallFunction(r providers.CallFunctionRequest) (resp provi
schema := p.GetProviderSchema()
if schema.Diagnostics.HasErrors() {
resp.Diagnostics = schema.Diagnostics
resp.Err = schema.Diagnostics.Err()
return resp
}
@ -744,15 +745,15 @@ func (p *GRPCProvider) CallFunction(r providers.CallFunctionRequest) (resp provi
// Should only get here if the caller has a bug, because we should
// have detected earlier any attempt to call a function that the
// provider didn't declare.
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("provider has no function named %q", r.FunctionName))
resp.Err = fmt.Errorf("provider has no function named %q", r.FunctionName)
return resp
}
if len(r.Arguments) < len(funcDecl.Parameters) {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("not enough arguments for function %q", r.FunctionName))
resp.Err = fmt.Errorf("not enough arguments for function %q", r.FunctionName)
return resp
}
if funcDecl.VariadicParameter == nil && len(r.Arguments) > len(funcDecl.Parameters) {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("too many arguments for function %q", r.FunctionName))
resp.Err = fmt.Errorf("too many arguments for function %q", r.FunctionName)
return resp
}
args := make([]*proto6.DynamicValue, len(r.Arguments))
@ -766,7 +767,7 @@ func (p *GRPCProvider) CallFunction(r providers.CallFunctionRequest) (resp provi
argValRaw, err := msgpack.Marshal(argVal, paramDecl.Type)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
resp.Err = err
return resp
}
args[i] = &proto6.DynamicValue{
@ -779,17 +780,28 @@ func (p *GRPCProvider) CallFunction(r providers.CallFunctionRequest) (resp provi
Arguments: args,
})
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
// functions can only support simple errors, but use our grpcError
// diagnostic function to format common problems is a more
// user-friendly manner.
resp.Err = grpcErr(err).Err()
return resp
}
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
if resp.Diagnostics.HasErrors() {
if protoResp.Error != nil {
resp.Err = errors.New(protoResp.Error.Text)
// If this is a problem with a specific argument, we can wrap the error
// in a function.ArgError
if protoResp.Error.FunctionArgument != nil {
resp.Err = function.NewArgError(int(*protoResp.Error.FunctionArgument), resp.Err)
}
return resp
}
resultVal, err := decodeDynamicValue(protoResp.Result, funcDecl.ReturnType)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
resp.Err = err
return resp
}

@ -173,7 +173,7 @@ func (s simple) ReadDataSource(req providers.ReadDataSourceRequest) (resp provid
func (s simple) CallFunction(req providers.CallFunctionRequest) (resp providers.CallFunctionResponse) {
if req.FunctionName != "noop" {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("CallFunction for undefined function %q", req.FunctionName))
resp.Err = fmt.Errorf("CallFunction for undefined function %q", req.FunctionName)
return resp
}

@ -114,11 +114,8 @@ func (d FunctionDecl) BuildFunction(providerAddr addrs.Provider, name string, re
FunctionName: name,
Arguments: args,
})
// NOTE: We don't actually have any way to surface warnings
// from the function here, because functions just return normal
// Go errors rather than diagnostics.
if resp.Diagnostics.HasErrors() {
return cty.UnknownVal(retType), resp.Diagnostics.Err()
if resp.Err != nil {
return cty.UnknownVal(retType), resp.Err
}
if resp.Result == cty.NilVal {

@ -501,6 +501,8 @@ type CallFunctionResponse struct {
// so can be left as cty.NilVal to represent the absense of a value.
Result cty.Value
// Diagnostics contains any warnings or errors from the function call.
Diagnostics tfdiags.Diagnostics
// Err is the error value from the function call. This may be an instance
// of function.ArgError from the go-cty package to specify a problem with a
// specific argument.
Err error
}

@ -2534,23 +2534,23 @@ func TestContext2Validate_providerContributedFunctions(t *testing.T) {
}
p.CallFunctionFn = func(req providers.CallFunctionRequest) (resp providers.CallFunctionResponse) {
if req.FunctionName != "count_e" {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("incorrect function name %q", req.FunctionName))
resp.Err = fmt.Errorf("incorrect function name %q", req.FunctionName)
return resp
}
if len(req.Arguments) != 1 {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("wrong number of arguments %d", len(req.Arguments)))
resp.Err = fmt.Errorf("wrong number of arguments %d", len(req.Arguments))
return resp
}
if req.Arguments[0].Type() != cty.String {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("wrong argument type %#v", req.Arguments[0].Type()))
resp.Err = fmt.Errorf("wrong argument type %#v", req.Arguments[0].Type())
return resp
}
if !req.Arguments[0].IsKnown() {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("argument is unknown"))
resp.Err = fmt.Errorf("argument is unknown")
return resp
}
if req.Arguments[0].IsNull() {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("argument is null"))
resp.Err = fmt.Errorf("argument is null")
return resp
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save