Skip to content

Commit

Permalink
core: Minimal working implementation of provider-contributed functions
Browse files Browse the repository at this point in the history
This establishes all of the wiring necessary for a provider's declared
functions to appear in the hcl.EvalContext when we evaluate expressions
inside a module that depends on a provider that contributes functions.

Although this includes the minimum required machinery to make it work,
it is still lacking in a few different ways:
- It spins up an entirely new instance of the contributing provider for
  each individual function call, which is likely to be pretty slow and
  memory intensive for real plugins that run as child processes. We'll
  need to find some way to reuse these for multiple calls and then
  clean them up when we're finished.
- It doesn't make any attempts to guarantee that the provider-contributed
  functions correctly honor the contracts such as behaving as pure
  functions. Properly checking this is important because if a function
  doesn't uphold Terraform's expectations then it will cause confusing
  errors reported downstream, incorrectly blaming other components for
  the inconsistency.

With that said then, there's still plenty more work to do here before this
could be shipped but at least it demonstrates that provider-contributed
functions are viable and demonstrates one design for how they might appear
in the Terraform language.
  • Loading branch information
apparentlymart committed Jun 11, 2022
1 parent 9b56ab9 commit d6e2c73
Show file tree
Hide file tree
Showing 4 changed files with 529 additions and 2 deletions.
103 changes: 103 additions & 0 deletions internal/providers/functions.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package providers

import (
"fmt"

"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"

"github.com/hashicorp/terraform/internal/configs/configschema"
)
Expand All @@ -25,3 +28,103 @@ type FunctionParam struct {
Description string
DescriptionKind configschema.StringKind
}

// BuildFunction takes a factory function which will return an unconfigured
// instance of the provider this declaration belongs to and returns a
// cty function that is ready to be called against that provider.
//
// The given name must be the name under which the provider originally
// registered this declaration, or the returned function will try to use an
// invalid name, leading to errors or undefined behavior.
//
// If the given factory returns an instance of any provider other than the
// one the declaration belongs to, or returns a _configured_ instance of
// the provider rather than an unconfigured one, the behavior of the returned
// function is undefined.
//
// Although not functionally required, callers should ideally pass a factory
// function that either retrieves already-running plugins or memoizes the
// plugins it returns so that many calls to functions in the same provider
// will not incur a repeated startup cost.
func (d *FunctionDecl) BuildFunction(name string, factory func() (Interface, error)) function.Function {

var params []function.Parameter
var varParam *function.Parameter
if len(d.Parameters) > 0 {
params = make([]function.Parameter, len(d.Parameters))
for i, paramDecl := range d.Parameters {
params[i] = paramDecl.ctyParameter()
}
}
if d.VariadicParameter != nil {
cp := d.VariadicParameter.ctyParameter()
varParam = &cp
}

argParamDecl := func(idx int) *FunctionParam {
if idx < len(d.Parameters) {
return &d.Parameters[idx]
}
return d.VariadicParameter
}

return function.New(&function.Spec{
Type: function.StaticReturnType(d.ReturnType),
Params: params,
VarParam: varParam,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
// We promise provider developers that we won't pass them even
// _nested_ unknown values unless they opt in to dealing with them.
for i, arg := range args {
if !argParamDecl(i).AllowUnknownValues {
if !arg.IsWhollyKnown() {
return cty.UnknownVal(retType), nil
}
}
}

provider, err := factory()
if err != nil {
return cty.UnknownVal(retType), fmt.Errorf("failed to launch provider plugin: %s", err)
}

resp := provider.CallFunction(CallFunctionRequest{
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.Result == cty.NilVal {
return cty.UnknownVal(retType), fmt.Errorf("provider returned no result and no errors")
}

err = provider.Close()
if err != nil {
return cty.UnknownVal(retType), fmt.Errorf("failed to terminate provider plugin: %s", err)
}

return resp.Result, nil
},
})
}

func (p *FunctionParam) ctyParameter() function.Parameter {
return function.Parameter{
Name: p.Name,
Type: p.Type,
AllowNull: p.Nullable,

// NOTE: Setting this is not a sufficient implementation of
// FunctionParam.AllowUnknownValues, because cty's function
// system only blocks passing in a top-level unknown, but
// our provider-contributed functions API promises to only
// pass wholly-known values unless AllowUnknownValues is true.
// The function implementation itself must also check this.
AllowUnknown: p.AllowUnknownValues,
}
}
Loading

0 comments on commit d6e2c73

Please sign in to comment.