mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Integrate provider functions (#1439)
Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
This commit is contained in:
parent
2d373f16fa
commit
b868012192
@ -15,6 +15,7 @@ STATE ENCRYPTION
|
||||
|
||||
NEW FEATURES:
|
||||
* Add support for a `removed` block that allows users to remove resources or modules from the state without destroying them. ([#1158](https://github.com/opentofu/opentofu/pull/1158))
|
||||
* Provider-defined functions are now available. They may be referenced via `provider::<provider_name>::<funcname>(args)`. ([#1439](https://github.com/opentofu/opentofu/pull/1439))
|
||||
|
||||
ENHANCEMENTS:
|
||||
* Added support to use `.tfvars` files from tests folder. ([#1386](https://github.com/opentofu/opentofu/pull/1386))
|
||||
|
3
go.mod
3
go.mod
@ -51,7 +51,7 @@ require (
|
||||
github.com/hashicorp/go-uuid v1.0.3
|
||||
github.com/hashicorp/go-version v1.6.0
|
||||
github.com/hashicorp/hcl v1.0.0
|
||||
github.com/hashicorp/hcl/v2 v2.19.1
|
||||
github.com/hashicorp/hcl/v2 v2.20.1
|
||||
github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d
|
||||
github.com/hashicorp/terraform-svchost v0.1.1
|
||||
github.com/jmespath/go-jmespath v0.4.0
|
||||
@ -230,7 +230,6 @@ require (
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/samber/lo v1.37.0 // indirect
|
||||
github.com/sergi/go-diff v1.2.0 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/cobra v1.6.1 // indirect
|
||||
|
6
go.sum
6
go.sum
@ -716,8 +716,8 @@ github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI=
|
||||
github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
|
||||
github.com/hashicorp/hcl/v2 v2.20.1 h1:M6hgdyz7HYt1UN9e61j+qKJBqR3orTWbI1HKBJEdxtc=
|
||||
github.com/hashicorp/hcl/v2 v2.20.1/go.mod h1:TZDqQ4kNKCbh1iJp99FdPiUaVDDUPivbqxZulxDYqL4=
|
||||
github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d h1:9ARUJJ1VVynB176G1HCwleORqCaXm/Vx0uUi0dL26I0=
|
||||
github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d/go.mod h1:Yog5+CPEM3c99L1CL2CFCYoSzgWm5vTU58idbRUaLik=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
@ -981,8 +981,6 @@ github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw=
|
||||
github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
|
||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/lang"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
@ -57,9 +58,9 @@ func Marshal(f map[string]function.Function) ([]byte, tfdiags.Diagnostics) {
|
||||
signatures := newFunctions()
|
||||
|
||||
for name, v := range f {
|
||||
if name == "can" {
|
||||
if name == "can" || name == lang.CoreNamespace+"can" {
|
||||
signatures.Signatures[name] = marshalCan(v)
|
||||
} else if name == "try" {
|
||||
} else if name == "try" || name == lang.CoreNamespace+"try" {
|
||||
signatures.Signatures[name] = marshalTry(v)
|
||||
} else {
|
||||
signature, err := marshalFunction(v)
|
||||
|
@ -79,7 +79,7 @@ Usage: tofu [global options] metadata functions -json
|
||||
|
||||
func isIgnoredFunction(name string) bool {
|
||||
for _, i := range ignoredFunctions {
|
||||
if i == name {
|
||||
if i == name || lang.CoreNamespace+i == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -7,10 +7,13 @@ package lang
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/ext/dynblock"
|
||||
"github.com/hashicorp/hcl/v2/hcldec"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
|
||||
@ -71,7 +74,7 @@ func (s *Scope) EvalBlock(body hcl.Body, schema *configschema.Block) (cty.Value,
|
||||
body = blocktoattr.FixUpBlockAttrs(body, schema)
|
||||
|
||||
val, evalDiags := hcldec.Decode(body, spec, ctx)
|
||||
diags = diags.Append(evalDiags)
|
||||
diags = diags.Append(s.enhanceFunctionDiags(evalDiags))
|
||||
|
||||
return val, diags
|
||||
}
|
||||
@ -149,7 +152,7 @@ func (s *Scope) EvalSelfBlock(body hcl.Body, self cty.Value, schema *configschem
|
||||
}
|
||||
|
||||
val, decDiags := hcldec.Decode(body, schema.DecoderSpec(), ctx)
|
||||
diags = diags.Append(decDiags)
|
||||
diags = diags.Append(s.enhanceFunctionDiags(decDiags))
|
||||
return val, diags
|
||||
}
|
||||
|
||||
@ -175,7 +178,7 @@ func (s *Scope) EvalExpr(expr hcl.Expression, wantType cty.Type) (cty.Value, tfd
|
||||
}
|
||||
|
||||
val, evalDiags := expr.Value(ctx)
|
||||
diags = diags.Append(evalDiags)
|
||||
diags = diags.Append(s.enhanceFunctionDiags(evalDiags))
|
||||
|
||||
if wantType != cty.DynamicPseudoType {
|
||||
var convErr error
|
||||
@ -196,6 +199,62 @@ func (s *Scope) EvalExpr(expr hcl.Expression, wantType cty.Type) (cty.Value, tfd
|
||||
return val, diags
|
||||
}
|
||||
|
||||
// Common provider function namespace form
|
||||
var providerFuncNamespace = regexp.MustCompile("^([^:]*)::([^:]*)::$")
|
||||
|
||||
// Identify and enhance any function related dialogs produced by a hcl.EvalContext
|
||||
func (s *Scope) enhanceFunctionDiags(diags hcl.Diagnostics) hcl.Diagnostics {
|
||||
out := make(hcl.Diagnostics, len(diags))
|
||||
for i, diag := range diags {
|
||||
out[i] = diag
|
||||
|
||||
if funcExtra, ok := diag.Extra.(hclsyntax.FunctionCallUnknownDiagExtra); ok {
|
||||
funcName := funcExtra.CalledFunctionName()
|
||||
// prefix::stuff::
|
||||
fullNamespace := funcExtra.CalledFunctionNamespace()
|
||||
|
||||
if !strings.Contains(fullNamespace, "::") {
|
||||
// Not a namespaced function, no enhancements nessesary
|
||||
continue
|
||||
}
|
||||
|
||||
// Insert the enhanced copy of diag into diags
|
||||
enhanced := *diag
|
||||
out[i] = &enhanced
|
||||
|
||||
// Update enhanced with additional details
|
||||
|
||||
if fullNamespace == CoreNamespace {
|
||||
// Error is in core namespace, mirror non-core equivalent
|
||||
enhanced.Summary = "Call to unknown function"
|
||||
enhanced.Detail = fmt.Sprintf("There is no builtin (%s) function named %q.", CoreNamespace, funcName)
|
||||
continue
|
||||
}
|
||||
|
||||
match := providerFuncNamespace.FindSubmatch([]byte(fullNamespace))
|
||||
if match == nil || string(match[1]) != "provider" {
|
||||
// complete mismatch or invalid prefix
|
||||
enhanced.Summary = "Invalid function format"
|
||||
enhanced.Detail = fmt.Sprintf("Expected provider::<provider_name>::<function_name>, instead found \"%s%s\"", fullNamespace, funcName)
|
||||
continue
|
||||
}
|
||||
|
||||
providerName := string(match[2])
|
||||
addr, ok := s.ProviderNames[providerName]
|
||||
if !ok {
|
||||
// Provider not registered
|
||||
enhanced.Summary = "Unknown function provider"
|
||||
enhanced.Detail = fmt.Sprintf("Provider %q does not exist within the required_providers of this module", providerName)
|
||||
} else {
|
||||
// Func not in provider
|
||||
enhanced.Summary = "Function not found in provider"
|
||||
enhanced.Detail = fmt.Sprintf("Function %q was not registered by provider named %q of type %q", funcName, providerName, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// EvalReference evaluates the given reference in the receiving scope and
|
||||
// returns the resulting value. The value will be converted to the given type before
|
||||
// it is returned if possible, or else an error diagnostic will be produced
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"github.com/opentofu/opentofu/internal/instances"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hcldec"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
@ -850,3 +851,102 @@ func TestScopeEvalSelfBlock(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_enhanceFunctionDiags(t *testing.T) {
|
||||
tests := []struct {
|
||||
Name string
|
||||
Config string
|
||||
Summary string
|
||||
Detail string
|
||||
}{
|
||||
{
|
||||
"Missing builtin function",
|
||||
"attr = missing_function(54)",
|
||||
"Call to unknown function",
|
||||
"There is no function named \"missing_function\".",
|
||||
},
|
||||
{
|
||||
"Missing core function",
|
||||
"attr = core::missing_function(54)",
|
||||
"Call to unknown function",
|
||||
"There is no builtin (core::) function named \"missing_function\".",
|
||||
},
|
||||
{
|
||||
"Invalid prefix",
|
||||
"attr = magic::missing_function(54)",
|
||||
"Invalid function format",
|
||||
"Expected provider::<provider_name>::<function_name>, instead found \"magic::missing_function\"",
|
||||
},
|
||||
{
|
||||
"Broken prefix",
|
||||
"attr = magic::foo::bar::extra::missing_function(54)",
|
||||
"Invalid function format",
|
||||
"Expected provider::<provider_name>::<function_name>, instead found \"magic::foo::bar::extra::missing_function\"",
|
||||
},
|
||||
{
|
||||
"Missing provider",
|
||||
"attr = provider::unknown::func(54)",
|
||||
"Unknown function provider",
|
||||
"Provider \"unknown\" does not exist within the required_providers of this module",
|
||||
},
|
||||
{
|
||||
"Missing function",
|
||||
"attr = provider::known::func(54)",
|
||||
"Function not found in provider",
|
||||
"Function \"func\" was not registered by provider named \"known\" of type \"hostname/namespace/type\"",
|
||||
},
|
||||
}
|
||||
|
||||
schema := &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"attr": {
|
||||
Type: cty.String,
|
||||
},
|
||||
},
|
||||
}
|
||||
spec := schema.DecoderSpec()
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
file, parseDiags := hclsyntax.ParseConfig([]byte(test.Config), "", hcl.Pos{Line: 1, Column: 1})
|
||||
if len(parseDiags) != 0 {
|
||||
t.Errorf("unexpected diagnostics during parse")
|
||||
for _, diag := range parseDiags {
|
||||
t.Errorf("- %s", diag)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
body := file.Body
|
||||
|
||||
scope := &Scope{
|
||||
ProviderNames: map[string]addrs.Provider{
|
||||
"known": addrs.Provider{
|
||||
Type: "type",
|
||||
Namespace: "namespace",
|
||||
Hostname: "hostname",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx, ctxDiags := scope.EvalContext(nil)
|
||||
if ctxDiags.HasErrors() {
|
||||
t.Fatalf("Unexpected ctxDiags, %#v", ctxDiags)
|
||||
}
|
||||
|
||||
_, evalDiags := hcldec.Decode(body, spec, ctx)
|
||||
diags := scope.enhanceFunctionDiags(evalDiags)
|
||||
if len(diags) != 1 {
|
||||
t.Fatalf("Expected 1 diag, got %d", len(diags))
|
||||
}
|
||||
diag := diags[0]
|
||||
if diag.Summary != test.Summary {
|
||||
t.Fatalf("Expected Summary %q, got %q", test.Summary, diag.Summary)
|
||||
}
|
||||
if diag.Detail != test.Detail {
|
||||
t.Fatalf("Expected Detail %q, got %q", test.Detail, diag.Detail)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,8 @@ var impureFunctions = []string{
|
||||
"uuid",
|
||||
}
|
||||
|
||||
const CoreNamespace = "core::"
|
||||
|
||||
// Functions returns the set of functions that should be used to when evaluating
|
||||
// expressions in the receiving scope.
|
||||
func (s *Scope) Functions() map[string]function.Function {
|
||||
@ -189,12 +191,22 @@ func (s *Scope) Functions() map[string]function.Function {
|
||||
}
|
||||
}
|
||||
|
||||
coreNames := make([]string, 0)
|
||||
// Add a description to each function and parameter based on the
|
||||
// contents of descriptionList.
|
||||
// One must create a matching description entry whenever a new
|
||||
// function is introduced.
|
||||
for name, f := range s.funcs {
|
||||
s.funcs[name] = funcs.WithDescription(name, f)
|
||||
coreNames = append(coreNames, name)
|
||||
}
|
||||
// Copy all stdlib funcs into core:: namespace
|
||||
for _, name := range coreNames {
|
||||
s.funcs[CoreNamespace+name] = s.funcs[name]
|
||||
}
|
||||
|
||||
for name, f := range s.ProviderFunctions {
|
||||
s.funcs[name] = f
|
||||
}
|
||||
}
|
||||
s.funcsLock.Unlock()
|
||||
|
@ -6,6 +6,7 @@
|
||||
package lang
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/lang/funcs"
|
||||
@ -20,14 +21,15 @@ func TestFunctionDescriptions(t *testing.T) {
|
||||
allFunctions := scope.Functions()
|
||||
|
||||
// plantimestamp isn't available with ConsoleMode: true
|
||||
expectedFunctionCount := len(funcs.DescriptionList) - 1
|
||||
// THis also includes the core:: prefixed functions
|
||||
expectedFunctionCount := (len(funcs.DescriptionList) - 1) * 2
|
||||
|
||||
if len(allFunctions) != expectedFunctionCount {
|
||||
t.Errorf("DescriptionList length expected: %d, got %d", len(allFunctions), expectedFunctionCount)
|
||||
}
|
||||
|
||||
for name := range allFunctions {
|
||||
_, ok := funcs.DescriptionList[name]
|
||||
_, ok := funcs.DescriptionList[strings.TrimPrefix(name, CoreNamespace)]
|
||||
if !ok {
|
||||
t.Errorf("missing DescriptionList entry for function %q", name)
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -1246,9 +1247,10 @@ func TestFunctions(t *testing.T) {
|
||||
// suitable type.
|
||||
for _, impureFunc := range impureFunctions {
|
||||
delete(allFunctions, impureFunc)
|
||||
delete(allFunctions, CoreNamespace+impureFunc)
|
||||
}
|
||||
for f := range scope.Functions() {
|
||||
if _, ok := tests[f]; !ok {
|
||||
if _, ok := tests[strings.TrimPrefix(f, CoreNamespace)]; !ok {
|
||||
t.Errorf("Missing test for function %s\n", f)
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +71,9 @@ type Scope struct {
|
||||
// PlanTimestamp is a timestamp representing when the plan was made. It will
|
||||
// either have been generated during this operation or read from the plan.
|
||||
PlanTimestamp time.Time
|
||||
|
||||
ProviderFunctions map[string]function.Function
|
||||
ProviderNames map[string]addrs.Provider
|
||||
}
|
||||
|
||||
// SetActiveExperiments allows a caller to declare that a set of experiments
|
||||
|
@ -718,7 +718,8 @@ func (p *GRPCProvider) CallFunction(r providers.CallFunctionRequest) (resp provi
|
||||
// Translate the arguments
|
||||
// As this is functionality is always sitting behind cty/function.Function, we skip some validation
|
||||
// checks of from the function and param spec. We still include basic validation to prevent panics,
|
||||
// just in case there are bugs in cty
|
||||
// just in case there are bugs in cty. See context_functions_test.go for explicit testing of argument
|
||||
// handling and short-circuiting.
|
||||
if len(r.Arguments) < len(spec.Parameters) {
|
||||
// This should be unreachable
|
||||
resp.Error = fmt.Errorf("invalid CallFunctionRequest: function %s expected %d parameters and got %d instead", r.Name, len(spec.Parameters), len(r.Arguments))
|
||||
|
@ -707,7 +707,8 @@ func (p *GRPCProvider) CallFunction(r providers.CallFunctionRequest) (resp provi
|
||||
// Translate the arguments
|
||||
// As this is functionality is always sitting behind cty/function.Function, we skip some validation
|
||||
// checks of from the function and param spec. We still include basic validation to prevent panics,
|
||||
// just in case there are bugs in cty
|
||||
// just in case there are bugs in cty. See context_functions_test.go for explicit testing of argument
|
||||
// handling and short-circuiting.
|
||||
if len(r.Arguments) < len(spec.Parameters) {
|
||||
// This should be unreachable
|
||||
resp.Error = fmt.Errorf("invalid CallFunctionRequest: function %s expected %d parameters and got %d instead", r.Name, len(spec.Parameters), len(r.Arguments))
|
||||
|
@ -169,7 +169,7 @@ type FunctionParameterSpec struct {
|
||||
Type cty.Type
|
||||
// Null values alowed for the parameter
|
||||
AllowNullValue bool
|
||||
// Unknown Values allowd for the parameter
|
||||
// Unknown Values allowed for the parameter
|
||||
// Implies the Return type of the function is also Unknown
|
||||
AllowUnknownValues bool
|
||||
// Human-readable documentation for the parameter
|
||||
|
@ -134,7 +134,10 @@ func NewContext(opts *ContextOpts) (*Context, tfdiags.Diagnostics) {
|
||||
par = 10
|
||||
}
|
||||
|
||||
plugins := newContextPlugins(opts.Providers, opts.Provisioners)
|
||||
plugins, err := newContextPlugins(opts.Providers, opts.Provisioners)
|
||||
if err != nil {
|
||||
return nil, diags.Append(err)
|
||||
}
|
||||
|
||||
log.Printf("[TRACE] tofu.NewContext: complete")
|
||||
|
||||
|
109
internal/tofu/context_functions.go
Normal file
109
internal/tofu/context_functions.go
Normal file
@ -0,0 +1,109 @@
|
||||
package tofu
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/providers"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
)
|
||||
|
||||
// Lazily creates a single instance of a provider for repeated use.
|
||||
// Concurrency safe
|
||||
func lazyProviderInstance(addr addrs.Provider, factory providers.Factory) providers.Factory {
|
||||
var provider providers.Interface
|
||||
var providerLock sync.Mutex
|
||||
var err error
|
||||
|
||||
return func() (providers.Interface, error) {
|
||||
providerLock.Lock()
|
||||
defer providerLock.Unlock()
|
||||
|
||||
if provider == nil {
|
||||
log.Printf("[TRACE] tofu.contextFunctions: Initializing function provider %q", addr)
|
||||
provider, err = factory()
|
||||
}
|
||||
return provider, err
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through all functions specified and build a map of name -> function.
|
||||
// All functions will use the same lazily initialized provider instance.
|
||||
// This instance will run until the application is terminated.
|
||||
func providerFunctions(addr addrs.Provider, funcSpecs map[string]providers.FunctionSpec, factory providers.Factory) map[string]function.Function {
|
||||
lazy := lazyProviderInstance(addr, factory)
|
||||
|
||||
functions := make(map[string]function.Function)
|
||||
for name, spec := range funcSpecs {
|
||||
log.Printf("[TRACE] tofu.contextFunctions: Registering function %q in provider type %q", name, addr)
|
||||
if _, ok := functions[name]; ok {
|
||||
panic(fmt.Sprintf("broken provider %q: multiple functions registered under name %q", addr, name))
|
||||
}
|
||||
functions[name] = providerFunction(name, spec, lazy)
|
||||
}
|
||||
return functions
|
||||
}
|
||||
|
||||
// Turn a provider function spec into a cty callable function
|
||||
// This will use the instance factory to get a provider to support the
|
||||
// function call.
|
||||
func providerFunction(name string, spec providers.FunctionSpec, instance providers.Factory) function.Function {
|
||||
params := make([]function.Parameter, len(spec.Parameters))
|
||||
for i, param := range spec.Parameters {
|
||||
params[i] = providerFunctionParameter(param)
|
||||
}
|
||||
|
||||
var varParam *function.Parameter
|
||||
if spec.VariadicParameter != nil {
|
||||
value := providerFunctionParameter(*spec.VariadicParameter)
|
||||
varParam = &value
|
||||
}
|
||||
|
||||
impl := func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
||||
provider, err := instance()
|
||||
if err != nil {
|
||||
// Incredibly unlikely
|
||||
return cty.UnknownVal(retType), err
|
||||
}
|
||||
resp := provider.CallFunction(providers.CallFunctionRequest{
|
||||
Name: name,
|
||||
Arguments: args,
|
||||
})
|
||||
|
||||
if argError, ok := resp.Error.(*providers.CallFunctionArgumentError); ok {
|
||||
// Convert ArgumentError to cty error
|
||||
return resp.Result, function.NewArgError(argError.FunctionArgument, errors.New(argError.Text))
|
||||
}
|
||||
|
||||
return resp.Result, resp.Error
|
||||
}
|
||||
|
||||
return function.New(&function.Spec{
|
||||
Description: spec.Summary,
|
||||
Params: params,
|
||||
VarParam: varParam,
|
||||
Type: function.StaticReturnType(spec.Return),
|
||||
Impl: impl,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// Simple mapping of function parameter spec to function parameter
|
||||
func providerFunctionParameter(spec providers.FunctionParameterSpec) function.Parameter {
|
||||
return function.Parameter{
|
||||
Name: spec.Name,
|
||||
Description: spec.Description,
|
||||
Type: spec.Type,
|
||||
AllowNull: spec.AllowNullValue,
|
||||
AllowUnknown: spec.AllowUnknownValues,
|
||||
// I don't believe this is allowable for provider functions
|
||||
AllowDynamicType: false,
|
||||
// force cty to strip marks ahead of time and re-add them to the resulting object
|
||||
// GRPC: failed: value has marks, so it cannot be serialized.
|
||||
AllowMarked: false,
|
||||
}
|
||||
}
|
290
internal/tofu/context_functions_test.go
Normal file
290
internal/tofu/context_functions_test.go
Normal file
@ -0,0 +1,290 @@
|
||||
package tofu
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/lang/marks"
|
||||
"github.com/opentofu/opentofu/internal/providers"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestFunctions(t *testing.T) {
|
||||
mockProvider := &MockProvider{
|
||||
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
|
||||
Provider: providers.Schema{},
|
||||
Functions: map[string]providers.FunctionSpec{
|
||||
"echo": providers.FunctionSpec{
|
||||
Parameters: []providers.FunctionParameterSpec{providers.FunctionParameterSpec{
|
||||
Name: "input",
|
||||
Type: cty.String,
|
||||
AllowNullValue: false,
|
||||
AllowUnknownValues: false,
|
||||
}},
|
||||
Return: cty.String,
|
||||
},
|
||||
"concat": providers.FunctionSpec{
|
||||
Parameters: []providers.FunctionParameterSpec{providers.FunctionParameterSpec{
|
||||
Name: "input",
|
||||
Type: cty.String,
|
||||
AllowNullValue: false,
|
||||
AllowUnknownValues: false,
|
||||
}},
|
||||
VariadicParameter: &providers.FunctionParameterSpec{
|
||||
Name: "vary",
|
||||
Type: cty.String,
|
||||
AllowNullValue: false,
|
||||
},
|
||||
Return: cty.String,
|
||||
},
|
||||
"coalesce": providers.FunctionSpec{
|
||||
Parameters: []providers.FunctionParameterSpec{providers.FunctionParameterSpec{
|
||||
Name: "input1",
|
||||
Type: cty.String,
|
||||
AllowNullValue: true,
|
||||
AllowUnknownValues: false,
|
||||
}, providers.FunctionParameterSpec{
|
||||
Name: "input2",
|
||||
Type: cty.String,
|
||||
AllowNullValue: false,
|
||||
AllowUnknownValues: false,
|
||||
}},
|
||||
Return: cty.String,
|
||||
},
|
||||
"unknown_param": providers.FunctionSpec{
|
||||
Parameters: []providers.FunctionParameterSpec{providers.FunctionParameterSpec{
|
||||
Name: "input",
|
||||
Type: cty.String,
|
||||
AllowNullValue: false,
|
||||
AllowUnknownValues: true,
|
||||
}},
|
||||
Return: cty.String,
|
||||
},
|
||||
"error_param": providers.FunctionSpec{
|
||||
Parameters: []providers.FunctionParameterSpec{providers.FunctionParameterSpec{
|
||||
Name: "input",
|
||||
Type: cty.String,
|
||||
AllowNullValue: false,
|
||||
AllowUnknownValues: false,
|
||||
}},
|
||||
Return: cty.String,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockProvider.CallFunctionFn = func(req providers.CallFunctionRequest) (resp providers.CallFunctionResponse) {
|
||||
switch req.Name {
|
||||
case "echo":
|
||||
resp.Result = req.Arguments[0]
|
||||
case "concat":
|
||||
str := ""
|
||||
for _, arg := range req.Arguments {
|
||||
str += arg.AsString()
|
||||
}
|
||||
resp.Result = cty.StringVal(str)
|
||||
case "coalesce":
|
||||
resp.Result = req.Arguments[0]
|
||||
if resp.Result.IsNull() {
|
||||
resp.Result = req.Arguments[1]
|
||||
}
|
||||
case "unknown_param":
|
||||
resp.Result = cty.StringVal("knownvalue")
|
||||
case "error_param":
|
||||
resp.Error = &providers.CallFunctionArgumentError{
|
||||
Text: "my error text",
|
||||
FunctionArgument: 0,
|
||||
}
|
||||
default:
|
||||
panic("Invalid function")
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// Initial call to getSchema
|
||||
expectProviderInit := true
|
||||
|
||||
mockFactory := func() (providers.Interface, error) {
|
||||
if !expectProviderInit {
|
||||
return nil, fmt.Errorf("Unexpected call to provider init!")
|
||||
}
|
||||
expectProviderInit = false
|
||||
return mockProvider, nil
|
||||
}
|
||||
|
||||
addr := addrs.NewDefaultProvider("mock")
|
||||
plugins := newContextPluginsForTest(map[addrs.Provider]providers.Factory{
|
||||
addr: mockFactory,
|
||||
}, t)
|
||||
|
||||
t.Run("empty names map", func(t *testing.T) {
|
||||
res := plugins.Functions(map[string]addrs.Provider{})
|
||||
if len(res.ProviderNames) != 0 {
|
||||
t.Error("did not expect any names")
|
||||
}
|
||||
if len(res.Functions) != 0 {
|
||||
t.Error("did not expect any functions")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("broken names map", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Errorf("Expected panic due to broken configuration")
|
||||
}
|
||||
}()
|
||||
|
||||
res := plugins.Functions(map[string]addrs.Provider{
|
||||
"borky": addrs.NewDefaultProvider("my_borky"),
|
||||
})
|
||||
if len(res.ProviderNames) != 0 {
|
||||
t.Error("did not expect any names")
|
||||
}
|
||||
if len(res.Functions) != 0 {
|
||||
t.Error("did not expect any functions")
|
||||
}
|
||||
})
|
||||
|
||||
res := plugins.Functions(map[string]addrs.Provider{
|
||||
"mockname": addr,
|
||||
})
|
||||
if res.ProviderNames["mockname"] != addr {
|
||||
t.Errorf("expected names %q, got %q", addr, res.ProviderNames["mockname"])
|
||||
}
|
||||
|
||||
ctx := &hcl.EvalContext{
|
||||
Functions: res.Functions,
|
||||
Variables: map[string]cty.Value{
|
||||
"unknown_value": cty.UnknownVal(cty.String),
|
||||
"sensitive_value": cty.StringVal("sensitive!").Mark(marks.Sensitive),
|
||||
},
|
||||
}
|
||||
evaluate := func(exprStr string) (cty.Value, hcl.Diagnostics) {
|
||||
expr, diags := hclsyntax.ParseExpression([]byte(exprStr), "exprtest", hcl.InitialPos)
|
||||
if diags.HasErrors() {
|
||||
t.Fatal(diags)
|
||||
}
|
||||
return expr.Value(ctx)
|
||||
}
|
||||
|
||||
t.Run("echo function", func(t *testing.T) {
|
||||
// These are all assumptions that the provider implementation should not have to worry about:
|
||||
|
||||
t.Log("Checking not enough arguments")
|
||||
_, diags := evaluate("provider::mockname::echo()")
|
||||
if !strings.Contains(diags.Error(), `Not enough function arguments; Function "provider::mockname::echo" expects 1 argument(s). Missing value for "input"`) {
|
||||
t.Error(diags.Error())
|
||||
}
|
||||
|
||||
t.Log("Checking too many arguments")
|
||||
_, diags = evaluate(`provider::mockname::echo("1", "2", "3")`)
|
||||
if !strings.Contains(diags.Error(), `Too many function arguments; Function "provider::mockname::echo" expects only 1 argument(s)`) {
|
||||
t.Error(diags.Error())
|
||||
}
|
||||
|
||||
t.Log("Checking null argument")
|
||||
_, diags = evaluate(`provider::mockname::echo(null)`)
|
||||
if !strings.Contains(diags.Error(), `Invalid function argument; Invalid value for "input" parameter: argument must not be null`) {
|
||||
t.Error(diags.Error())
|
||||
}
|
||||
|
||||
t.Log("Checking unknown argument")
|
||||
val, diags := evaluate(`provider::mockname::echo(unknown_value)`)
|
||||
if diags.HasErrors() {
|
||||
t.Error(diags.Error())
|
||||
}
|
||||
if !val.RawEquals(cty.UnknownVal(cty.String)) {
|
||||
t.Error(val.AsString())
|
||||
}
|
||||
|
||||
// Actually test the function implementation
|
||||
|
||||
// Do this a few times but only expect a single init()
|
||||
expectProviderInit = true
|
||||
for i := 0; i < 5; i++ {
|
||||
t.Log("Checking valid argument")
|
||||
|
||||
val, diags = evaluate(`provider::mockname::echo("hello functions!")`)
|
||||
if diags.HasErrors() {
|
||||
t.Error(diags.Error())
|
||||
}
|
||||
if !val.RawEquals(cty.StringVal("hello functions!")) {
|
||||
t.Error(val.AsString())
|
||||
}
|
||||
|
||||
if expectProviderInit {
|
||||
t.Error("Expected provider init to have been called")
|
||||
}
|
||||
}
|
||||
|
||||
t.Log("Checking sensitive argument")
|
||||
|
||||
val, diags = evaluate(`provider::mockname::echo(sensitive_value)`)
|
||||
if diags.HasErrors() {
|
||||
t.Error(diags.Error())
|
||||
}
|
||||
if !val.RawEquals(cty.StringVal("sensitive!").Mark(marks.Sensitive)) {
|
||||
t.Error(val.AsString())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("concat function", func(t *testing.T) {
|
||||
// Make sure varargs are handled properly
|
||||
|
||||
// Single
|
||||
val, diags := evaluate(`provider::mockname::concat("foo")`)
|
||||
if diags.HasErrors() {
|
||||
t.Error(diags.Error())
|
||||
}
|
||||
if !val.RawEquals(cty.StringVal("foo")) {
|
||||
t.Error(val.AsString())
|
||||
}
|
||||
|
||||
// Multi
|
||||
val, diags = evaluate(`provider::mockname::concat("foo", "bar", "baz")`)
|
||||
if diags.HasErrors() {
|
||||
t.Error(diags.Error())
|
||||
}
|
||||
if !val.RawEquals(cty.StringVal("foobarbaz")) {
|
||||
t.Error(val.AsString())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("coalesce function", func(t *testing.T) {
|
||||
val, diags := evaluate(`provider::mockname::coalesce("first", "second")`)
|
||||
if diags.HasErrors() {
|
||||
t.Error(diags.Error())
|
||||
}
|
||||
if !val.RawEquals(cty.StringVal("first")) {
|
||||
t.Error(val.AsString())
|
||||
}
|
||||
|
||||
val, diags = evaluate(`provider::mockname::coalesce(null, "second")`)
|
||||
if diags.HasErrors() {
|
||||
t.Error(diags.Error())
|
||||
}
|
||||
if !val.RawEquals(cty.StringVal("second")) {
|
||||
t.Error(val.AsString())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unknown_param function", func(t *testing.T) {
|
||||
val, diags := evaluate(`provider::mockname::unknown_param(unknown_value)`)
|
||||
if diags.HasErrors() {
|
||||
t.Error(diags.Error())
|
||||
}
|
||||
if !val.RawEquals(cty.StringVal("knownvalue")) {
|
||||
t.Error(val.AsString())
|
||||
}
|
||||
})
|
||||
t.Run("error_param function", func(t *testing.T) {
|
||||
_, diags := evaluate(`provider::mockname::error_param("foo")`)
|
||||
if !strings.Contains(diags.Error(), `Invalid function argument; Invalid value for "input" parameter: my error text.`) {
|
||||
t.Error(diags.Error())
|
||||
}
|
||||
})
|
||||
}
|
@ -13,6 +13,7 @@ import (
|
||||
"github.com/opentofu/opentofu/internal/configs/configschema"
|
||||
"github.com/opentofu/opentofu/internal/providers"
|
||||
"github.com/opentofu/opentofu/internal/provisioners"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
)
|
||||
|
||||
// contextPlugins represents a library of available plugins (providers and
|
||||
@ -21,15 +22,50 @@ import (
|
||||
// about the providers for performance reasons.
|
||||
type contextPlugins struct {
|
||||
providerFactories map[addrs.Provider]providers.Factory
|
||||
providerFunctions map[addrs.Provider]map[string]function.Function
|
||||
provisionerFactories map[string]provisioners.Factory
|
||||
}
|
||||
|
||||
func newContextPlugins(providerFactories map[addrs.Provider]providers.Factory, provisionerFactories map[string]provisioners.Factory) *contextPlugins {
|
||||
func newContextPlugins(providerFactories map[addrs.Provider]providers.Factory, provisionerFactories map[string]provisioners.Factory) (*contextPlugins, error) {
|
||||
ret := &contextPlugins{
|
||||
providerFactories: providerFactories,
|
||||
provisionerFactories: provisionerFactories,
|
||||
}
|
||||
return ret
|
||||
|
||||
// This is a bit convoluted as we need to use the ProviderSchema function call below to
|
||||
// validate and initialize the provider schemas. Long term the whole provider abstraction
|
||||
// needs to be re-thought.
|
||||
var err error
|
||||
ret.providerFunctions, err = ret.buildProviderFunctions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Loop through all of the providerFactories and build a map of addr -> functions
|
||||
// As a side effect, this initialzes the schema cache if not already initialized, with the proper validation path.
|
||||
func (cp *contextPlugins) buildProviderFunctions() (map[addrs.Provider]map[string]function.Function, error) {
|
||||
funcs := make(map[addrs.Provider]map[string]function.Function)
|
||||
|
||||
// Pull all functions out of given providers
|
||||
for addr, factory := range cp.providerFactories {
|
||||
addr := addr
|
||||
factory := factory
|
||||
|
||||
// Before functions, the provider schemas were already pre-loaded and cached. That initial caching
|
||||
// has been moved here. When the provider abstraction layers are refactored, this could instead
|
||||
// expose and use provider.GetFunctions instead of needing to load and cache the whole schema.
|
||||
// However, at the time of writing there is no benefit to defer caching these schemas in code
|
||||
// paths which build a tofu.Context.
|
||||
schema, err := cp.ProviderSchema(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
funcs[addr] = providerFunctions(addr, schema.Functions, factory)
|
||||
}
|
||||
return funcs, nil
|
||||
}
|
||||
|
||||
func (cp *contextPlugins) HasProvider(addr addrs.Provider) bool {
|
||||
@ -77,6 +113,7 @@ func (cp *contextPlugins) ProviderSchema(addr addrs.Provider) (providers.Provide
|
||||
// That is because we're checking *prior* to the provider's instantiation.
|
||||
// GetProviderSchemaOptional only says that *if we instantiate a provider*,
|
||||
// then we need to run the get schema call at least once.
|
||||
// BUG This SHORT CIRCUITS the logic below and is not the only code which inserts provider schemas into the cache!!
|
||||
schemas, ok := providers.SchemaCache.Get(addr)
|
||||
if ok {
|
||||
log.Printf("[TRACE] tofu.contextPlugins: Serving provider %q schema from global schema cache", addr)
|
||||
@ -179,3 +216,26 @@ func (cp *contextPlugins) ProvisionerSchema(typ string) (*configschema.Block, er
|
||||
|
||||
return resp.Provisioner, nil
|
||||
}
|
||||
|
||||
type ProviderFunctions struct {
|
||||
ProviderNames map[string]addrs.Provider
|
||||
Functions map[string]function.Function
|
||||
}
|
||||
|
||||
// Functions provides a map of provider::<provider_name>::<function> for a given provider type.
|
||||
// All providers of a given type use the same functions and provider instance and
|
||||
// additional names do not incur any performance penalty.
|
||||
func (cp *contextPlugins) Functions(names map[string]addrs.Provider) *ProviderFunctions {
|
||||
providerFuncs := &ProviderFunctions{
|
||||
ProviderNames: names,
|
||||
Functions: make(map[string]function.Function),
|
||||
}
|
||||
|
||||
for name, addr := range names {
|
||||
funcs := cp.providerFunctions[addr]
|
||||
for fn_name, fn := range funcs {
|
||||
providerFuncs.Functions[fmt.Sprintf("provider::%s::%s", name, fn_name)] = fn
|
||||
}
|
||||
}
|
||||
return providerFuncs
|
||||
}
|
||||
|
@ -6,6 +6,8 @@
|
||||
package tofu
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
@ -85,3 +87,11 @@ func simpleTestSchema() *configschema.Block {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newContextPluginsForTest(providerFactories map[addrs.Provider]providers.Factory, t *testing.T) *contextPlugins {
|
||||
plugins, err := newContextPlugins(providerFactories, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
return plugins
|
||||
}
|
||||
|
@ -69,6 +69,8 @@ type BuiltinEvalContext struct {
|
||||
ProviderLock *sync.Mutex
|
||||
ProvisionerCache map[string]provisioners.Interface
|
||||
ProvisionerLock *sync.Mutex
|
||||
FunctionCache *ProviderFunctions
|
||||
FunctionLock sync.Mutex
|
||||
ChangesValue *plans.ChangesSync
|
||||
StateValue *states.SyncState
|
||||
ChecksValue *checks.State
|
||||
@ -87,6 +89,8 @@ func (ctx *BuiltinEvalContext) WithPath(path addrs.ModuleInstance) EvalContext {
|
||||
newCtx := *ctx
|
||||
newCtx.pathSet = true
|
||||
newCtx.PathValue = path
|
||||
newCtx.FunctionCache = nil
|
||||
newCtx.FunctionLock = sync.Mutex{}
|
||||
return &newCtx
|
||||
}
|
||||
|
||||
@ -403,7 +407,6 @@ func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, source
|
||||
InstanceKeyData: keyData,
|
||||
Operation: ctx.Evaluator.Operation,
|
||||
}
|
||||
scope := ctx.Evaluator.Scope(data, self, source)
|
||||
|
||||
// ctx.PathValue is the path of the module that contains whatever
|
||||
// expression the caller will be trying to evaluate, so this will
|
||||
@ -412,9 +415,28 @@ func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, source
|
||||
// package itself works. The nil check here is for robustness in
|
||||
// incompletely-mocked testing situations; mc should never be nil in
|
||||
// real situations.
|
||||
if mc := ctx.Evaluator.Config.DescendentForInstance(ctx.PathValue); mc != nil {
|
||||
scope.SetActiveExperiments(mc.Module.ActiveExperiments)
|
||||
mc := ctx.Evaluator.Config.DescendentForInstance(ctx.PathValue)
|
||||
|
||||
if mc == nil || mc.Module.ProviderRequirements == nil {
|
||||
return ctx.Evaluator.Scope(data, self, source, nil)
|
||||
}
|
||||
|
||||
ctx.FunctionLock.Lock()
|
||||
defer ctx.FunctionLock.Unlock()
|
||||
if ctx.FunctionCache == nil {
|
||||
names := make(map[string]addrs.Provider)
|
||||
|
||||
// Providers must exist within required_providers to register their functions
|
||||
for name, provider := range mc.Module.ProviderRequirements.RequiredProviders {
|
||||
// Functions are only registered under their name, not their type name
|
||||
names[name] = provider.Type
|
||||
}
|
||||
|
||||
ctx.FunctionCache = ctx.Plugins.Functions(names)
|
||||
}
|
||||
scope := ctx.Evaluator.Scope(data, self, source, ctx.FunctionCache)
|
||||
scope.SetActiveExperiments(mc.Module.ActiveExperiments)
|
||||
|
||||
return scope
|
||||
}
|
||||
|
||||
|
@ -64,9 +64,9 @@ func TestBuildingEvalContextInitProvider(t *testing.T) {
|
||||
ctx = ctx.WithPath(addrs.RootModuleInstance).(*BuiltinEvalContext)
|
||||
ctx.ProviderLock = &lock
|
||||
ctx.ProviderCache = make(map[string]providers.Interface)
|
||||
ctx.Plugins = newContextPlugins(map[addrs.Provider]providers.Factory{
|
||||
ctx.Plugins = newContextPluginsForTest(map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): providers.FactoryFixed(testP),
|
||||
}, nil)
|
||||
}, t)
|
||||
|
||||
providerAddrDefault := addrs.AbsProviderConfig{
|
||||
Module: addrs.RootModule,
|
||||
|
@ -76,7 +76,10 @@ type Evaluator struct {
|
||||
// If the "self" argument is nil then the "self" object is not available
|
||||
// in evaluated expressions. Otherwise, it behaves as an alias for the given
|
||||
// address.
|
||||
func (e *Evaluator) Scope(data lang.Data, self addrs.Referenceable, source addrs.Referenceable) *lang.Scope {
|
||||
func (e *Evaluator) Scope(data lang.Data, self addrs.Referenceable, source addrs.Referenceable, functions *ProviderFunctions) *lang.Scope {
|
||||
if functions == nil {
|
||||
functions = new(ProviderFunctions)
|
||||
}
|
||||
return &lang.Scope{
|
||||
Data: data,
|
||||
ParseRef: addrs.ParseRef,
|
||||
@ -85,6 +88,9 @@ func (e *Evaluator) Scope(data lang.Data, self addrs.Referenceable, source addrs
|
||||
PureOnly: e.Operation != walkApply && e.Operation != walkDestroy && e.Operation != walkEval,
|
||||
BaseDir: ".", // Always current working directory for now.
|
||||
PlanTimestamp: e.PlanTimestamp,
|
||||
// Can't pass the object directly as it would cause an import loop
|
||||
ProviderNames: functions.ProviderNames,
|
||||
ProviderFunctions: functions.Functions,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ func TestEvaluatorGetTerraformAttr(t *testing.T) {
|
||||
data := &evaluationStateData{
|
||||
Evaluator: evaluator,
|
||||
}
|
||||
scope := evaluator.Scope(data, nil, nil)
|
||||
scope := evaluator.Scope(data, nil, nil, nil)
|
||||
|
||||
t.Run("workspace", func(t *testing.T) {
|
||||
want := cty.StringVal("foo")
|
||||
@ -61,7 +61,7 @@ func TestEvaluatorGetPathAttr(t *testing.T) {
|
||||
data := &evaluationStateData{
|
||||
Evaluator: evaluator,
|
||||
}
|
||||
scope := evaluator.Scope(data, nil, nil)
|
||||
scope := evaluator.Scope(data, nil, nil, nil)
|
||||
|
||||
t.Run("module", func(t *testing.T) {
|
||||
want := cty.StringVal("bar/baz")
|
||||
@ -127,7 +127,7 @@ func TestEvaluatorGetOutputValue(t *testing.T) {
|
||||
data := &evaluationStateData{
|
||||
Evaluator: evaluator,
|
||||
}
|
||||
scope := evaluator.Scope(data, nil, nil)
|
||||
scope := evaluator.Scope(data, nil, nil, nil)
|
||||
|
||||
want := cty.StringVal("first").Mark(marks.Sensitive)
|
||||
got, diags := scope.Data.GetOutput(addrs.OutputValue{
|
||||
@ -194,7 +194,7 @@ func TestEvaluatorGetInputVariable(t *testing.T) {
|
||||
data := &evaluationStateData{
|
||||
Evaluator: evaluator,
|
||||
}
|
||||
scope := evaluator.Scope(data, nil, nil)
|
||||
scope := evaluator.Scope(data, nil, nil, nil)
|
||||
|
||||
want := cty.StringVal("bar").Mark(marks.Sensitive)
|
||||
got, diags := scope.Data.GetInputVariable(addrs.InputVariable{
|
||||
@ -338,13 +338,13 @@ func TestEvaluatorGetResource(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}, t),
|
||||
}
|
||||
|
||||
data := &evaluationStateData{
|
||||
Evaluator: evaluator,
|
||||
}
|
||||
scope := evaluator.Scope(data, nil, nil)
|
||||
scope := evaluator.Scope(data, nil, nil, nil)
|
||||
|
||||
want := cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("foo"),
|
||||
@ -504,13 +504,13 @@ func TestEvaluatorGetResource_changes(t *testing.T) {
|
||||
},
|
||||
},
|
||||
State: stateSync,
|
||||
Plugins: schemaOnlyProvidersForTesting(schemas.Providers),
|
||||
Plugins: schemaOnlyProvidersForTesting(schemas.Providers, t),
|
||||
}
|
||||
|
||||
data := &evaluationStateData{
|
||||
Evaluator: evaluator,
|
||||
}
|
||||
scope := evaluator.Scope(data, nil, nil)
|
||||
scope := evaluator.Scope(data, nil, nil, nil)
|
||||
|
||||
want := cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("foo"),
|
||||
@ -545,7 +545,7 @@ func TestEvaluatorGetModule(t *testing.T) {
|
||||
data := &evaluationStateData{
|
||||
Evaluator: evaluator,
|
||||
}
|
||||
scope := evaluator.Scope(data, nil, nil)
|
||||
scope := evaluator.Scope(data, nil, nil, nil)
|
||||
want := cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("bar").Mark(marks.Sensitive)})
|
||||
got, diags := scope.Data.GetModule(addrs.ModuleCall{
|
||||
Name: "mod",
|
||||
@ -573,7 +573,7 @@ func TestEvaluatorGetModule(t *testing.T) {
|
||||
data = &evaluationStateData{
|
||||
Evaluator: evaluator,
|
||||
}
|
||||
scope = evaluator.Scope(data, nil, nil)
|
||||
scope = evaluator.Scope(data, nil, nil, nil)
|
||||
want = cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("baz").Mark(marks.Sensitive)})
|
||||
got, diags = scope.Data.GetModule(addrs.ModuleCall{
|
||||
Name: "mod",
|
||||
@ -591,7 +591,7 @@ func TestEvaluatorGetModule(t *testing.T) {
|
||||
data = &evaluationStateData{
|
||||
Evaluator: evaluator,
|
||||
}
|
||||
scope = evaluator.Scope(data, nil, nil)
|
||||
scope = evaluator.Scope(data, nil, nil, nil)
|
||||
want = cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("baz").Mark(marks.Sensitive)})
|
||||
got, diags = scope.Data.GetModule(addrs.ModuleCall{
|
||||
Name: "mod",
|
||||
|
@ -114,7 +114,7 @@ For example, to correlate with indices of a referring resource, use:
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}, t),
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
@ -732,9 +732,9 @@ func TestApplyGraphBuilder_withChecks(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
plugins := newContextPlugins(map[addrs.Provider]providers.Factory{
|
||||
plugins := newContextPluginsForTest(map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("aws"): providers.FactoryFixed(awsProvider),
|
||||
}, nil)
|
||||
}, t)
|
||||
|
||||
b := &ApplyGraphBuilder{
|
||||
Config: testModule(t, "apply-with-checks"),
|
||||
|
@ -33,10 +33,10 @@ func TestPlanGraphBuilder(t *testing.T) {
|
||||
},
|
||||
}
|
||||
openstackProvider := mockProviderWithResourceTypeSchema("openstack_floating_ip", simpleTestSchema())
|
||||
plugins := newContextPlugins(map[addrs.Provider]providers.Factory{
|
||||
plugins := newContextPluginsForTest(map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("aws"): providers.FactoryFixed(awsProvider),
|
||||
addrs.NewDefaultProvider("openstack"): providers.FactoryFixed(openstackProvider),
|
||||
}, nil)
|
||||
}, t)
|
||||
|
||||
b := &PlanGraphBuilder{
|
||||
Config: testModule(t, "graph-builder-plan-basic"),
|
||||
@ -77,9 +77,9 @@ func TestPlanGraphBuilder_dynamicBlock(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
plugins := newContextPlugins(map[addrs.Provider]providers.Factory{
|
||||
plugins := newContextPluginsForTest(map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): providers.FactoryFixed(provider),
|
||||
}, nil)
|
||||
}, t)
|
||||
|
||||
b := &PlanGraphBuilder{
|
||||
Config: testModule(t, "graph-builder-plan-dynblock"),
|
||||
@ -133,9 +133,9 @@ func TestPlanGraphBuilder_attrAsBlocks(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
plugins := newContextPlugins(map[addrs.Provider]providers.Factory{
|
||||
plugins := newContextPluginsForTest(map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): providers.FactoryFixed(provider),
|
||||
}, nil)
|
||||
}, t)
|
||||
|
||||
b := &PlanGraphBuilder{
|
||||
Config: testModule(t, "graph-builder-plan-attr-as-blocks"),
|
||||
@ -198,9 +198,9 @@ func TestPlanGraphBuilder_targetModule(t *testing.T) {
|
||||
func TestPlanGraphBuilder_forEach(t *testing.T) {
|
||||
awsProvider := mockProviderWithResourceTypeSchema("aws_instance", simpleTestSchema())
|
||||
|
||||
plugins := newContextPlugins(map[addrs.Provider]providers.Factory{
|
||||
plugins := newContextPluginsForTest(map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("aws"): providers.FactoryFixed(awsProvider),
|
||||
}, nil)
|
||||
}, t)
|
||||
|
||||
b := &PlanGraphBuilder{
|
||||
Config: testModule(t, "plan-for-each"),
|
||||
|
@ -87,6 +87,11 @@ type MockProvider struct {
|
||||
ReadDataSourceRequest providers.ReadDataSourceRequest
|
||||
ReadDataSourceFn func(providers.ReadDataSourceRequest) providers.ReadDataSourceResponse
|
||||
|
||||
CallFunctionCalled bool
|
||||
CallFunctionResponse *providers.CallFunctionResponse
|
||||
CallFunctionRequest providers.CallFunctionRequest
|
||||
CallFunctionFn func(providers.CallFunctionRequest) providers.CallFunctionResponse
|
||||
|
||||
CloseCalled bool
|
||||
CloseError error
|
||||
}
|
||||
@ -516,8 +521,21 @@ func (p *MockProvider) ReadDataSource(r providers.ReadDataSourceRequest) (resp p
|
||||
return resp
|
||||
}
|
||||
|
||||
func (p *MockProvider) CallFunction(r providers.CallFunctionRequest) providers.CallFunctionResponse {
|
||||
panic("Not Implemented")
|
||||
func (p *MockProvider) CallFunction(r providers.CallFunctionRequest) (resp providers.CallFunctionResponse) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
p.CallFunctionCalled = true
|
||||
p.CallFunctionRequest = r
|
||||
|
||||
if p.CallFunctionFn != nil {
|
||||
return p.CallFunctionFn(r)
|
||||
}
|
||||
|
||||
if p.CallFunctionResponse != nil {
|
||||
resp = *p.CallFunctionResponse
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func (p *MockProvider) Close() error {
|
||||
|
@ -6,6 +6,8 @@
|
||||
package tofu
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/configs/configschema"
|
||||
"github.com/opentofu/opentofu/internal/providers"
|
||||
@ -33,7 +35,7 @@ func simpleTestSchemas() *Schemas {
|
||||
// The intended use for this is in testing components that use schemas to
|
||||
// drive other behavior, such as reference analysis during graph construction,
|
||||
// but that don't actually need to interact with providers otherwise.
|
||||
func schemaOnlyProvidersForTesting(schemas map[addrs.Provider]providers.ProviderSchema) *contextPlugins {
|
||||
func schemaOnlyProvidersForTesting(schemas map[addrs.Provider]providers.ProviderSchema, t *testing.T) *contextPlugins {
|
||||
factories := make(map[addrs.Provider]providers.Factory, len(schemas))
|
||||
|
||||
for providerAddr, schema := range schemas {
|
||||
@ -48,5 +50,5 @@ func schemaOnlyProvidersForTesting(schemas map[addrs.Provider]providers.Provider
|
||||
}
|
||||
}
|
||||
|
||||
return newContextPlugins(factories, nil)
|
||||
return newContextPluginsForTest(factories, t)
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ func TestTransitiveReductionTransformer(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}, t),
|
||||
}
|
||||
if err := transform.Transform(&g); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
|
@ -1,10 +1,12 @@
|
||||
---
|
||||
description: >-
|
||||
An introduction to the built-in functions that you can use to transform and
|
||||
An introduction to the functions which can be used to transform and
|
||||
combine values in expressions.
|
||||
---
|
||||
|
||||
# Built-in Functions
|
||||
# Functions
|
||||
|
||||
## Built-in Functions
|
||||
|
||||
The OpenTofu language includes a number of built-in functions that you can
|
||||
call from within expressions to transform and combine values. The general
|
||||
@ -19,9 +21,6 @@ For more details on syntax, see
|
||||
[_Function Calls_](/docs/language/expressions/function-calls)
|
||||
in the Expressions section.
|
||||
|
||||
The OpenTofu language does not support user-defined functions, and so only
|
||||
the functions built in to the language are available for use. The documentation includes a page for all of the available built-in functions.
|
||||
|
||||
You can experiment with the behavior of OpenTofu's built-in functions from
|
||||
the OpenTofu expression console, by running
|
||||
[the `tofu console` command](/docs/cli/commands/console):
|
||||
@ -33,3 +32,36 @@ the OpenTofu expression console, by running
|
||||
|
||||
The examples in the documentation for each function use console output to
|
||||
illustrate the result of calling the function with different parameters.
|
||||
|
||||
## Provider-defined Functions
|
||||
|
||||
As of OpenTofu 1.7.0, providers may define their own functions to be available during
|
||||
execution.
|
||||
|
||||
OpenTofu iterates through the [required_providers](/docs/language/providers/requirements/) block
|
||||
and queries the specified providers for any functions they wish to register. Functions are
|
||||
added to the current module's context under `provider::<provider_name>::<function_name>`.
|
||||
Functions are scoped to the module that requires the provider and are not inherited by child modules.
|
||||
|
||||
### Example:
|
||||
|
||||
```hcl
|
||||
terraform {
|
||||
required_providers {
|
||||
myhelper = {
|
||||
# yantrio/helpers registers a function named "echo"
|
||||
source = "yantrio/helpers"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
myval = provider::myhelper::echo("Hello Functions!")
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Notes for Provider Authors:
|
||||
* Support for functions was added in protocol version 5.5 and 6.5
|
||||
* OpenTofu's provider protocol is compatible with Terraform's provider protocol
|
||||
* CallFunction is executed on an *Unconfigured* instance of the provider
|
||||
|
Loading…
Reference in New Issue
Block a user